From d2b2491211a62b6f6fd5734263add6c4164dd363 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 8 Oct 2024 12:25:41 -0400 Subject: [PATCH] Add markdown alert parsing, fix buffer issue when switching files (#988) Adds the GitHub alert syntax parsing to the markdown element, fixes an issue where the file edit buffer was not getting unset when the file path changed, continues my crusade on star imports --- SECURITY.md | 2 +- frontend/app/element/markdown.tsx | 3 +- frontend/app/view/preview/preview.tsx | 223 +++++++++++++------------- package.json | 1 + yarn.lock | 10 ++ 5 files changed, 123 insertions(+), 116 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 10f45830a..966322e19 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,4 +2,4 @@ To report vulnerabilities or security concerns, please email us at: [security@commandline.dev](mailto:security@commandline.dev) -** Please do not report security vulnerabilities through public github issues. ** \ No newline at end of file +**Please do not report security vulnerabilities through public github issues.** \ No newline at end of file diff --git a/frontend/app/element/markdown.tsx b/frontend/app/element/markdown.tsx index f605c66db..07f745546 100644 --- a/frontend/app/element/markdown.tsx +++ b/frontend/app/element/markdown.tsx @@ -17,6 +17,7 @@ import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import rehypeSlug from "rehype-slug"; import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc"; import remarkGfm from "remark-gfm"; +import { remarkAlert } from "remark-github-blockquote-alert"; import { openLink } from "../store/global"; import { IconButton } from "./iconbutton"; import "./markdown.less"; @@ -258,7 +259,7 @@ const Markdown = ({ options={{ scrollbars: { autoHide: "leave" } }} > ; - viewIcon: jotai.Atom; - viewName: jotai.Atom; - viewText: jotai.Atom; - preIconButton: jotai.Atom; - endIconButtons: jotai.Atom; + blockAtom: Atom; + viewIcon: Atom; + viewName: Atom; + viewText: Atom; + preIconButton: Atom; + endIconButtons: Atom; previewTextRef: React.RefObject; - editMode: jotai.Atom; - canPreview: jotai.PrimitiveAtom; - specializedView: jotai.Atom>; - loadableSpecializedView: jotai.Atom>; - manageConnection: jotai.Atom; - connStatus: jotai.Atom; + editMode: Atom; + canPreview: PrimitiveAtom; + specializedView: Atom>; + loadableSpecializedView: Atom>; + manageConnection: Atom; + connStatus: Atom; - metaFilePath: jotai.Atom; - statFilePath: jotai.Atom>; - normFilePath: jotai.Atom>; - loadableStatFilePath: jotai.Atom>; - loadableFileInfo: jotai.Atom>; - connection: jotai.Atom; - statFile: jotai.Atom>; - fullFile: jotai.Atom>; - fileMimeType: jotai.Atom>; - fileMimeTypeLoadable: jotai.Atom>; - fileContentSaved: jotai.PrimitiveAtom; - fileContent: jotai.WritableAtom, [string], void>; - newFileContent: jotai.PrimitiveAtom; + metaFilePath: Atom; + statFilePath: Atom>; + normFilePath: Atom>; + loadableStatFilePath: Atom>; + loadableFileInfo: Atom>; + connection: Atom; + statFile: Atom>; + fullFile: Atom>; + fileMimeType: Atom>; + fileMimeTypeLoadable: Atom>; + fileContentSaved: PrimitiveAtom; + fileContent: WritableAtom, [string], void>; + newFileContent: PrimitiveAtom; - openFileModal: jotai.PrimitiveAtom; - openFileError: jotai.PrimitiveAtom; + openFileModal: PrimitiveAtom; + openFileError: PrimitiveAtom; openFileModalGiveFocusRef: React.MutableRefObject<() => boolean>; - markdownShowToc: jotai.PrimitiveAtom; + markdownShowToc: PrimitiveAtom; monacoRef: React.MutableRefObject; - showHiddenFiles: jotai.PrimitiveAtom; - refreshVersion: jotai.PrimitiveAtom; + showHiddenFiles: PrimitiveAtom; + refreshVersion: PrimitiveAtom; refreshCallback: () => void; directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; - setPreviewFileName(fileName: string) { - globalStore.set(this.fileContentSaved, null); - globalStore.set(this.newFileContent, null); - services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName }); - } - constructor(blockId: string, nodeModel: NodeModel) { this.viewType = "preview"; this.blockId = blockId; this.nodeModel = nodeModel; let showHiddenFiles = globalStore.get(getSettingsKeyAtom("preview:showhiddenfiles")) ?? true; - this.showHiddenFiles = jotai.atom(showHiddenFiles); - this.refreshVersion = jotai.atom(0); + this.showHiddenFiles = atom(showHiddenFiles); + this.refreshVersion = atom(0); this.previewTextRef = createRef(); - this.openFileModal = jotai.atom(false); - this.openFileError = jotai.atom(null) as jotai.PrimitiveAtom; + this.openFileModal = atom(false); + this.openFileError = atom(null) as PrimitiveAtom; this.openFileModalGiveFocusRef = createRef(); - this.manageConnection = jotai.atom(true); + this.manageConnection = atom(true); this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); - this.markdownShowToc = jotai.atom(false); + this.markdownShowToc = atom(false); this.monacoRef = createRef(); - this.viewIcon = jotai.atom((get) => { + this.viewIcon = atom((get) => { const blockData = get(this.blockAtom); if (blockData?.meta?.icon) { return blockData.meta.icon; @@ -173,9 +165,8 @@ export class PreviewModel implements ViewModel { if (connStatus?.status != "connected") { return null; } - const fileName = get(this.metaFilePath); const mimeTypeLoadable = get(this.fileMimeTypeLoadable); - const mimeType = util.jotaiLoadableValue(mimeTypeLoadable, ""); + const mimeType = jotaiLoadableValue(mimeTypeLoadable, ""); if (mimeType == "directory") { return { elemtype: "iconbutton", @@ -208,12 +199,12 @@ export class PreviewModel implements ViewModel { } return iconForFile(mimeType); }); - this.editMode = jotai.atom((get) => { + this.editMode = atom((get) => { const blockData = get(this.blockAtom); return blockData?.meta?.edit ?? false; }); - this.viewName = jotai.atom("Preview"); - this.viewText = jotai.atom((get) => { + this.viewName = atom("Preview"); + this.viewText = atom((get) => { let headerPath = get(this.metaFilePath); const connStatus = get(this.connStatus); if (connStatus?.status != "connected") { @@ -282,12 +273,12 @@ export class PreviewModel implements ViewModel { }, ] as HeaderElem[]; }); - this.preIconButton = jotai.atom((get) => { + this.preIconButton = atom((get) => { const connStatus = get(this.connStatus); if (connStatus?.status != "connected") { return null; } - const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); + const mimeType = jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); const metaPath = get(this.metaFilePath); if (mimeType == "directory" && metaPath == "/") { return null; @@ -298,12 +289,12 @@ export class PreviewModel implements ViewModel { click: this.goParentDirectory.bind(this), }; }); - this.endIconButtons = jotai.atom((get) => { + this.endIconButtons = atom((get) => { const connStatus = get(this.connStatus); if (connStatus?.status != "connected") { return null; } - const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); + const mimeType = jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); const loadableSV = get(this.loadableSpecializedView); const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit"; if (mimeType == "directory") { @@ -335,18 +326,18 @@ export class PreviewModel implements ViewModel { } return null; }); - this.metaFilePath = jotai.atom((get) => { + this.metaFilePath = atom((get) => { const file = get(this.blockAtom)?.meta?.file; - if (util.isBlank(file)) { + if (isBlank(file)) { return "~"; } return file; }); - this.statFilePath = jotai.atom>(async (get) => { + this.statFilePath = atom>(async (get) => { const fileInfo = await get(this.statFile); return fileInfo?.path; }); - this.normFilePath = jotai.atom>(async (get) => { + this.normFilePath = atom>(async (get) => { const fileInfo = await get(this.statFile); if (fileInfo == null) { return null; @@ -357,10 +348,10 @@ export class PreviewModel implements ViewModel { return fileInfo.dir + "/" + fileInfo.name; }); this.loadableStatFilePath = loadable(this.statFilePath); - this.connection = jotai.atom((get) => { + this.connection = atom((get) => { return get(this.blockAtom)?.meta?.connection; }); - this.statFile = jotai.atom>(async (get) => { + this.statFile = atom>(async (get) => { const fileName = get(this.metaFilePath); if (fileName == null) { return null; @@ -369,15 +360,15 @@ export class PreviewModel implements ViewModel { const statFile = await services.FileService.StatFile(conn, fileName); return statFile; }); - this.fileMimeType = jotai.atom>(async (get) => { + this.fileMimeType = atom>(async (get) => { const fileInfo = await get(this.statFile); return fileInfo?.mimetype; }); this.fileMimeTypeLoadable = loadable(this.fileMimeType); - this.newFileContent = jotai.atom(null) as jotai.PrimitiveAtom; + this.newFileContent = atom(null) as PrimitiveAtom; this.goParentDirectory = this.goParentDirectory.bind(this); - const fullFileAtom = jotai.atom>(async (get) => { + const fullFileAtom = atom>(async (get) => { const fileName = get(this.metaFilePath); if (fileName == null) { return null; @@ -387,8 +378,8 @@ export class PreviewModel implements ViewModel { return file; }); - this.fileContentSaved = jotai.atom(null) as jotai.PrimitiveAtom; - const fileContentAtom = jotai.atom( + this.fileContentSaved = atom(null) as PrimitiveAtom; + const fileContentAtom = atom( async (get) => { const _ = get(this.metaFilePath); const newContent = get(this.newFileContent); @@ -400,7 +391,7 @@ export class PreviewModel implements ViewModel { return savedContent; } const fullFile = await get(fullFileAtom); - return util.base64ToString(fullFile?.data64); + return base64ToString(fullFile?.data64); }, (get, set, update: string) => { set(this.fileContentSaved, update); @@ -410,13 +401,13 @@ export class PreviewModel implements ViewModel { this.fullFile = fullFileAtom; this.fileContent = fileContentAtom; - this.specializedView = jotai.atom>(async (get) => { + this.specializedView = atom>(async (get) => { return this.getSpecializedView(get); }); this.loadableSpecializedView = loadable(this.specializedView); - this.canPreview = jotai.atom(false); + this.canPreview = atom(false); this.loadableFileInfo = loadable(this.statFile); - this.connStatus = jotai.atom((get) => { + this.connStatus = atom((get) => { const blockData = get(this.blockAtom); const connName = blockData?.meta?.connection; const connAtom = getConnStatusAtom(connName); @@ -428,7 +419,7 @@ export class PreviewModel implements ViewModel { globalStore.set(this.markdownShowToc, !globalStore.get(this.markdownShowToc)); } - async getSpecializedView(getFn: jotai.Getter): Promise<{ specializedView?: string; errorStr?: string }> { + async getSpecializedView(getFn: Getter): Promise<{ specializedView?: string; errorStr?: string }> { const mimeType = await getFn(this.fileMimeType); const fileInfo = await getFn(this.statFile); const fileName = await getFn(this.statFilePath); @@ -490,12 +481,16 @@ export class PreviewModel implements ViewModel { fileName = ""; } const blockMeta = globalStore.get(this.blockAtom)?.meta; - const updateMeta = historyutil.goHistory("file", fileName, newPath, blockMeta); + const updateMeta = goHistory("file", fileName, newPath, blockMeta); if (updateMeta == null) { return; } const blockOref = WOS.makeORef("block", this.blockId); services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + + // Clear the saved file buffers + globalStore.set(this.fileContentSaved, null); + globalStore.set(this.newFileContent, null); } async getParentInfo(fileInfo: FileInfo): Promise { @@ -543,7 +538,7 @@ export class PreviewModel implements ViewModel { goHistoryBack() { const blockMeta = globalStore.get(this.blockAtom)?.meta; const curPath = globalStore.get(this.metaFilePath); - const updateMeta = historyutil.goHistoryBack("file", curPath, blockMeta, true); + const updateMeta = goHistoryBack("file", curPath, blockMeta, true); if (updateMeta == null) { return; } @@ -555,7 +550,7 @@ export class PreviewModel implements ViewModel { goHistoryForward() { const blockMeta = globalStore.get(this.blockAtom)?.meta; const curPath = globalStore.get(this.metaFilePath); - const updateMeta = historyutil.goHistoryForward("file", curPath, blockMeta); + const updateMeta = goHistoryForward("file", curPath, blockMeta); if (updateMeta == null) { return; } @@ -582,7 +577,7 @@ export class PreviewModel implements ViewModel { } const conn = globalStore.get(this.connection) ?? ""; try { - services.FileService.SaveFile(conn, filePath, util.stringToBase64(newFileContent)); + services.FileService.SaveFile(conn, filePath, stringToBase64(newFileContent)); globalStore.set(this.fileContent, newFileContent); globalStore.set(this.newFileContent, null); console.log("saved file", filePath); @@ -644,7 +639,7 @@ export class PreviewModel implements ViewModel { navigator.clipboard.writeText(fileInfo.name); }, }); - const mimeType = util.jotaiLoadableValue(globalStore.get(this.fileMimeTypeLoadable), ""); + const mimeType = jotaiLoadableValue(globalStore.get(this.fileMimeTypeLoadable), ""); if (mimeType == "directory") { menuItems.push({ label: "Open Terminal in New Block", @@ -694,29 +689,29 @@ export class PreviewModel implements ViewModel { } keyDownHandler(e: WaveKeyboardEvent): boolean { - if (keyutil.checkKeyPressed(e, "Cmd:ArrowLeft")) { + if (checkKeyPressed(e, "Cmd:ArrowLeft")) { this.goHistoryBack(); return true; } - if (keyutil.checkKeyPressed(e, "Cmd:ArrowRight")) { + if (checkKeyPressed(e, "Cmd:ArrowRight")) { this.goHistoryForward(); return true; } - if (keyutil.checkKeyPressed(e, "Cmd:ArrowUp")) { + if (checkKeyPressed(e, "Cmd:ArrowUp")) { // handle up directory this.goParentDirectory({}); return true; } const openModalOpen = globalStore.get(this.openFileModal); if (!openModalOpen) { - if (keyutil.checkKeyPressed(e, "Cmd:o")) { + if (checkKeyPressed(e, "Cmd:o")) { this.updateOpenFileModalAndError(true); return true; } } const canPreview = globalStore.get(this.canPreview); if (canPreview) { - if (keyutil.checkKeyPressed(e, "Cmd:e")) { + if (checkKeyPressed(e, "Cmd:e")) { const editMode = globalStore.get(this.editMode); this.setEditMode(!editMode); return true; @@ -744,9 +739,9 @@ function makePreviewModel(blockId: string, nodeModel: NodeModel): PreviewModel { } function MarkdownPreview({ model }: SpecializedViewProps) { - const connName = jotai.useAtomValue(model.connection); - const fileInfo = jotai.useAtomValue(model.statFile); - const resolveOpts: MarkdownResolveOpts = React.useMemo(() => { + const connName = useAtomValue(model.connection); + const fileInfo = useAtomValue(model.statFile); + const resolveOpts: MarkdownResolveOpts = useMemo(() => { return { connName: connName, baseDir: fileInfo.dir, @@ -760,8 +755,8 @@ function MarkdownPreview({ model }: SpecializedViewProps) { } function StreamingPreview({ model }: SpecializedViewProps) { - const conn = jotai.useAtomValue(model.connection); - const fileInfo = jotai.useAtomValue(model.statFile); + const conn = useAtomValue(model.connection); + const fileInfo = useAtomValue(model.statFile); const filePath = fileInfo.path; const usp = new URLSearchParams(); usp.set("path", filePath); @@ -805,27 +800,27 @@ function StreamingPreview({ model }: SpecializedViewProps) { } function CodeEditPreview({ model }: SpecializedViewProps) { - const fileContent = jotai.useAtomValue(model.fileContent); - const setNewFileContent = jotai.useSetAtom(model.newFileContent); - const fileName = jotai.useAtomValue(model.statFilePath); + const fileContent = useAtomValue(model.fileContent); + const setNewFileContent = useSetAtom(model.newFileContent); + const fileName = useAtomValue(model.statFilePath); function codeEditKeyDownHandler(e: WaveKeyboardEvent): boolean { - if (keyutil.checkKeyPressed(e, "Cmd:e")) { + if (checkKeyPressed(e, "Cmd:e")) { model.setEditMode(false); return true; } - if (keyutil.checkKeyPressed(e, "Cmd:s")) { + if (checkKeyPressed(e, "Cmd:s")) { model.handleFileSave(); return true; } - if (keyutil.checkKeyPressed(e, "Cmd:r")) { + if (checkKeyPressed(e, "Cmd:r")) { model.handleFileRevert(); return true; } return false; } - React.useEffect(() => { + useEffect(() => { model.codeEditKeyDownHandler = codeEditKeyDownHandler; return () => { model.codeEditKeyDownHandler = null; @@ -837,7 +832,7 @@ function CodeEditPreview({ model }: SpecializedViewProps) { model.monacoRef.current = editor; editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => { - const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(e.browserEvent); + const waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent); const handled = tryReinjectKey(waveEvent); if (handled) { e.stopPropagation(); @@ -864,8 +859,8 @@ function CodeEditPreview({ model }: SpecializedViewProps) { } function CSVViewPreview({ model, parentRef }: SpecializedViewProps) { - const fileContent = jotai.useAtomValue(model.fileContent); - const fileName = jotai.useAtomValue(model.statFilePath); + const fileContent = useAtomValue(model.fileContent); + const fileName = useAtomValue(model.statFilePath); return ; } @@ -898,11 +893,11 @@ function iconForFile(mimeType: string): string { } function SpecializedView({ parentRef, model }: SpecializedViewProps) { - const specializedView = jotai.useAtomValue(model.specializedView); - const mimeType = jotai.useAtomValue(model.fileMimeType); - const setCanPreview = jotai.useSetAtom(model.canPreview); + const specializedView = useAtomValue(model.specializedView); + const mimeType = useAtomValue(model.fileMimeType); + const setCanPreview = useSetAtom(model.canPreview); - React.useEffect(() => { + useEffect(() => { setCanPreview(canPreview(mimeType)); }, [mimeType, setCanPreview]); @@ -927,7 +922,7 @@ function PreviewView({ contentRef: React.RefObject; model: PreviewModel; }) { - const connStatus = jotai.useAtomValue(model.connStatus); + const connStatus = useAtomValue(model.connStatus); if (connStatus?.status != "connected") { return null; } @@ -943,7 +938,7 @@ function PreviewView({ ); } -const OpenFileModal = React.memo( +const OpenFileModal = memo( ({ model, blockRef, @@ -953,19 +948,19 @@ const OpenFileModal = React.memo( blockRef: React.RefObject; blockId: string; }) => { - const openFileModal = jotai.useAtomValue(model.openFileModal); - const curFileName = jotai.useAtomValue(model.metaFilePath); + const openFileModal = useAtomValue(model.openFileModal); + const curFileName = useAtomValue(model.metaFilePath); const [filePath, setFilePath] = useState(""); - const isNodeFocused = jotai.useAtomValue(model.nodeModel.isFocused); + const isNodeFocused = useAtomValue(model.nodeModel.isFocused); const handleKeyDown = useCallback( - keyutil.keydownWrapper((waveEvent: WaveKeyboardEvent): boolean => { - if (keyutil.checkKeyPressed(waveEvent, "Escape")) { + keydownWrapper((waveEvent: WaveKeyboardEvent): boolean => { + if (checkKeyPressed(waveEvent, "Escape")) { model.updateOpenFileModalAndError(false); return true; } const handleCommandOperations = async () => { - if (keyutil.checkKeyPressed(waveEvent, "Enter")) { + if (checkKeyPressed(waveEvent, "Enter")) { model.handleOpenFile(filePath); return true; } diff --git a/package.json b/package.json index 97863d567..96d18a463 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "rehype-slug": "^6.0.0", "remark-flexible-toc": "^1.1.1", "remark-gfm": "^4.0.0", + "remark-github-blockquote-alert": "^1.2.1", "rxjs": "^7.8.1", "sharp": "^0.33.5", "shell-quote": "^1.8.1", diff --git a/yarn.lock b/yarn.lock index 482e56847..9e2fda072 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9632,6 +9632,15 @@ __metadata: languageName: node linkType: hard +"remark-github-blockquote-alert@npm:^1.2.1": + version: 1.2.1 + resolution: "remark-github-blockquote-alert@npm:1.2.1" + dependencies: + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/48f70a56347ba6d2649ec647f9b126fe2e22ee4efcbc4962e1645967ac0994859a930a1af3a8b75c2989d55ed5b7ce2df6fc86a4b0f2c0aa39d5ed2162c857ca + languageName: node + linkType: hard + "remark-parse@npm:^11.0.0": version: 11.0.0 resolution: "remark-parse@npm:11.0.0" @@ -11496,6 +11505,7 @@ __metadata: rehype-slug: "npm:^6.0.0" remark-flexible-toc: "npm:^1.1.1" remark-gfm: "npm:^4.0.0" + remark-github-blockquote-alert: "npm:^1.2.1" rollup-plugin-flow: "npm:^1.1.1" rxjs: "npm:^7.8.1" semver: "npm:^7.6.3"