feat: import HTML wiki (#305)

* add import htmlWiki function

* Update useImportHtmlWiki.ts
This commit is contained in:
WhiteFall 2022-11-18 21:21:18 +08:00 committed by GitHub
parent 00ad8b88ae
commit ad7de01309
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 361 additions and 1 deletions

View file

@ -110,6 +110,9 @@
"CreateWiki": "创建WIKI",
"ImportWiki": "导入WIKI",
"CloneWiki": "导入线上Wiki",
"OpenLocalWikiFromHTML":"导入WIKI.HTML",
"LocalWikiHtml":"tiddlywiki.html文件路径",
"StoreWikiFolderLocation":"转换后的WIKI父文件夹路径",
"NotLoggedIn": "未登录",
"LogoutToGetStorageServiceToken": "登录在线存储服务以获取最新凭证",
"LogoutGithubAccount": "登出Github账号",

View file

@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { useTranslation } from 'react-i18next';
import Alert from '@material-ui/lab/Alert';
import { Typography, LinearProgress, Snackbar } from '@material-ui/core';
import type { IWikiWorkspaceFormProps } from './useForm';
import { useValidateHtmlWiki, useImportHtmlWiki } from './useImportHtmlWiki';
import { useWikiCreationProgress } from './useIndicator';
import { WikiLocation, CloseButton, ReportErrorFabButton } from './FormComponents';
export function ImportHtmlWikiDoneButton({
form,
isCreateMainWorkspace,
isCreateSyncedWorkspace,
errorInWhichComponentSetter,
}: IWikiWorkspaceFormProps & { isCreateMainWorkspace: boolean; isCreateSyncedWorkspace: boolean }): JSX.Element {
const { t } = useTranslation();
const [hasError, wikiCreationMessage, wikiCreationMessageSetter, hasErrorSetter] = useValidateHtmlWiki(
isCreateMainWorkspace,
isCreateSyncedWorkspace,
form,
errorInWhichComponentSetter,
);
const onSubmit = useImportHtmlWiki(
isCreateMainWorkspace,
isCreateSyncedWorkspace,
form,
wikiCreationMessageSetter,
hasErrorSetter,
errorInWhichComponentSetter,
);
const [logPanelOpened, logPanelSetter, inProgressOrError] = useWikiCreationProgress(wikiCreationMessageSetter, wikiCreationMessage, hasError);
if (hasError) {
return (
<>
<CloseButton variant="contained" disabled>
{wikiCreationMessage}
</CloseButton>
{wikiCreationMessage !== undefined && <ReportErrorFabButton message={wikiCreationMessage} />}
</>
);
}
return (
<>
{inProgressOrError && <LinearProgress color="secondary" />}
{/* 这个好像是log面板 */}
<Snackbar open={logPanelOpened} autoHideDuration={5000} onClose={() => logPanelSetter(false)}>
<Alert severity="info">{wikiCreationMessage}</Alert>
</Snackbar>
<CloseButton variant="contained" color="secondary" disabled={inProgressOrError} onClick={onSubmit}>
<Typography variant="body1" display="inline">
{t('AddWorkspace.ImportWiki')}
</Typography>
<WikiLocation>{form.wikiHtmlPath}</WikiLocation>
</CloseButton>
</>
);
}

View file

@ -0,0 +1,96 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Typography } from '@material-ui/core';
import { Folder as FolderIcon } from '@material-ui/icons';
import { useValidateHtmlWiki } from './useImportHtmlWiki';
import { CreateContainer, LocationPickerContainer, LocationPickerInput, LocationPickerButton } from './FormComponents';
import type { IWikiWorkspaceFormProps } from './useForm';
export function ImportHtmlWikiForm({
form,
isCreateMainWorkspace,
isCreateSyncedWorkspace,
errorInWhichComponent,
errorInWhichComponentSetter,
}: IWikiWorkspaceFormProps & { isCreateSyncedWorkspace: boolean }): JSX.Element {
const { t } = useTranslation();
const { wikiHtmlPathSetter, extractWikiHtmlParentFolderSetter } = form;
useValidateHtmlWiki(isCreateMainWorkspace, isCreateSyncedWorkspace, form, errorInWhichComponentSetter);
const onWikiLocationChange = useCallback(
async (newLocation: string) => {
if (newLocation !== undefined) {
wikiHtmlPathSetter(newLocation);
}
},
[wikiHtmlPathSetter],
);
const onSaveLocationChange = useCallback(
async (newLocation: string) => {
if (newLocation !== undefined) {
extractWikiHtmlParentFolderSetter(newLocation);
}
},
[extractWikiHtmlParentFolderSetter],
);
return (
<CreateContainer elevation={2} square>
<LocationPickerContainer>
<LocationPickerInput
error={errorInWhichComponent.extractWikiHtmlParentFolder}
onChange={(event) => {
// https://zh-hans.reactjs.org/docs/events.html#clipboard-events
onWikiLocationChange(event.target.value);
}}
label={t('AddWorkspace.LocalWikiHtml')}
value={form.wikiHtmlPath}
/>
<LocationPickerButton
// 第一个输入框的选择文件夹按钮。
onClick={async () => {
// first clear the text, so button will refresh
wikiHtmlPathSetter('');
const filePaths = await window.service.native.pickFile([{ name: 'html文件', extensions: ['html', 'htm'] }]);
if (filePaths?.length > 0) {
wikiHtmlPathSetter(filePaths[0]);
}
}}
endIcon={<FolderIcon />}>
<Typography variant="button" display="inline">
{t('AddWorkspace.Choose')}
</Typography>
</LocationPickerButton>
</LocationPickerContainer>
<LocationPickerContainer>
<LocationPickerInput
error={errorInWhichComponent.extractWikiHtmlParentFolder}
onChange={(event) => {
onSaveLocationChange(event.target.value);
}}
label={t('AddWorkspace.StoreWikiFolderLocation')}
value={form.extractWikiHtmlParentFolder}
/>
<LocationPickerButton
// 第二个输入框的选择文件夹按钮。
onClick={async () => {
// first clear the text, so button will refresh
extractWikiHtmlParentFolderSetter('');
const filePaths = await window.service.native.pickDirectory(form.wikiFolderLocation);
if (filePaths?.length > 0) {
extractWikiHtmlParentFolderSetter(filePaths[0]);
}
}}
endIcon={<FolderIcon />}>
<Typography variant="button" display="inline">
{t('AddWorkspace.Choose')}
</Typography>
</LocationPickerButton>
</LocationPickerContainer>
</CreateContainer>
);
}

