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"