接入桌面壳文本文件导出能力
新增 HostBridge file.exportText 契约、文件名清洗和 H5 导出入口 Tauri 桌面壳通过受控 host_bridge_request 打开保存对话框并写入文本文件 Expo 移动壳对未接入的文件导出能力明确返回 unsupported 更新宿主壳方案、统一协议和项目共享决策记录
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { normalizeHostBridgeExternalUrl } from './hostBridge';
|
||||
import {
|
||||
normalizeHostBridgeExportFileName,
|
||||
normalizeHostBridgeExternalUrl,
|
||||
} from './hostBridge';
|
||||
|
||||
describe('HostBridge shared contract helpers', () => {
|
||||
test('只允许明确的外链协议交给宿主打开', () => {
|
||||
@@ -24,4 +27,19 @@ describe('HostBridge shared contract helpers', () => {
|
||||
).toBeNull();
|
||||
expect(normalizeHostBridgeExternalUrl('/relative/path')).toBeNull();
|
||||
});
|
||||
|
||||
test('归一化宿主导出文件名', () => {
|
||||
expect(normalizeHostBridgeExportFileName(' 作品:记录?.txt ')).toBe(
|
||||
'作品-记录-.txt',
|
||||
);
|
||||
expect(normalizeHostBridgeExportFileName('../secret.txt')).toBe(
|
||||
'secret.txt',
|
||||
);
|
||||
expect(normalizeHostBridgeExportFileName('')).toBe(
|
||||
'genarrative-export.txt',
|
||||
);
|
||||
expect(normalizeHostBridgeExportFileName('a'.repeat(140))).toHaveLength(
|
||||
120,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ export type HostBridgeMethod =
|
||||
| 'app.openExternalUrl'
|
||||
| 'app.setTitle'
|
||||
| 'clipboard.writeText'
|
||||
| 'file.exportText'
|
||||
| 'haptics.impact';
|
||||
|
||||
export type HostBridgeCapability =
|
||||
@@ -143,6 +144,18 @@ export type ClipboardWriteTextPayload = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type FileExportTextPayload = {
|
||||
fileName: string;
|
||||
content: string;
|
||||
mimeType?: string;
|
||||
};
|
||||
|
||||
export type FileExportTextResult = {
|
||||
action: 'saved';
|
||||
fileName: string;
|
||||
bytes: number;
|
||||
};
|
||||
|
||||
export type HapticsImpactPayload = {
|
||||
style?: 'light' | 'medium' | 'heavy';
|
||||
};
|
||||
@@ -156,3 +169,34 @@ export type ShareOpenPayload = {
|
||||
message?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const HOST_BRIDGE_FILE_NAME_FALLBACK = 'genarrative-export.txt';
|
||||
const HOST_BRIDGE_FILE_NAME_MAX_LENGTH = 120;
|
||||
|
||||
function isHostBridgeInvalidFileNameCharacter(value: string) {
|
||||
if (hasHostBridgeControlCharacter(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ['<', '>', ':', '"', '/', '\\', '|', '?', '*'].includes(value);
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeExportFileName(rawFileName: unknown) {
|
||||
if (typeof rawFileName !== 'string') {
|
||||
return HOST_BRIDGE_FILE_NAME_FALLBACK;
|
||||
}
|
||||
|
||||
const fileName = rawFileName
|
||||
.trim()
|
||||
.split('')
|
||||
.map((character) =>
|
||||
isHostBridgeInvalidFileNameCharacter(character) ? '-' : character,
|
||||
)
|
||||
.join('')
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/^[.\s-]+/, '')
|
||||
.slice(0, HOST_BRIDGE_FILE_NAME_MAX_LENGTH)
|
||||
.trim();
|
||||
|
||||
return fileName || HOST_BRIDGE_FILE_NAME_FALLBACK;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user