View file

@ -23,11 +23,14 @@ import { TokenForm } from '@/components/TokenForm';
import { GitRepoUrlForm } from './GitRepoUrlForm';
import { LocationPickerContainer, LocationPickerInput } from './FormComponents';
import { usePromiseValue } from '@/helpers/useServiceValue';
import { ImportHtmlWikiForm } from './ImportHtmlWikiForm';
import { ImportHtmlWikiDoneButton } from './ImportHtmlWikiDoneButton';
enum CreateWorkspaceTabs {
CloneOnlineWiki = 'CloneOnlineWiki',
CreateNewWiki = 'CreateNewWiki',
OpenLocalWiki = 'OpenLocalWiki',
OpenLocalWikiFromHtml = 'OpenLocalWikiFromHtml',
}
export const Paper = styled(PaperRaw)`
@ -122,6 +125,7 @@ export function AddWorkspace(): JSX.Element {
<Tab label={t('AddWorkspace.CreateNewWiki')} value={CreateWorkspaceTabs.CreateNewWiki} />
<Tab label={t(`AddWorkspace.CloneOnlineWiki`)} value={CreateWorkspaceTabs.CloneOnlineWiki} />
<Tab label={t('AddWorkspace.OpenLocalWiki')} value={CreateWorkspaceTabs.OpenLocalWiki} />
<Tab label={t('AddWorkspace.OpenLocalWikiFromHTML')} value={CreateWorkspaceTabs.OpenLocalWikiFromHtml} />
</TabList>
</Paper>
</AppBar>
@ -172,6 +176,12 @@ export function AddWorkspace(): JSX.Element {
<ExistedWikiDoneButton {...formProps} isCreateSyncedWorkspace={isCreateSyncedWorkspace} />
</Container>
</TabPanel>
<TabPanel value={CreateWorkspaceTabs.OpenLocalWikiFromHtml}>
<Container>
<ImportHtmlWikiForm {...formProps} isCreateSyncedWorkspace={isCreateSyncedWorkspace} />
<ImportHtmlWikiDoneButton {...formProps} isCreateSyncedWorkspace={isCreateSyncedWorkspace} />
</Container>
</TabPanel>
</TabContext>
);
}

View file

@ -106,6 +106,23 @@ export function useWikiWorkspaceForm(options?: { fromExisted: boolean }) {
})();
}, [gitRepoUrlSetter, wikiFolderLocation, options?.fromExisted]);
/*
* wikiHTML,使wiki解压父文件夹路径,wiki文件夹保存位置.
* wikiHtmlPathwikiHtmlPathSetterextractWikiHtmlParentFolderextractWikiHtmlParentFolderSetter,
*/
const [wikiHtmlPath, wikiHtmlPathSetter] = useState<string>('');
useEffect(() => {
void (async function getDefaultWikiHtmlPathEffect() {})();
}, []);
const [extractWikiHtmlParentFolder, extractWikiHtmlParentFolderSetter] = useState<string>('');
useEffect(() => {
void (async function getDefaultExtractWikiHtmlFolderPathEffect() {
const desktopPathAsDefaultExtractWikiHtmlParentFolderPath = await window.service.context.get('DEFAULT_WIKI_FOLDER');
extractWikiHtmlParentFolderSetter(desktopPathAsDefaultExtractWikiHtmlParentFolderPath);
})();
}, []);
return {
storageProvider,
storageProviderSetter,
@ -128,6 +145,10 @@ export function useWikiWorkspaceForm(options?: { fromExisted: boolean }) {
workspaceList,
mainWorkspaceList,
mainWikiToLinkIndex,
wikiHtmlPath,
wikiHtmlPathSetter,
extractWikiHtmlParentFolder,
extractWikiHtmlParentFolderSetter,
};
}

View file

@ -0,0 +1,104 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IErrorInWhichComponent, IWikiWorkspaceForm } from './useForm';
import { updateErrorInWhichComponentSetterByErrorMessage } from './useIndicator';
export function useValidateHtmlWiki(
isCreateMainWorkspace: boolean,
isCreateSyncedWorkspace: boolean,
form: IWikiWorkspaceForm,
errorInWhichComponentSetter: (errors: IErrorInWhichComponent) => void,
): [boolean, string | undefined, (m: string) => void, (m: boolean) => void] {
const { t } = useTranslation();
const [wikiCreationMessage, wikiCreationMessageSetter] = useState<string | undefined>();
const [hasError, hasErrorSetter] = useState<boolean>(false);
useEffect(() => {
if (!form.parentFolderLocation) {
wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}${t('AddWorkspace.WorkspaceFolder')}`);
errorInWhichComponentSetter({ parentFolderLocation: true });
hasErrorSetter(true);
} else if (!form.wikiHtmlPath) {
// 判断wikiHtmlPath是否存在
wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}${t('AddWorkspace.LocalWikiHtml')}`);
errorInWhichComponentSetter({ wikiHtmlPath: true });
hasErrorSetter(true);
} else if (!form.wikiFolderName) {
wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}${t('AddWorkspace.WorkspaceFolderNameToCreate')}`);
errorInWhichComponentSetter({ wikiFolderName: true });
hasErrorSetter(true);
} else if (isCreateSyncedWorkspace && !form.gitRepoUrl) {
wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}${t('AddWorkspace.GitRepoUrl')}`);
errorInWhichComponentSetter({ gitRepoUrl: true });
hasErrorSetter(true);
} else if (!isCreateMainWorkspace && !form.mainWikiToLink?.wikiFolderLocation) {
wikiCreationMessageSetter(`${t('AddWorkspace.NotFilled')}${t('AddWorkspace.MainWorkspace')}`);
errorInWhichComponentSetter({ mainWikiToLink: true });
hasErrorSetter(true);
} else if (isCreateSyncedWorkspace && (form.gitUserInfo === undefined || !(form.gitUserInfo.accessToken?.length > 0))) {
wikiCreationMessageSetter(t('AddWorkspace.NotLoggedIn'));
errorInWhichComponentSetter({ gitUserInfo: true });
hasErrorSetter(true);
} else {
wikiCreationMessageSetter('');
errorInWhichComponentSetter({});
hasErrorSetter(false);
}
}, [
t,
isCreateMainWorkspace,
isCreateSyncedWorkspace,
form.parentFolderLocation,
form.wikiFolderName,
form.gitRepoUrl,
form.gitUserInfo,
form.mainWikiToLink?.wikiFolderLocation,
form.tagName,
// 监听wikiHtmlPath如果不存在就在提示用户。
form.wikiHtmlPath,
errorInWhichComponentSetter,
]);
return [hasError, wikiCreationMessage, wikiCreationMessageSetter, hasErrorSetter];
}
export function useImportHtmlWiki(
isCreateMainWorkspace: boolean,
isCreateSyncedWorkspace: boolean,
form: IWikiWorkspaceForm,
wikiCreationMessageSetter: (m: string) => void,
hasErrorSetter: (m: boolean) => void,
errorInWhichComponentSetter: (errors: IErrorInWhichComponent) => void,
): () => Promise<void> {
const { t } = useTranslation();
const onSubmit = useCallback(async () => {
wikiCreationMessageSetter(t('AddWorkspace.Processing'));
hasErrorSetter(false);
try {
// 如果是HTML文件即使转换错误删掉在执行一次也不会出错。
// 我希望判断用户输入的是否是HTML文件如果不是就不予执行。然后在判断如果失败了就删除这个数据并且提示错误信息。如果输入的是html类型的文件是不会出错的即使是非wiki类型的文件。如果输出的目录非空那么会导致异常闪退。
// 我希望可以创建一个与HTML文件名一样的文件夹这样的话就可以在父文件夹中解压出一个HTML文件名一样的文件夹而不只是wiki数据。
// 我希望信息可以显示在log面板。当出现解压错误后就提示用户。
const extractState = await window.service.wiki.extractWikiHTML(form.wikiHtmlPath, form.extractWikiHtmlParentFolder);
// 待解决: var extractState用于接收执行是否解压是否成功。但是接收不到window.service.wiki.extractWikiHTML函数的返回值并且在该函数上下打log都未显示。
// 我希望在解压成功后设置好工作区的信息执行打开解压后的wiki文件夹的操作。
} catch (error) {
wikiCreationMessageSetter((error as Error).message);
updateErrorInWhichComponentSetterByErrorMessage(t, (error as Error).message, errorInWhichComponentSetter);
hasErrorSetter(true);
}
}, [
wikiCreationMessageSetter,
t,
hasErrorSetter,
form,
isCreateMainWorkspace,
isCreateSyncedWorkspace,
form.wikiHtmlPath,
form.extractWikiHtmlParentFolder,
errorInWhichComponentSetter,
]);
return onSubmit;
}

View file

@ -168,6 +168,22 @@ export class Wiki implements IWikiService {
});
}
public async extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath: string): Promise<boolean> {
// hope saveWikiFolderPath = ParentFolderPath + wikifolderPath
// await fs.remove(saveWikiFolderPath); removes the folder function that failed to convert.
// We want the folder where the WIKI is saved to be empty, and we want the input htmlWiki to be an HTML file even if it is a non-wikiHTML file. Otherwise the program will exit abnormally.
// this.wikiWorkers[saveWikiFolderPath] = worker;
// Then, you can use this.getWorker (saveWikiFolderPath) method to call this wikiWorker that belongs to the HTMLWIKI after decompression
const worker = await spawn<WikiWorker>(new Worker(workerURL as string), { timeout: 1000 * 60 });
const result = await worker.ExtractWikiHTMLAndGetExtractState(htmlWikiPath, saveWikiFolderPath);
return result;
}
public async packetHTMLFromWikiFolder(folderWikiPath: string, saveWikiHtmlFolder: string): Promise<void> {
const worker = await spawn<WikiWorker>(new Worker(workerURL as string), { timeout: 1000 * 60 });
await worker.packetHTMLFromWikiFolder(folderWikiPath, saveWikiHtmlFolder);
}
public async stopWiki(wikiFolderLocation: string): Promise<void> {
const worker = this.getWorker(wikiFolderLocation);
if (worker === undefined) {

View file

@ -35,6 +35,7 @@ export interface IWikiService {
*/
createSubWiki(parentFolderLocation: string, folderName: string, mainWikiPath: string, tagName?: string, onlyLink?: boolean): Promise<void>;
ensureWikiExist(wikiPath: string, shouldBeMainWiki: boolean): Promise<void>;
extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath: string): Promise<boolean>;
getSubWikiPluginContent(mainWikiPath: string): Promise<ISubWikiPluginContent[]>;
getTiddlerText(workspace: IWorkspace, title: string): Promise<string | undefined>;
getWikiLogs(homePath: string): Promise<{ content: string; filePath: string }>;
@ -50,6 +51,7 @@ export interface IWikiService {
* @param title tiddler title to open
*/
openTiddlerInExternal(homePath: string, title: string): Promise<void>;
packetHTMLFromWikiFolder(folderWikiPath: string, saveWikiHtmlfolder: string): Promise<void>;
removeWiki(wikiPath: string, mainWikiToUnLink?: string, onlyRemoveLink?: boolean): Promise<void>;
requestOpenTiddlerInWiki(tiddlerName: string): Promise<void>;
/** send tiddlywiki action message to current active wiki */
@ -98,6 +100,9 @@ export const WikiServiceIPCDescriptor = {
updateSubWikiPluginContent: ProxyPropertyType.Function,
wikiOperation: ProxyPropertyType.Function,
wikiStartup: ProxyPropertyType.Function,
// Register here to unpack and package wikiHTML functions
extractWikiHTML: ProxyPropertyType.Function,
packetHTMLFromWikiFolder: ProxyPropertyType.Function,
},
};

View file

@ -131,6 +131,52 @@ function executeZxScript(file: IZxFileInput, zxPath: string): Observable<IZxWork
});
}
const wikiWorker = { startNodeJSWiki, getTiddlerFileMetadata: (tiddlerTitle: string) => wikiInstance?.boot?.files?.[tiddlerTitle], executeZxScript };
function extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath: string): boolean {
// tiddlywiki --load ./mywiki.html --savewikifolder ./mywikifolder
// --savewikifolder <wikifolderpath> [<filter>]
// . /mywikifolder is the path where the tiddlder and plugins folders are stored
let extractState = false;
// eslint-disable-next-line prefer-regex-literals
const reg = new RegExp(/(?:html|htm|Html|HTML|HTM)$/);
const isHtmlWiki = reg.test(htmlWikiPath);
if (!isHtmlWiki) {
console.error('Please enter the path to the tiddlywiki.html file. But the current path is: ' + htmlWikiPath);
return extractState;
} else {
try {
const wikiInstance = TiddlyWiki();
wikiInstance.boot.argv = ['--load', htmlWikiPath, '--savewikifolder', saveWikiFolderPath];
wikiInstance.boot.startup({});
// eslint-disable-next-line security-node/detect-crlf
console.log('Extract Wiki Html Successful: ' + saveWikiFolderPath);
extractState = true;
} catch (error) {
const message = `Tiddlywiki extractWikiHTML with error ${(error as Error).message} ${(error as Error).stack ?? ''}`;
console.error(message);
}
}
return extractState;
}
function packetHTMLFromWikiFolder(folderWikiPath: string, saveWikiHtmlFolder: string): void {
// tiddlywiki ./mywikifolder --rendertiddler '$:/core/save/all' mywiki.html text/plain
// . /mywikifolder is the path to the wiki folder, which generally contains the tiddlder and plugins directories
try {
const wikiInstance = TiddlyWiki();
wikiInstance.boot.argv = [folderWikiPath, '--rendertiddler', '$:/core/save/all', saveWikiHtmlFolder, 'text/plain'];
wikiInstance.boot.startup({});
} catch (error) {
const message = `Tiddlywiki packetHTMLFromWikiFolder with error ${(error as Error).message} ${(error as Error).stack ?? ''}`;
console.error(message);
}
}
const wikiWorker = {
startNodeJSWiki,
getTiddlerFileMetadata: (tiddlerTitle: string) => wikiInstance?.boot?.files?.[tiddlerTitle],
executeZxScript,
ExtractWikiHTMLAndGetExtractState: (htmlWikiPath: string, saveWikiFolderPath: string) => extractWikiHTML(htmlWikiPath, saveWikiFolderPath),
packetHTMLFromWikiFolder,
};
export type WikiWorker = typeof wikiWorker;
expose(wikiWorker);