chore: standardize project comments in English
This commit is contained in:
1 parent
a874a39ea5
commit
ccb16acbea
167 files changed
+820
-820
No files matched your search
@@ -2,7 +2,7 @@ import type * as Assets from './type'
|
||||
import { encodePath } from '@@/utils/paths'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 上传笔记图片。 */
|
||||
/** Upload an image for a note. */
|
||||
export function uploadImageApi(data: Assets.UploadImageRequestData) {
|
||||
const body = new FormData()
|
||||
body.append('file', data.file, data.file.name || 'image')
|
||||
@@ -15,7 +15,7 @@ export function uploadImageApi(data: Assets.UploadImageRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 上传目录图片。 */
|
||||
/** Upload an image for a folder. */
|
||||
export function uploadFolderImageApi(data: Assets.FolderImageRequestData) {
|
||||
const body = new FormData()
|
||||
body.append('file', data.file)
|
||||
@@ -28,7 +28,7 @@ export function uploadFolderImageApi(data: Assets.FolderImageRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 替换原路径图片。 */
|
||||
/** Replace an image at its existing path. */
|
||||
export function replaceImageApi(data: Assets.ReplaceImageRequestData) {
|
||||
const body = new FormData()
|
||||
body.append('file', data.file)
|
||||
@@ -40,7 +40,7 @@ export function replaceImageApi(data: Assets.ReplaceImageRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 下载并导入远程图片。 */
|
||||
/** Download and import a remote image. */
|
||||
export function importImageApi(data: Assets.ImportImageRequestData) {
|
||||
return request<Assets.ImageResponseData>({
|
||||
url: 'assets/import',
|
||||
@@ -51,7 +51,7 @@ export function importImageApi(data: Assets.ImportImageRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 核对引用后清理图片。 */
|
||||
/** Clean up images after checking references. */
|
||||
export function cleanupImagesApi(data: Assets.CleanupImagesRequestData) {
|
||||
return request<Assets.CleanupImagesResponseData>({
|
||||
url: 'assets/cleanup',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Auth from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 查询初始化状态。 */
|
||||
/** Query initialization status. */
|
||||
export function getAuthStateApi() {
|
||||
return request<Auth.StateResponseData>({
|
||||
url: 'auth/state',
|
||||
@@ -11,7 +11,7 @@ export function getAuthStateApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 签发或刷新登录验证码。 */
|
||||
/** Issue or refresh a sign-in captcha. */
|
||||
export function getCaptchaApi(data: Auth.CaptchaRequestData) {
|
||||
return request<Auth.CaptchaResponseData>({
|
||||
url: 'auth/captcha',
|
||||
@@ -22,7 +22,7 @@ export function getCaptchaApi(data: Auth.CaptchaRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化登录密码。 */
|
||||
/** Initialize the login password. */
|
||||
export function setupApi(data: Auth.SetupRequestData) {
|
||||
return request<Auth.LoginResponseData>({
|
||||
url: 'auth/setup',
|
||||
@@ -33,7 +33,7 @@ export function setupApi(data: Auth.SetupRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 使用密码和验证码登录。 */
|
||||
/** Sign in with a password and captcha. */
|
||||
export function loginApi(data: Auth.LoginRequestData) {
|
||||
return request<Auth.LoginResponseData>({
|
||||
url: 'auth/login',
|
||||
@@ -44,7 +44,7 @@ export function loginApi(data: Auth.LoginRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复当前身份与权限。 */
|
||||
/** Restore the current identity and permissions. */
|
||||
export function getCurrentUserApi() {
|
||||
return request<Auth.IdentityResponseData>({
|
||||
url: 'auth/me',
|
||||
@@ -53,7 +53,7 @@ export function getCurrentUserApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 撤销当前服务端会话。 */
|
||||
/** Revoke the current server session. */
|
||||
export function logoutApi() {
|
||||
return request<void>({
|
||||
url: 'auth/logout',
|
||||
@@ -61,7 +61,7 @@ export function logoutApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 签发受限资源票据。 */
|
||||
/** Issue restricted resource tickets. */
|
||||
export function createResourceTicketApi(data: Auth.TicketRequestData) {
|
||||
return request<Auth.TicketResponseData>({
|
||||
url: 'auth/tickets',
|
||||
@@ -71,7 +71,7 @@ export function createResourceTicketApi(data: Auth.TicketRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改登录密码。 */
|
||||
/** Change the login password. */
|
||||
export function changePasswordApi(data: Auth.PasswordRequestData) {
|
||||
return request<Auth.PasswordResponseData>({
|
||||
url: 'auth/password',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { EventHandlers } from './type'
|
||||
|
||||
import { createResourceTicketApi } from '@@/apis/auth'
|
||||
|
||||
// 订阅仓库事件;每次连接领取一次性票据,重连后补全量同步。
|
||||
// Subscribe to vault events; obtain a one-use ticket for each connection and fully sync after reconnecting.
|
||||
import { isCancel } from 'axios'
|
||||
import { pinia } from '@/pinia'
|
||||
import { useUserStore } from '@/pinia/stores/user'
|
||||
|
||||
@@ -2,7 +2,7 @@ import type * as Files from './type'
|
||||
import { encodePath } from '@@/utils/paths'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取 Markdown 原文与版本。 */
|
||||
/** Read original Markdown and its revision. */
|
||||
export function getFileApi(path: string) {
|
||||
return request<string>({
|
||||
url: `files/${encodePath(path)}`,
|
||||
@@ -13,7 +13,7 @@ export function getFileApi(path: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 按版本写入 Markdown。 */
|
||||
/** Write Markdown with a revision precondition. */
|
||||
export function writeFileApi(data: Files.WriteFileRequestData) {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'text/markdown; charset=utf-8' }
|
||||
if (data.ifMatch === '') headers['If-None-Match'] = '*'
|
||||
@@ -28,7 +28,7 @@ export function writeFileApi(data: Files.WriteFileRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 移入回收站。 */
|
||||
/** Move an item to the recycle bin. */
|
||||
export function deleteFileApi(path: string) {
|
||||
return request<Files.DeleteFileResponseData>({
|
||||
url: `files/${encodePath(path)}`,
|
||||
@@ -37,7 +37,7 @@ export function deleteFileApi(path: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建文件夹。 */
|
||||
/** Create a folder. */
|
||||
export function createFolderApi(data: Files.FileRequestData) {
|
||||
return request<Files.FileOperationResponseData>({
|
||||
url: 'folders',
|
||||
@@ -47,7 +47,7 @@ export function createFolderApi(data: Files.FileRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 移动文件或目录。 */
|
||||
/** Move a file or folder. */
|
||||
export function moveFileApi(data: Files.MoveRequestData) {
|
||||
return request<Files.FileOperationResponseData>({
|
||||
url: 'move',
|
||||
@@ -57,7 +57,7 @@ export function moveFileApi(data: Files.MoveRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 导入 Markdown 或 ZIP。 */
|
||||
/** Import Markdown or a ZIP archive. */
|
||||
export function importNotesApi(data: Files.ImportRequestData) {
|
||||
const body = new FormData()
|
||||
body.append('dir', data.dir)
|
||||
@@ -71,7 +71,7 @@ export function importNotesApi(data: Files.ImportRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 导出选中内容为 Markdown ZIP。 */
|
||||
/** Export selected contents as a Markdown ZIP. */
|
||||
export function exportFilesApi(data: Files.ExportRequestData) {
|
||||
return request<Blob>({
|
||||
url: 'export',
|
||||
@@ -83,7 +83,7 @@ export function exportFilesApi(data: Files.ExportRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 下载原始文件。 */
|
||||
/** Download a raw file. */
|
||||
export function getRawFileApi(path: string) {
|
||||
return request<Blob>({
|
||||
url: `raw/${encodePath(path)}`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Maintenance from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取维护状态。 */
|
||||
/** Read maintenance status. */
|
||||
export function getMaintenanceStatusApi() {
|
||||
return request<Maintenance.MaintenanceResponseData>({
|
||||
url: 'maintenance/status',
|
||||
@@ -10,7 +10,7 @@ export function getMaintenanceStatusApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 导出实例或设置备份。 */
|
||||
/** Export an instance or settings backup. */
|
||||
export function exportBackupApi(kind: Maintenance.BackupKind) {
|
||||
return request<Blob>({
|
||||
url: `maintenance/export/${kind}`,
|
||||
@@ -21,7 +21,7 @@ export function exportBackupApi(kind: Maintenance.BackupKind) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 导出全部 Markdown 笔记。 */
|
||||
/** Export all Markdown notes. */
|
||||
export function exportNotesApi() {
|
||||
return request<Blob>({
|
||||
url: 'maintenance/notes/export',
|
||||
@@ -32,7 +32,7 @@ export function exportNotesApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 导出系统日志。 */
|
||||
/** Export system logs. */
|
||||
export function exportLogsApi() {
|
||||
return request<Blob>({
|
||||
url: 'maintenance/logs/export',
|
||||
@@ -43,7 +43,7 @@ export function exportLogsApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建备份上传。 */
|
||||
/** Create a backup upload. */
|
||||
export function startBackupUploadApi(data: Maintenance.BackupUploadRequestData) {
|
||||
return request<Maintenance.BackupUploadResponseData>({
|
||||
url: 'maintenance/uploads',
|
||||
@@ -53,7 +53,7 @@ export function startBackupUploadApi(data: Maintenance.BackupUploadRequestData)
|
||||
})
|
||||
}
|
||||
|
||||
/** 顺序上传备份分块。 */
|
||||
/** Upload backup chunks sequentially. */
|
||||
export function uploadBackupChunkApi(data: Maintenance.BackupChunkRequestData) {
|
||||
return request<Maintenance.BackupChunkResponseData>({
|
||||
url: `maintenance/uploads/${encodeURIComponent(data.id)}`,
|
||||
@@ -64,7 +64,7 @@ export function uploadBackupChunkApi(data: Maintenance.BackupChunkRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 校验完整备份。 */
|
||||
/** Validate the complete backup. */
|
||||
export function validateBackupApi(id: string) {
|
||||
return request<Maintenance.BackupSummaryResponseData>({
|
||||
url: `maintenance/uploads/${encodeURIComponent(id)}/validate`,
|
||||
@@ -73,7 +73,7 @@ export function validateBackupApi(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 取消备份上传并清理暂存。 */
|
||||
/** Cancel a backup upload and remove staging files. */
|
||||
export function cancelBackupUploadApi(id: string) {
|
||||
return request<Maintenance.MaintenanceOperationResponseData>({
|
||||
url: `maintenance/uploads/${encodeURIComponent(id)}`,
|
||||
@@ -82,7 +82,7 @@ export function cancelBackupUploadApi(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 导入已确认的设置备份。 */
|
||||
/** Import a confirmed settings backup. */
|
||||
export function importSettingsApi(id: string) {
|
||||
return request<Maintenance.MaintenanceOperationResponseData>({
|
||||
url: 'maintenance/settings/import',
|
||||
@@ -92,7 +92,7 @@ export function importSettingsApi(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复已确认的实例备份。 */
|
||||
/** Restore a confirmed instance backup. */
|
||||
export function restoreBackupApi(id: string) {
|
||||
return request<Maintenance.RestoreBackupResponseData>({
|
||||
url: 'maintenance/restore',
|
||||
@@ -102,7 +102,7 @@ export function restoreBackupApi(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 备份后重置设置。 */
|
||||
/** Back up and reset settings. */
|
||||
export function resetSettingsApi() {
|
||||
return request<Maintenance.MaintenanceOperationResponseData>({
|
||||
url: 'maintenance/settings/reset',
|
||||
@@ -112,7 +112,7 @@ export function resetSettingsApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 优化或重建派生索引。 */
|
||||
/** Optimize or rebuild the derived index. */
|
||||
export function maintainIndexApi(operation: 'optimize' | 'rebuild') {
|
||||
return request<Maintenance.MaintenanceOperationResponseData>({
|
||||
url: `maintenance/index/${operation}`,
|
||||
@@ -121,7 +121,7 @@ export function maintainIndexApi(operation: 'optimize' | 'rebuild') {
|
||||
})
|
||||
}
|
||||
|
||||
/** 清理非活动临时文件。 */
|
||||
/** Clean inactive temporary files. */
|
||||
export function clearTempApi() {
|
||||
return request<Maintenance.ClearTempResponseData>({
|
||||
url: 'maintenance/temp/clear',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Metadata from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取笔记元数据索引。 */
|
||||
/** Read indexed note metadata. */
|
||||
export function getMetadataApi() {
|
||||
return request<Metadata.MetadataResponseData>({
|
||||
url: 'metadata',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Plugins from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取已安装插件。 */
|
||||
/** List installed plugins. */
|
||||
export function getPluginsApi() {
|
||||
return request<Plugins.PluginsResponseData>({
|
||||
url: 'plugins',
|
||||
@@ -10,7 +10,7 @@ export function getPluginsApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 安装插件 ZIP。 */
|
||||
/** Install a plugin ZIP. */
|
||||
export function installPluginApi(file: File) {
|
||||
const data = new FormData()
|
||||
data.append('file', file)
|
||||
@@ -22,7 +22,7 @@ export function installPluginApi(file: File) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 启用或停用插件。 */
|
||||
/** Enable or disable a plugin. */
|
||||
export function togglePluginApi(data: Plugins.TogglePluginRequestData) {
|
||||
return request<Plugins.PluginResponseData>({
|
||||
url: `plugins/${encodeURIComponent(data.id)}`,
|
||||
@@ -32,7 +32,7 @@ export function togglePluginApi(data: Plugins.TogglePluginRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 卸载插件并保留插件数据。 */
|
||||
/** Uninstall a plugin while retaining its data. */
|
||||
export function uninstallPluginApi(id: string) {
|
||||
return request<Plugins.PluginOperationResponseData>({
|
||||
url: `plugins/${encodeURIComponent(id)}`,
|
||||
@@ -41,7 +41,7 @@ export function uninstallPluginApi(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 读取插件存储。 */
|
||||
/** Read plugin storage. */
|
||||
export function getPluginDataApi(id: string) {
|
||||
return request<Plugins.PluginDataResponseData>({
|
||||
url: `plugins/${encodeURIComponent(id)}/data`,
|
||||
@@ -50,7 +50,7 @@ export function getPluginDataApi(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 保存插件存储。 */
|
||||
/** Save plugin storage. */
|
||||
export function savePluginDataApi(data: Plugins.PluginDataRequestData) {
|
||||
return request<Plugins.PluginDataResponseData>({
|
||||
url: `plugins/${encodeURIComponent(data.id)}/data`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Search from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 搜索笔记。 */
|
||||
/** Search notes. */
|
||||
export function searchApi(params: Search.SearchRequestData) {
|
||||
return request<Search.SearchResponseData>({
|
||||
url: 'search',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface SearchRequestData { q: string }
|
||||
export interface SearchResult {
|
||||
path: string
|
||||
/** 第一处匹配行,从1开始;只有路径匹配时为0。 */
|
||||
/** First matching line, starting at 1; 0 when only the path matches. */
|
||||
line: number
|
||||
snippet: string
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Settings from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取设置。 */
|
||||
/** Read settings. */
|
||||
export function getSettingsApi() {
|
||||
return request<Settings.SettingsResponseData>({
|
||||
url: 'settings',
|
||||
@@ -10,7 +10,7 @@ export function getSettingsApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新设置。 */
|
||||
/** Update settings. */
|
||||
export function patchSettingsApi(data: Settings.SettingsData) {
|
||||
return request<Settings.SettingsResponseData>({
|
||||
url: 'settings',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/** 保留插件写入的未知设置,应用端按已知字段校验。 */
|
||||
/** Preserve unknown plugin settings; the application validates known fields. */
|
||||
export type SettingsData = Record<string, unknown>
|
||||
export type SettingsResponseData = ApiResponseData<SettingsData>
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Storage from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 统计实际存储占用。 */
|
||||
/** Measure actual storage usage. */
|
||||
export function getStorageApi() {
|
||||
return request<Storage.StorageResponseData>({
|
||||
url: 'storage',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Themes from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取可选主题。 */
|
||||
/** List available themes. */
|
||||
export function getThemesApi() {
|
||||
return request<Themes.ThemesResponseData>({
|
||||
url: 'themes',
|
||||
@@ -10,7 +10,7 @@ export function getThemesApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 读取主题与样式。 */
|
||||
/** Read a theme and its styles. */
|
||||
export function getThemeApi(id: string) {
|
||||
return request<Themes.ThemeResponseData>({
|
||||
url: `themes/${encodeURIComponent(id)}`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Tokens from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取 API 令牌列表。 */
|
||||
/** List API tokens. */
|
||||
export function getTokensApi() {
|
||||
return request<Tokens.TokensResponseData>({
|
||||
url: 'tokens',
|
||||
@@ -10,7 +10,7 @@ export function getTokensApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建 API 令牌。 */
|
||||
/** Create an API token. */
|
||||
export function createTokenApi(data: Tokens.CreateTokenRequestData) {
|
||||
return request<Tokens.CreateTokenResponseData>({
|
||||
url: 'tokens',
|
||||
@@ -20,7 +20,7 @@ export function createTokenApi(data: Tokens.CreateTokenRequestData) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 撤销 API 令牌。 */
|
||||
/** Revoke an API token. */
|
||||
export function deleteTokenApi(id: string) {
|
||||
return request<Tokens.DeleteTokenResponseData>({
|
||||
url: `tokens/${encodeURIComponent(id)}`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Trash from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取回收站。 */
|
||||
/** Read the recycle bin. */
|
||||
export function getTrashApi() {
|
||||
return request<Trash.TrashResponseData>({
|
||||
url: 'trash',
|
||||
@@ -10,7 +10,7 @@ export function getTrashApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 恢复回收站条目。 */
|
||||
/** Restore a recycle-bin entry. */
|
||||
export function restoreTrashApi(id: string) {
|
||||
return request<Trash.RestoreTrashResponseData>({
|
||||
url: `trash/${encodeURIComponent(id)}/restore`,
|
||||
@@ -19,7 +19,7 @@ export function restoreTrashApi(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 清空回收站并返回逐项结果。 */
|
||||
/** Empty the recycle bin and return per-entry results. */
|
||||
export function clearTrashApi() {
|
||||
return request<Trash.ClearTrashResponseData>({
|
||||
url: 'trash',
|
||||
@@ -28,7 +28,7 @@ export function clearTrashApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 永久删除回收站条目。 */
|
||||
/** Permanently delete a recycle-bin entry. */
|
||||
export function purgeTrashApi(id: string) {
|
||||
return request<Trash.PurgeTrashResponseData>({
|
||||
url: `trash/${encodeURIComponent(id)}`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type * as Tree from './type'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取目录树。 */
|
||||
/** Read the directory tree. */
|
||||
export function getTreeApi() {
|
||||
return request<Tree.TreeResponseData>({
|
||||
url: 'tree',
|
||||
@@ -10,7 +10,7 @@ export function getTreeApi() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 设置或重置目录项图标。 */
|
||||
/** Set or reset a tree entry's icon. */
|
||||
export function setTreeIconApi(data: Tree.TreeIconRequestData) {
|
||||
return request<Tree.TreeIconResponseData>({
|
||||
url: 'tree-icon',
|
||||
|
||||
@@ -56,7 +56,7 @@ function render() {
|
||||
root.dataset.rmColorMode = dark ? 'dark' : 'light'
|
||||
root.dataset.rmTheme = appearancePaused ? 'default' : current.theme
|
||||
style('rm-theme', appearancePaused ? '' : themeCSS)
|
||||
// 独立样式元素避免某个片段的未闭合语法吞掉其他片段。
|
||||
// Separate style elements prevent unclosed syntax in one snippet from swallowing others.
|
||||
document.querySelectorAll('style[data-rm-snippet]').forEach(el => el.remove())
|
||||
if (!appearancePaused) {
|
||||
for (const snippet of current.cssSnippets) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import 'highlight.js/styles/atom-one-dark.css';
|
||||
@import './maple-font.css';
|
||||
|
||||
/* 基础主题。--rm-* 变量继续作为主题接口,字体在本地加载。 */
|
||||
/* Base theme. --rm-* variables remain the theme interface; fonts load locally. */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--rm-sidebar-width: 288px;
|
||||
@@ -10,7 +10,7 @@
|
||||
--rm-font-heading: var(--rm-font-editor, var(--rm-font-text));
|
||||
--rm-font-mono: 'Maple Mono Normal NL NF CN', monospace;
|
||||
--rm-font-size: 16px;
|
||||
/* 界面排版独立于正文;辅助文字随界面缩放,但不小于 13px。 */
|
||||
/* Interface typography is independent of body text; secondary text scales with the interface but stays at least 13px. */
|
||||
--rm-ui-font-size: 14px;
|
||||
--rm-ui-font-small: max(13px, calc(var(--rm-ui-font-size) - 2px));
|
||||
--rm-ui-font-caption: max(13px, calc(var(--rm-ui-font-size) - 3px));
|
||||
@@ -159,7 +159,7 @@ textarea {
|
||||
background: var(--rm-selection) !important;
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
/* Headings. */
|
||||
.cm-line:is(.cm-rm-h1, .cm-rm-h2, .cm-rm-h3, .cm-rm-h4, .cm-rm-h5, .cm-rm-h6) {
|
||||
line-height: 1.4;
|
||||
padding-top: 1rem;
|
||||
@@ -186,7 +186,7 @@ textarea {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Markdown 标记(激活行显示源码时) */
|
||||
/* Markdown markers shown as source on active lines. */
|
||||
.cm-rm-markup,
|
||||
.cm-rm-url {
|
||||
color: var(--rm-text-muted);
|
||||
@@ -198,7 +198,7 @@ textarea {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* 行内代码 */
|
||||
/* Inline code. */
|
||||
.cm-rm-inline-code {
|
||||
font-family: var(--rm-font-mono);
|
||||
font-size: 0.875em;
|
||||
@@ -208,8 +208,8 @@ textarea {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 代码块
|
||||
* 背景画在 ::before 上并置于选区图层之下:若直接给 .cm-line 设背景,会盖住 CodeMirror 绘制的选区 */
|
||||
/* Code blocks. Paint backgrounds on ::before below the selection
|
||||
* layer; a background on .cm-line would cover CodeMirror's selection. */
|
||||
.cm-line.cm-rm-code {
|
||||
position: relative;
|
||||
font-family: var(--rm-font-mono);
|
||||
@@ -246,7 +246,7 @@ textarea {
|
||||
.cm-line.cm-rm-code-begin.cm-rm-code-end::before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
/* 围栏展开前后保持同一行高,语言标签与复制按钮不参与行高计算,避免代码内容跳动。 */
|
||||
/* Keep fence line heights unchanged when expanded; language labels and copy buttons must not affect line height or shift code. */
|
||||
.cm-rm-code-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -291,14 +291,14 @@ textarea {
|
||||
color: var(--rm-text-muted);
|
||||
}
|
||||
|
||||
/* 引用 */
|
||||
/* Blockquotes. */
|
||||
.cm-line.cm-rm-quote {
|
||||
border-left: 3px solid var(--rm-accent);
|
||||
padding: 5px 15px;
|
||||
background: color-mix(in srgb, var(--rm-accent) 4%, transparent);
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
/* Lists. */
|
||||
.cm-rm-bullet {
|
||||
color: var(--rm-text-muted);
|
||||
display: inline-block;
|
||||
@@ -311,7 +311,7 @@ textarea {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
/* Thematic breaks. */
|
||||
.cm-rm-hr {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
@@ -319,7 +319,7 @@ textarea {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
/* Images. */
|
||||
.cm-rm-image {
|
||||
display: block;
|
||||
}
|
||||
@@ -332,7 +332,7 @@ textarea {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 表格:光标在表格内时以等宽源码显示,否则为可编辑的渲染表格 */
|
||||
/* Tables show monospaced source while the cursor is inside, otherwise an editable rendered table. */
|
||||
.cm-line.cm-rm-table {
|
||||
font-family: var(--rm-font-mono);
|
||||
font-size: 0.9em;
|
||||
@@ -379,7 +379,7 @@ textarea {
|
||||
.cm-rm-table-widget a {
|
||||
color: var(--rm-accent-strong);
|
||||
}
|
||||
/* 工具栏占据表格上方的固定空间,只切换可见性,不遮挡表格也不引起跳动 */
|
||||
/* Reserve fixed space above tables for the toolbar; toggle visibility without covering the table or shifting its position. */
|
||||
.cm-rm-table-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -452,7 +452,7 @@ textarea {
|
||||
.cm-editor .cm-content {
|
||||
padding: 16px 16px 40vh;
|
||||
}
|
||||
/* 触屏没有悬停,工具栏常显,按钮加大触控热区 */
|
||||
/* Touchscreens lack hover: keep the toolbar visible and enlarge button touch targets. */
|
||||
.cm-rm-table-toolbar {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
@@ -463,7 +463,7 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Typora 扩展语法 ---- */
|
||||
/* ---- Typora syntax extensions ---- */
|
||||
.cm-rm-highlight,
|
||||
.cm-rm-table-widget mark {
|
||||
background: var(--rm-highlight-bg);
|
||||
@@ -505,7 +505,7 @@ textarea {
|
||||
font-size: 0.9em;
|
||||
color: var(--rm-text-muted);
|
||||
}
|
||||
/* 编辑时显示在源码下方的预览 */
|
||||
/* Preview displayed below source while editing. */
|
||||
.cm-rm-preview {
|
||||
margin: 4px 0 8px;
|
||||
padding: 8px 12px;
|
||||
@@ -628,7 +628,7 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
/* opacity 保留排版和字体请求;等待期间不显示备用字体,也覆盖 Teleport 到 body 的菜单。 */
|
||||
/* opacity preserves layout and font requests; hide fallback fonts while waiting, including menus teleported to body. */
|
||||
:root:is([data-rm-fonts='loading'], [data-rm-fonts='error']) body > :not(.rm-font-error) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t as translate } from '@@/i18n/runtime'
|
||||
// 使用浏览器实际排版触发的字体集合,覆盖首屏、切换笔记与后来出现的字形。
|
||||
// Use the font set requested by actual browser layout, covering initial render, note switches, and later glyphs.
|
||||
export function watchFontLoading() {
|
||||
const root = document.documentElement
|
||||
const fonts = document.fonts
|
||||
@@ -22,7 +22,7 @@ export function watchFontLoading() {
|
||||
|
||||
function ready() {
|
||||
if (root.dataset.rmFonts === 'error') return
|
||||
// fonts.ready 在下载失败时也会完成,不能将失败当作字体已就绪。
|
||||
// fonts.ready also resolves after download failure; failure must not count as fonts being ready.
|
||||
if ([...fonts].some(font => font.status === 'error')) failed()
|
||||
else if (fonts.status === 'loaded') root.dataset.rmFonts = 'ready'
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export function watchFontLoading() {
|
||||
})
|
||||
fonts.addEventListener('loadingerror', failed)
|
||||
|
||||
// 挂载完成后触发一次排版,确保等待的是当前文字用到的字体。
|
||||
// Trigger layout after mounting so the wait covers fonts used by the current text.
|
||||
requestAnimationFrame(() => {
|
||||
document.body.getBoundingClientRect()
|
||||
void fonts.ready.then(ready)
|
||||
|
||||
@@ -13,7 +13,7 @@ export function applyFontCatalog(fonts: CustomFont[]) {
|
||||
if (!style) {
|
||||
style = document.createElement('style'); style.id = 'rm-custom-fonts'; document.head.append(style)
|
||||
}
|
||||
// 名称仅用于界面;CSS 标识和资源路径只由服务端内容哈希构成。
|
||||
// Names are display-only; CSS identifiers and resource paths use only server-generated content hashes.
|
||||
const css = fonts.map(font => `@font-face { font-family: '${font.id}'; src: url('${fontResourceRoot()}${font.id}/file'); font-display: block; }`).join('\n')
|
||||
if (style.textContent !== css) style.textContent = css
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Maple Mono v7.9;由 scripts/prepare-maple-font.py 生成,授权见 fonts/maple/LICENSE.txt。 */
|
||||
/* Maple Mono v7.9; generated by scripts/prepare-maple-font.py. See fonts/maple/LICENSE.txt for licensing. */
|
||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-00.woff2') format('woff2'); unicode-range: U+0-FFF; }
|
||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-01.woff2') format('woff2'); unicode-range: U+1000-1FFF; }
|
||||
@font-face { font-family: 'Maple Mono Normal NL NF CN'; font-style: normal; font-weight: 400; font-display: block; src: url('./fonts/maple/regular-02.woff2') format('woff2'); unicode-range: U+2000-2FFF; }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t as translate } from '@@/i18n/runtime'
|
||||
// 编辑器排版设置;账户、快捷键等设置不进入此模块。
|
||||
// Editor typography settings only; account, shortcuts, and other settings do not belong here.
|
||||
const maple = { label: 'Maple Mono Normal NL NF CN', css: '\'Maple Mono Normal NL NF CN\', monospace' }
|
||||
export const TEXT_FONTS = {
|
||||
maple,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* 应用外壳样式,颜色与字体沿用 themes/base.css 中的 --rm-* 变量 */
|
||||
/* Application shell styles; colors and fonts use --rm-* variables from common/appearance/base.css. */
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
/* 软键盘弹出时为可视区域高度,见 viewport.ts */
|
||||
/* Use the visible viewport height when the software keyboard opens; see viewport.ts. */
|
||||
#app {
|
||||
height: var(--rm-app-height, 100%);
|
||||
display: flex;
|
||||
@@ -52,7 +52,7 @@ button {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ---- 通用控件 ---- */
|
||||
/* ---- Shared controls ---- */
|
||||
.rm-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -121,7 +121,7 @@ button {
|
||||
color: var(--rm-text-muted);
|
||||
}
|
||||
|
||||
/* ---- 登录 ---- */
|
||||
/* ---- Sign-in ---- */
|
||||
.rm-auth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -159,7 +159,7 @@ button {
|
||||
font-size: var(--rm-ui-font-small);
|
||||
}
|
||||
|
||||
/* ---- 布局 ---- */
|
||||
/* ---- Layout ---- */
|
||||
.rm-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -220,7 +220,7 @@ button {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* ---- 大纲 ---- */
|
||||
/* ---- Outline ---- */
|
||||
.rm-outline-backdrop {
|
||||
display: flex;
|
||||
flex: none;
|
||||
@@ -292,7 +292,7 @@ button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---- 侧栏 ---- */
|
||||
/* ---- Sidebar ---- */
|
||||
.rm-sidebar-inner {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
@@ -346,7 +346,7 @@ button {
|
||||
--rm-tree-row-overlay: var(--rm-hover);
|
||||
background: var(--rm-hover);
|
||||
}
|
||||
/* 长按由应用处理,不弹出 iOS 的系统菜单 */
|
||||
/* The application handles long presses instead of showing the iOS system menu. */
|
||||
.rm-tree-row {
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
@@ -403,7 +403,7 @@ button {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* 按钮覆盖名称末尾,不为隐藏按钮留位;不透明底色遮住下方文字。 */
|
||||
/* Buttons overlay the end of the name without reserving hidden space; an opaque background covers underlying text. */
|
||||
.rm-tree-row .rm-tree-more {
|
||||
background: linear-gradient(var(--rm-tree-row-overlay), var(--rm-tree-row-overlay)), var(--rm-sidebar-bg);
|
||||
}
|
||||
@@ -432,7 +432,7 @@ button {
|
||||
font-size: var(--rm-ui-font-small);
|
||||
text-align: center;
|
||||
}
|
||||
/* ---- 搜索 ---- */
|
||||
/* ---- Search ---- */
|
||||
.rm-search {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -515,7 +515,7 @@ button {
|
||||
color: var(--rm-text);
|
||||
border-radius: 2px;
|
||||
}
|
||||
/* ---- 快速切换 ---- */
|
||||
/* ---- Quick switcher ---- */
|
||||
.rm-switcher-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
@@ -583,7 +583,7 @@ button {
|
||||
color: var(--rm-text);
|
||||
}
|
||||
|
||||
/* ---- 菜单 ---- */
|
||||
/* ---- Menus ---- */
|
||||
.rm-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
@@ -626,7 +626,7 @@ button {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* ---- 标签页 ---- */
|
||||
/* ---- Tabs ---- */
|
||||
.rm-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
@@ -733,7 +733,7 @@ button {
|
||||
border-radius: 50%;
|
||||
background: var(--rm-accent);
|
||||
}
|
||||
/* 有未保存修改时显示圆点,悬停时换回关闭图标 */
|
||||
/* Show a dot for unsaved changes; replace it with the close icon on hover. */
|
||||
.rm-tab-close.dirty .rm-dirty-dot {
|
||||
display: block;
|
||||
}
|
||||
@@ -747,7 +747,7 @@ button {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* ---- 提示条 ---- */
|
||||
/* ---- Status bars ---- */
|
||||
.rm-banner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -767,7 +767,7 @@ button {
|
||||
color: var(--rm-warn-text);
|
||||
}
|
||||
|
||||
/* ---- 编辑区 ---- */
|
||||
/* ---- Editor area ---- */
|
||||
.rm-editor-pane {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -780,7 +780,7 @@ button {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ---- 对话框 ---- */
|
||||
/* ---- Dialogs ---- */
|
||||
.rm-dialog-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -823,7 +823,7 @@ button {
|
||||
font-size: var(--rm-ui-font-small);
|
||||
user-select: all;
|
||||
}
|
||||
/* ---- 设置 ---- */
|
||||
/* ---- Settings ---- */
|
||||
.rm-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -849,7 +849,7 @@ button {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* ---- 通知 ---- */
|
||||
/* ---- Notifications ---- */
|
||||
.rm-toasts {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
@@ -883,7 +883,7 @@ button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ---- 移动端 ---- */
|
||||
/* ---- Mobile ---- */
|
||||
@media (max-width: 767px) {
|
||||
.rm-sidebar {
|
||||
position: fixed;
|
||||
@@ -966,7 +966,7 @@ button {
|
||||
.rm-sidebar-resize {
|
||||
display: none;
|
||||
}
|
||||
/* 大纲在手机上为从右侧滑入的浮层 */
|
||||
/* On phones, the outline is an overlay sliding in from the right. */
|
||||
.rm-outline-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
@@ -984,12 +984,12 @@ button {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
box-shadow: var(--rm-shadow);
|
||||
}
|
||||
/* 触控热区不小于 44px */
|
||||
/* Touch targets must be at least 44px. */
|
||||
.rm-tree-row {
|
||||
height: var(--rm-control-size);
|
||||
font-size: inherit;
|
||||
}
|
||||
/* 小于 16px 时 iOS Safari 聚焦输入框会自动放大页面 */
|
||||
/* iOS Safari zooms focused inputs when their font size is below 16px. */
|
||||
.rm-search-input {
|
||||
height: var(--rm-control-size);
|
||||
font-size: var(--rm-ui-font-size);
|
||||
@@ -1001,14 +1001,14 @@ button {
|
||||
.rm-search-item {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
/* 手机上从顶部弹出,给软键盘留出空间 */
|
||||
/* Open from the top on phones to leave room for the software keyboard. */
|
||||
.rm-switcher-item {
|
||||
height: var(--rm-control-size);
|
||||
font-size: var(--rm-ui-font-size);
|
||||
}
|
||||
}
|
||||
|
||||
/* 主题与 CSS 编辑 */
|
||||
/* Theme and CSS editing. */
|
||||
.rm-appearance-note {
|
||||
margin: var(--rm-space-2) var(--rm-space-4) var(--rm-space-4);
|
||||
font-size: var(--rm-ui-font-small);
|
||||
@@ -1195,7 +1195,7 @@ button {
|
||||
background: var(--rm-active);
|
||||
}
|
||||
|
||||
/* 轻玻璃工作台 */
|
||||
/* Glass workspace. */
|
||||
.rm-vault-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1348,7 +1348,7 @@ button {
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
/* 选区工具栏与局部外观面板 */
|
||||
/* Selection toolbar and local appearance panel. */
|
||||
.rm-selection-popover {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
@@ -1597,7 +1597,7 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
/* 文件树子层按实际内容高度展开;退出期间不能继续点击或聚焦。 */
|
||||
/* Tree children expand to their actual content height; exiting items must not remain clickable or focusable. */
|
||||
.rm-tree-children {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
gap: inherit;
|
||||
}
|
||||
|
||||
/* 复用 RawMark 主题,弹出层与页面使用相同的字体、色彩和密度。 */
|
||||
/* Reuse the RawMark theme so popovers and pages share fonts, colors, and density. */
|
||||
:root {
|
||||
--el-font-size-small: var(--rm-ui-font-small);
|
||||
--el-font-size-extra-small: var(--rm-ui-font-small);
|
||||
|
||||
@@ -20,7 +20,7 @@ async function update() {
|
||||
return
|
||||
}
|
||||
if (!activated.value && worker) {
|
||||
// 等待新版激活后,仅刷新主动点击更新的页面。
|
||||
// After the new version activates, refresh only the page that requested the update.
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const finish = () => {
|
||||
if (worker.state !== 'activated' && worker.state !== 'redundant') return
|
||||
@@ -39,7 +39,7 @@ async function update() {
|
||||
})
|
||||
activated.value = true
|
||||
}
|
||||
// 激活期间仍可能继续输入,刷新前再次确认;保留按钮供保存后重试。
|
||||
// Typing may continue during activation; confirm again before refreshing and keep the button for retrying after save.
|
||||
if (await props.beforeReload()) location.reload()
|
||||
else toast(translate('common.some_changes_or_operations_are_a5fda9'))
|
||||
} catch {
|
||||
|
||||
@@ -67,7 +67,7 @@ async function importFont(event: Event) {
|
||||
try {
|
||||
if (file.size > 30 * 1024 * 1024) throw new Error(translate('common.font_files_must_not_exceed_cc001c'))
|
||||
if (!/\.(ttf|otf|woff2?)$/i.test(file.name)) throw new Error(translate('common.choose_a_ttf_otf_woff_eb6ab7'))
|
||||
// 让浏览器校验实际字体;无效字体不会上传或进入页面的字体加载集合。
|
||||
// Let the browser validate the actual font; invalid fonts are neither uploaded nor added to the page's loading set.
|
||||
const data = await file.arrayBuffer()
|
||||
try {
|
||||
await new FontFace('rawmark-import-check', data).load()
|
||||
|
||||
@@ -22,7 +22,7 @@ function onSystem() {
|
||||
system.addEventListener('change', onSystem)
|
||||
onBeforeUnmount(() => system.removeEventListener('change', onSystem))
|
||||
const dark = computed(() => (fixedThemeMode(settings.theme) ?? (settings.colorMode === 'system' ? (systemDark.value ? 'dark' : 'light') : settings.colorMode)) === 'dark')
|
||||
// 思源色板顺序;只影响选中文字,不修改主题背景。
|
||||
// SiYuan palette order; affect only selected text, not the theme background.
|
||||
const foreground = computed(() => dark.value
|
||||
? ['#f39993', '#ffd599', '#a6d5fa', '#b7dfb9', '#9aa0a6', '#3575f0', '#f3a92f', '#d23f31', '#f47c9c', '#4dd0e1', '#10df54', '#ecd424', '#1e1e1e']
|
||||
: ['#790600', '#8a4f00', '#005599', '#006b04', '#5f6368', '#3575f0', '#ff9200', '#d23f31', '#a40074', '#007080', '#006e2a', '#6b5a00', '#ffffff'])
|
||||
|
||||
@@ -8,7 +8,7 @@ const CACHE_LIMIT = 500
|
||||
let worker: Worker | undefined
|
||||
let nextId = 1
|
||||
const pending = new Map<number, { resolve: (tokens: TokenRun | null) => void, reject: (reason: unknown) => void }>()
|
||||
// 以"语言 + 代码"为键缓存结果;Map 保持插入顺序,超限时淘汰最早的条目
|
||||
// Cache by "language + code"; Map preserves insertion order, so evict the oldest entry when the limit is exceeded.
|
||||
const cache = new Map<string, TokenRun | null>()
|
||||
|
||||
function getWorker(): Worker {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 主线程与高亮 Worker 之间的消息格式
|
||||
// Message format between the main thread and highlight Worker.
|
||||
|
||||
export interface HighlightRequest {
|
||||
id: number
|
||||
@@ -6,8 +6,8 @@ export interface HighlightRequest {
|
||||
code: string
|
||||
}
|
||||
|
||||
// 保留 highlight.js 的嵌套 token 与原文,编辑器使用官方 token 类样式。
|
||||
// Preserve highlight.js nested tokens and source text; the editor uses official token-class styles.
|
||||
export type TokenRun = (string | { classes: string, content: TokenRun })[]
|
||||
|
||||
// 语言不受支持时 tokens 为 null;实际失败必须与纯文本结果区分。
|
||||
// tokens is null for unsupported languages; actual failures must remain distinct from plain-text results.
|
||||
export type HighlightResponse = { id: number, tokens: TokenRun | null } | { id: number, error: Error }
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-template-curly-in-string -- 测试样例需要保留源码中的模板插值。 */
|
||||
/* eslint-disable no-template-curly-in-string -- Test fixtures must retain source template interpolation. */
|
||||
import type { HighlightResponse, TokenRun } from './protocol'
|
||||
import { afterAll, beforeAll, expect, it, vi } from 'vitest'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HighlightRequest, HighlightResponse, TokenRun } from './protocol'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
// 只转换 highlight.js 生成的 span 和转义文本,不向编辑器插入 HTML。
|
||||
// Convert only highlight.js-generated spans and escaped text; never insert HTML into the editor.
|
||||
function serialize(html: string): TokenRun {
|
||||
const root: TokenRun = []
|
||||
const stack = [root]
|
||||
@@ -23,7 +23,7 @@ function serialize(html: string): TokenRun {
|
||||
function highlight(req: HighlightRequest): TokenRun | null {
|
||||
if (!hljs.getLanguage(req.lang)) return null
|
||||
const result = hljs.highlight(req.code, { language: req.lang, ignoreIllegals: true })
|
||||
// 库的安全模式会随纯文本结果返回异常,仍需传递真实失败原因。
|
||||
// The library's safe mode may return an exception alongside plain text; propagate the actual failure.
|
||||
if (result.errorRaised) throw result.errorRaised
|
||||
return serialize(result.value)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function applyLocale(value: Locale) {
|
||||
if (typeof document !== 'undefined') document.documentElement.lang = value
|
||||
}
|
||||
|
||||
/** 登录页仅缓存显示语言;登录后由服务器账户设置覆盖。 */
|
||||
/** The sign-in page caches display language only; server account settings override it after authentication. */
|
||||
export function chooseAnonymousLocale(value: Locale) {
|
||||
const cached = JSON.parse(localStorage.getItem('rawmark.settings') ?? '{}')
|
||||
localStorage.setItem('rawmark.settings', JSON.stringify({ ...cached, locale: value }))
|
||||
|
||||
@@ -4,7 +4,7 @@ import { typoraExtensions } from './extensions'
|
||||
|
||||
const parser = base.configure([GFM, ...typoraExtensions])
|
||||
|
||||
/** 列出指定类型节点覆盖的原文 */
|
||||
/** List original text covered by nodes of the specified type. */
|
||||
function nodes(src: string, name: string): string[] {
|
||||
const out: string[] = []
|
||||
parser.parse(src).iterate({
|
||||
@@ -57,7 +57,7 @@ describe('typora 扩展语法', () => {
|
||||
expect(nodes('---\ntitle: a\n...\n正文', 'FrontMatter')).toEqual(['---\ntitle: a\n...'])
|
||||
expect(nodes('正文\n\n---\na: b\n---', 'FrontMatter')).toEqual([])
|
||||
expect(nodes('正文\n\n---\n', 'HorizontalRule')).toEqual(['---'])
|
||||
// 闭合后的 --- 照常解析为分隔线
|
||||
// After front matter closes, --- is parsed as a thematic break again.
|
||||
expect(nodes('---\na: 1\n---\n\n---\n', 'HorizontalRule')).toEqual(['---'])
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BlockContext, LeafBlockParser, Line, MarkdownConfig } from '@lezer/markdown'
|
||||
// Typora 扩展语法:==高亮==、$行内公式$、$$公式块$$、脚注、[TOC]、YAML front matter。
|
||||
// 上标 ^x^、下标 ~x~、:emoji: 由 @lezer/markdown 自带的扩展解析
|
||||
// Typora extensions: ==highlight==, $inline math$, $$math blocks$$, footnotes, [TOC], and YAML front matter.
|
||||
// Superscript ^x^, subscript ~x~, and :emoji: use built-in @lezer/markdown extensions.
|
||||
import { tags } from '@lezer/highlight'
|
||||
import { inlineStyleSyntax } from './inline-styles'
|
||||
|
||||
@@ -8,7 +8,7 @@ const PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~\xA1\u2010-\u2027]/
|
||||
|
||||
const highlightDelim = { resolve: 'Highlight', mark: 'HighlightMark' }
|
||||
|
||||
/** ==高亮==,定界规则与 GFM 删除线相同 */
|
||||
/** ==highlight== uses the same delimiter rules as GFM strikethrough. */
|
||||
const Highlight: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
{ name: 'Highlight', style: { 'Highlight/...': tags.special(tags.content) } },
|
||||
@@ -39,8 +39,8 @@ const Highlight: MarkdownConfig = {
|
||||
}
|
||||
|
||||
/**
|
||||
* $行内公式$(Pandoc 规则):开头 $ 后不能是空白,结尾 $ 前不能是空白、后面不能紧跟数字,
|
||||
* 因此"$5 和 $10"这类金额不会被当成公式;\$ 为转义
|
||||
* $inline math$ follows Pandoc rules: no whitespace after the opening $ or before the closing $, and
|
||||
* no digit immediately after the closing $. Thus "$5 and $10" is not math; \$ is escaped.
|
||||
*/
|
||||
const InlineMath: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
@@ -70,8 +70,8 @@ const InlineMath: MarkdownConfig = {
|
||||
const isMathFence = (line: Line) => /^\$\$\s*$/.test(line.text.slice(line.pos))
|
||||
|
||||
/**
|
||||
* 独占一行的 $$ 之间为公式块,未闭合时延续到文档末尾。只在顶层识别:解析器的公开接口无法判断
|
||||
* 下一行是否仍属于引用、列表等容器,与表格一样,容器内的公式块以源码显示
|
||||
* Standalone $$ lines delimit math blocks; unclosed blocks extend to the document end. Recognize only top-level blocks: the public parser
|
||||
* API cannot tell whether the next line remains in a quote/list container, so nested blocks show source, like tables.
|
||||
*/
|
||||
const BlockMath: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
@@ -103,7 +103,7 @@ const BlockMath: MarkdownConfig = {
|
||||
|
||||
const FOOTNOTE_LABEL = /^\[\^([^\]\s]+)\]/
|
||||
|
||||
/** 脚注引用 [^label] 与脚注定义 [^label]: 内容(定义可跨行,直到空行) */
|
||||
/** Footnote references [^label] and definitions [^label]: content; definitions may span lines until a blank line. */
|
||||
const Footnote: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
{ name: 'FootnoteReference', style: tags.labelName },
|
||||
@@ -128,8 +128,8 @@ const Footnote: MarkdownConfig = {
|
||||
const m = /^\[\^[^\]\s]+\]:/.exec(leaf.content)
|
||||
if (!m) return null
|
||||
const self: LeafBlockParser = {
|
||||
// "[^1]: 文字"同时也是合法的链接引用定义;本解析器排在前面,续行时去掉其他候选,整块归为脚注定义。
|
||||
// 须原地修改:调用方正在遍历这个数组
|
||||
// "[^1]: text" is also a valid link reference definition; run this parser first and remove other continuation candidates to keep the entire block a footnote.
|
||||
// Mutate in place because the caller is iterating this array.
|
||||
nextLine(_cx, _line, leaf) {
|
||||
leaf.parsers.splice(0, leaf.parsers.length, self)
|
||||
return false
|
||||
@@ -148,7 +148,7 @@ const Footnote: MarkdownConfig = {
|
||||
],
|
||||
}
|
||||
|
||||
/** 独占一行的 [TOC](不区分大小写)显示为目录 */
|
||||
/** A standalone [TOC], case-insensitively, displays a table of contents. */
|
||||
const TableOfContents: MarkdownConfig = {
|
||||
defineNodes: [{ name: 'TableOfContents', block: true, style: tags.meta }],
|
||||
parseBlock: [
|
||||
@@ -168,8 +168,8 @@ const TableOfContents: MarkdownConfig = {
|
||||
const isFrontMatterEnd = (line: Line) => /^(---|\.\.\.)\s*$/.test(line.text)
|
||||
|
||||
/**
|
||||
* 文档首行为 --- 时,到下一个 --- 或 ... 为 YAML front matter。解析器无法向后预读整个文档,
|
||||
* 未闭合时延续到文末——与 Typora(首行输入 --- 即创建 front matter)及 GitHub、Hugo 的识别方式一致
|
||||
* When the first line is ---, YAML front matter runs through the next --- or ... . The parser cannot look ahead
|
||||
* through the whole document, so unclosed front matter extends to the end, matching Typora's immediate creation after --- and GitHub/Hugo recognition.
|
||||
*/
|
||||
const FrontMatter: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
|
||||
@@ -6,7 +6,7 @@ import { commonmarkParser, renderMarkdown } from './html'
|
||||
const window = new Window()
|
||||
const document = window.document
|
||||
|
||||
// 规范化 HTML:解析为 DOM 后重新序列化,消除实体写法、标签间空白和 URL 编码差异
|
||||
// Normalize HTML by parsing and reserializing the DOM to remove entity, inter-tag whitespace, and URL-encoding differences.
|
||||
function normalize(html: string): string {
|
||||
const root = document.createElement('div')
|
||||
root.innerHTML = html
|
||||
@@ -17,7 +17,7 @@ function normalize(html: string): string {
|
||||
try {
|
||||
el.setAttribute(attr, decodeURI(v))
|
||||
} catch {
|
||||
// 非法的百分号序列保持原样比较
|
||||
// Compare invalid percent-escape sequences without changing them.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,9 +40,9 @@ const examples = specTests.map(t => ({
|
||||
html: t.html.replace(/→/g, '\t'),
|
||||
}))
|
||||
|
||||
// @lezer/markdown 1.8.0 解析结果本身偏离规范的用例(已逐个核对语法树,非渲染器问题):
|
||||
// 制表符 ATX 标题、空列表项后的段落、<textarea> HTML 块、方括号嵌套与链接优先级、非法 HTML 标签/注释。
|
||||
// 用 it.fails 标记:上游修复后这些用例会转为通过并报错,提醒移出此列表。
|
||||
// Cases where @lezer/markdown 1.8.0 itself differs from the specification (syntax trees checked individually; not renderer defects):
|
||||
// Tab-separated ATX headings, paragraphs after empty list items, <textarea> HTML blocks, bracket nesting/link precedence, and invalid HTML tags/comments.
|
||||
// Use it.fails so an upstream fix makes these cases pass and fail the test run, prompting removal from this list.
|
||||
const PARSER_DEVIATIONS = new Set([10, 171, 280, 493, 512, 523, 528, 569, 571, 621, 625, 626])
|
||||
|
||||
describe('安全模式', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { parser as commonmarkParser, Emoji, GFM, Subscript, Superscript } from '
|
||||
import { typoraExtensions } from './extensions'
|
||||
import { inlineStyleText, stylesFromTag } from './inline-styles'
|
||||
|
||||
// 与编辑器一致的语法集(lang-markdown 的 markdownLanguage 加 Typora 扩展)
|
||||
// Match the editor's syntax set: lang-markdown's markdownLanguage plus Typora extensions.
|
||||
export const markdownParser = commonmarkParser.configure([GFM, Subscript, Superscript, Emoji, ...typoraExtensions])
|
||||
|
||||
export { commonmarkParser }
|
||||
@@ -27,7 +27,7 @@ export function escapeHtml(s: string): string {
|
||||
return s.replace(/[&<>"]/g, c => (c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : '"'))
|
||||
}
|
||||
|
||||
// 属性值中保留实体引用(交给浏览器解码),只转义其余的特殊字符
|
||||
// Preserve entity references in attributes for browser decoding; escape only other special characters.
|
||||
function escapeAttr(s: string): string {
|
||||
return s.replace(/&(?!(?:#\d{1,7}|#x[\da-f]{1,6}|[A-Z][A-Z\d]{1,31});)|[<>"]/gi, c =>
|
||||
c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : '"')
|
||||
@@ -37,7 +37,7 @@ function unescapeBackslashes(s: string): string {
|
||||
return s.replace(/\\([\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E])/g, '$1')
|
||||
}
|
||||
|
||||
// 对非法 URL 字符做百分号编码,已有的 %XX 保留
|
||||
// Percent-encode invalid URL characters while preserving existing %XX escapes.
|
||||
function normalizeUrl(s: string): string {
|
||||
return s.replace(/%(?![\da-f]{2})|[^\w\-.~:/?#[\]@!$&'()*+,;=%]/gi, c => encodeURIComponent(c))
|
||||
}
|
||||
@@ -52,7 +52,7 @@ function tabWidth(col: number): number {
|
||||
|
||||
export interface RenderOptions {
|
||||
parser?: MarkdownParser
|
||||
// 为 true 时原样输出原始 HTML、不过滤危险链接协议;仅用于可信内容(如规范测试)
|
||||
// When true, preserve raw HTML and unsafe link schemes; use only with trusted content such as specification tests.
|
||||
unsafe?: boolean
|
||||
}
|
||||
|
||||
@@ -60,12 +60,12 @@ const UNSAFE_PROTOCOL = /^\s*(javascript|vbscript|file|data):/i
|
||||
const SAFE_DATA_IMAGE = /^\s*data:image\/(png|gif|jpeg|webp);/i
|
||||
|
||||
function safeHref(href: string): string {
|
||||
// 协议由 ASCII 组成;属性实体会被浏览器解码,URL 解析还会忽略制表符和换行。
|
||||
// Schemes are ASCII; browsers decode attribute entities, and URL parsing also ignores tabs and newlines.
|
||||
const protocol = href.replace(/&#(?:(\d{1,7})|[xX]([\da-fA-F]{1,6}));|&(colon|Tab|NewLine);/g, (entity, decimal, hex, named) => {
|
||||
if (named) return named === 'colon' ? ':' : named === 'Tab' ? '\t' : '\n'
|
||||
const code = decimal ? Number(decimal) : Number.parseInt(hex, 16)
|
||||
return code <= 127 ? String.fromCharCode(code) : entity
|
||||
// eslint-disable-next-line no-control-regex -- URL 安全检查必须移除 ASCII 控制字符。
|
||||
// eslint-disable-next-line no-control-regex -- URL safety checks must remove ASCII control characters.
|
||||
}).replace(/[\t\r\n]/g, '').replace(/^[\x00-\x20]+/, '')
|
||||
return UNSAFE_PROTOCOL.test(protocol) && !SAFE_DATA_IMAGE.test(protocol) ? '#' : href
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
return node ? unescapeBackslashes(text(node.from + 1, node.to - 1)) : null
|
||||
}
|
||||
|
||||
// ---- 容器前缀剥离:代码块、HTML 块需要按 CommonMark 的列规则去掉引用符与列表缩进 ----
|
||||
// ---- Strip container prefixes: code and HTML blocks must remove quote markers and list indentation using CommonMark column rules ----
|
||||
|
||||
function containers(node: SyntaxNode): SyntaxNode[] {
|
||||
const out: SyntaxNode[] = []
|
||||
@@ -125,7 +125,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
return col
|
||||
}
|
||||
|
||||
// 列表项内容的起始列(绝对列号)
|
||||
// Absolute starting column of list-item content.
|
||||
function itemContentColumn(item: SyntaxNode): number {
|
||||
const mark = item.getChild('ListMark')!
|
||||
const markEnd = columnAt(mark.to)
|
||||
@@ -137,7 +137,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
return col
|
||||
}
|
||||
|
||||
// 去掉一行开头属于容器的部分,返回剩余文本(被拆开的制表符以空格补齐)
|
||||
// Remove the container prefix from a line and return the remaining text, padding split tabs with spaces.
|
||||
function stripContainers(lineFrom: number, cs: SyntaxNode[]): { rest: string, col: number } {
|
||||
const end = lineEnd(lineFrom)
|
||||
let i = lineFrom
|
||||
@@ -177,7 +177,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
} else {
|
||||
const target = itemContentColumn(c)
|
||||
if (lineStart(c.from) === lineFrom) {
|
||||
// 列表项首行:跳过列表标记本身
|
||||
// First list-item line: skip the list marker itself.
|
||||
const mark = c.getChild('ListMark')!
|
||||
col = columnAt(mark.to)
|
||||
i = mark.to
|
||||
@@ -191,11 +191,11 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
eatColumns(target)
|
||||
}
|
||||
}
|
||||
// pad 个空格(被拆开的制表符剩余部分)从第 col 列开始
|
||||
// pad spaces (the remainder of a split tab) start at column col.
|
||||
return { rest: ' '.repeat(pad) + src.slice(i, end), col }
|
||||
}
|
||||
|
||||
// 去掉至多 n 列的前导空白;startCol 为该行文本起始的绝对列,用于计算制表符宽度
|
||||
// Remove up to n columns of leading whitespace; startCol is the line's absolute starting column for tab-width calculation.
|
||||
function stripIndent({ rest: line, col: startCol }: Line, n: number): string {
|
||||
let col = 0
|
||||
let i = 0
|
||||
@@ -212,7 +212,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
const cs = containers(node)
|
||||
const lines: Line[] = []
|
||||
let pos = lineStart(node.from)
|
||||
// 未闭合的块会延伸到文末换行之后,那一行不属于内容
|
||||
// Unclosed blocks extend past the final newline; that trailing line is not content.
|
||||
const last = lineStart(node.to > node.from && src[node.to - 1] === '\n' ? node.to - 1 : node.to)
|
||||
for (let first = true; pos <= last; first = false) {
|
||||
const isLast = pos === last
|
||||
@@ -223,12 +223,12 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
return lines
|
||||
}
|
||||
|
||||
// ---- 行内 ----
|
||||
// ---- Inline ----
|
||||
|
||||
function inline(parent: SyntaxNode, from: number, to: number): string {
|
||||
let out = ''
|
||||
let pos = from
|
||||
// 引用符和硬换行之后的行首空白不属于内容
|
||||
// Leading whitespace after quote markers or hard breaks is not content.
|
||||
let afterQuote = false
|
||||
for (let ch = parent.firstChild; ch; ch = ch.nextSibling) {
|
||||
if (ch.to <= from || ch.from >= to) continue
|
||||
@@ -289,7 +289,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
const styles = stylesFromTag(text(open.from, open.to))!
|
||||
return `<span style="${escapeAttr(inlineStyleText(styles))}">${innerOf(node, 'InlineStyleMark')}</span>`
|
||||
}
|
||||
// 公式与 emoji 输出占位元素,由 render.ts 的 hydrate 按需加载依赖后填充
|
||||
// Math and emoji emit placeholders; render.ts hydrate loads dependencies and fills them as needed.
|
||||
case 'InlineMath': {
|
||||
const tex = text(node.from + 1, node.to - 1)
|
||||
return `<span data-math="${escapeHtml(tex)}">${escapeHtml(tex)}</span>`
|
||||
@@ -320,7 +320,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
return `<a href="${escapeAttr(linkHref(normalizeUrl(href)))}">${escapeHtml(raw)}</a>`
|
||||
}
|
||||
case 'URL': {
|
||||
// GFM 裸链接
|
||||
// GFM autolinks.
|
||||
const raw = text(node.from, node.to)
|
||||
const href = raw.startsWith('www.') ? `http://${raw}` : raw.includes('@') && !raw.includes(':') ? `mailto:${raw}` : raw
|
||||
return `<a href="${escapeAttr(linkHref(normalizeUrl(href)))}">${escapeHtml(raw)}</a>`
|
||||
@@ -349,7 +349,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
const key = label && text(label.from, label.to) !== '[]' ? text(label.from, label.to) : text(labelFrom - 1, closeBracket.to)
|
||||
def = defs.get(normalizeLabel(key))
|
||||
if (!def) {
|
||||
// 引用未定义:按字面文本输出
|
||||
// Undefined reference: output literal text.
|
||||
const tail = label ? escapeHtml(unescapeBackslashes(text(label.from, label.to))) : ''
|
||||
return `${isImage ? '!' : ''}[${content}]${tail}`
|
||||
}
|
||||
@@ -359,7 +359,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
return `<a href="${escapeAttr(linkHref(def.href))}"${title}>${content}</a>`
|
||||
}
|
||||
|
||||
// ---- 块级 ----
|
||||
// ---- Blocks ----
|
||||
|
||||
function isBlank(line: string): boolean {
|
||||
return /^[ \t>]*$/.test(line)
|
||||
@@ -477,10 +477,10 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
const name = text(label.from + 2, label.to - 2)
|
||||
return `<div class="footnote" id="${escapeAttr(footnoteId(name))}"><sup>${escapeHtml(name)}</sup> ${inline(node, label.to, node.to).trim()}</div>\n`
|
||||
}
|
||||
// 目录占位,由调用方根据标题填充
|
||||
// Table-of-contents placeholder; the caller fills it from headings.
|
||||
case 'TableOfContents':
|
||||
return '<nav class="toc"></nav>\n'
|
||||
// front matter 是元数据,不输出
|
||||
// Front matter is metadata and is not rendered.
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
@@ -531,7 +531,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
return blocks(tree.topNode)
|
||||
}
|
||||
|
||||
// 渲染单行行内 Markdown(表格单元格等),不产生块级结构
|
||||
// Render single-line inline Markdown, such as table cells, without creating block structures.
|
||||
export function renderInlineMarkdown(src: string, options?: RenderOptions): string {
|
||||
const html = renderMarkdown(src, options)
|
||||
const m = /^<p>([\s\S]*)<\/p>\n$/.exec(html)
|
||||
|
||||
@@ -6,7 +6,7 @@ export const HOLLOW = '0.2px currentColor'
|
||||
export const SHADOW = '1px 1px 2px currentColor'
|
||||
export type InlineStyles = Partial<Record<'color' | 'background-color' | 'font-family' | 'font-size' | '-webkit-text-stroke' | '-webkit-text-fill-color' | 'text-shadow', string>>
|
||||
|
||||
/** 笔记是外部输入;仅允许工具栏实际生成的样式,不开放通用 HTML/CSS。 */
|
||||
/** Notes are external input; allow only styles actually produced by the toolbar, not general HTML/CSS. */
|
||||
export function parseInlineStyles(value: string): InlineStyles | null {
|
||||
const result: InlineStyles = {}
|
||||
for (const declaration of value.split(';')) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 浏览器在呈现链接属性时会解码实体;源码操作也须按相同规则解析目标。
|
||||
// Browsers decode entities in link attributes; source operations must resolve destinations using the same rules.
|
||||
export function decodeLinkDestination(raw: string): string {
|
||||
const field = document.createElement('textarea')
|
||||
return raw.replace(/\\([\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E])/g, '$1').replace(/&(?:#\d{1,7}|#x[\da-f]{1,6}|[A-Z][A-Z\d]{1,31});/gi, (entity) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 数学公式、Mermaid 图表、emoji 的渲染。依赖体积较大,首次用到时才加载(各自打成独立的包)
|
||||
// Render math, Mermaid, and emoji. Load these large dependencies on first use as separate chunks.
|
||||
import type { MermaidConfig } from 'mermaid'
|
||||
|
||||
let katexLoad: Promise<typeof import('katex').default> | undefined
|
||||
@@ -7,7 +7,7 @@ function loadKatex() {
|
||||
return katexLoad
|
||||
}
|
||||
|
||||
/** 渲染公式;语法错误以红色显示错误信息而不是抛出 */
|
||||
/** Render math; display syntax errors in red instead of throwing. */
|
||||
export async function renderMath(el: HTMLElement, tex: string, displayMode: boolean): Promise<void> {
|
||||
const katex = await loadKatex()
|
||||
katex.render(tex, el, { displayMode, throwOnError: false, output: 'htmlAndMathml' })
|
||||
@@ -20,19 +20,19 @@ function loadMermaid() {
|
||||
return mermaidLoad
|
||||
}
|
||||
|
||||
/** 渲染 Mermaid 图表,返回 SVG 标记;语法错误时抛出带说明的 Error */
|
||||
/** Render Mermaid to SVG markup; throw a descriptive Error for invalid syntax. */
|
||||
async function renderDiagram(code: string): Promise<string> {
|
||||
const mermaid = await loadMermaid()
|
||||
const mode = document.documentElement.dataset.rmColorMode
|
||||
const dark = mode === 'dark' || (mode === undefined && matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
// 配置与渲染串行进行,避免切换模式时并发图表读到另一个模式的全局配置。
|
||||
// Serialize configuration and rendering so concurrent diagrams cannot read another mode's global configuration during a mode switch.
|
||||
const config: MermaidConfig = { startOnLoad: false, securityLevel: 'strict', theme: dark ? 'dark' : 'default' }
|
||||
mermaid.initialize(config)
|
||||
const id = `rm-mermaid-${++mermaidSeq}`
|
||||
try {
|
||||
return (await mermaid.render(id, code)).svg
|
||||
} catch (err) {
|
||||
// 出错时 mermaid 会把临时节点留在 body 中
|
||||
// Mermaid leaves temporary nodes in body after errors.
|
||||
document.getElementById(`d${id}`)?.remove()
|
||||
throw err instanceof Error ? err : new Error(String(err))
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export function renderMermaid(code: string): Promise<string> {
|
||||
|
||||
let emojiLoad: Promise<Map<string, string>> | undefined
|
||||
|
||||
/** 按 GitHub/Typora 的名称查找 emoji,如 smile → 😄;未知名称返回 undefined */
|
||||
/** Look up emoji by GitHub/Typora names, such as smile -> smiling emoji; return undefined for unknown names. */
|
||||
export async function emojiFor(name: string): Promise<string | undefined> {
|
||||
emojiLoad ??= import('gemoji').then(({ gemoji }) => {
|
||||
const map = new Map<string, string>()
|
||||
@@ -59,7 +59,7 @@ export async function emojiFor(name: string): Promise<string | undefined> {
|
||||
return (await emojiLoad).get(name)
|
||||
}
|
||||
|
||||
/** 为渲染器输出的占位元素(表格单元格等处)填充公式与 emoji */
|
||||
/** Fill renderer placeholders, including table cells, with math and emoji. */
|
||||
export function hydrate(root: HTMLElement) {
|
||||
for (const el of root.querySelectorAll<HTMLElement>('[data-math]')) {
|
||||
void renderMath(el, el.dataset.math!, el.tagName === 'DIV')
|
||||
|
||||
Vendored
+1
-1
@@ -26,7 +26,7 @@ export interface HostModules {
|
||||
export interface PluginCommand {
|
||||
id: string
|
||||
title: string
|
||||
/** 保留声明兼容;默认不绑定,请用户在设置中添加快捷键。 */
|
||||
/** Retain declaration compatibility; do not bind by default. Users can add shortcuts in settings. */
|
||||
keys?: string[]
|
||||
when?: () => boolean
|
||||
run: () => Awaitable<void>
|
||||
|
||||
@@ -241,7 +241,7 @@ async function pump() {
|
||||
loading = true
|
||||
const currentSession = session
|
||||
try {
|
||||
// eslint-disable-next-line no-unmodified-loop-condition -- await 期间可由停用或刷新变更会话。
|
||||
// eslint-disable-next-line no-unmodified-loop-condition -- Disabling or refreshing may change the session during await.
|
||||
while (running && currentSession === session && !pluginState.error) {
|
||||
const next = pluginState.installed.find(p => p.enabled && !instances.has(p.id) && faults.get(p.id) !== p.revision)
|
||||
if (!next) break
|
||||
@@ -316,7 +316,7 @@ export function retryPlugin(id: string) {
|
||||
export function pluginEditorFailure(error: unknown) {
|
||||
const owners = new Set([...editorExtensions.values(), ...markdownSyntax.values()].map(entry => entry.owner))
|
||||
if (!owners.size) throw error
|
||||
// CodeMirror 的事务/解析错误无法可靠定位任意第三方扩展;先保留文档并撤回外部扩展。
|
||||
// CodeMirror transaction/parser errors cannot reliably identify arbitrary third-party extensions; preserve the document and remove external extensions first.
|
||||
queueMicrotask(() => {
|
||||
for (const owner of owners) {
|
||||
const instance = instances.get(owner)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AsyncCleanup, Awaitable, MountContext } from '@@/plugin-api'
|
||||
import { t as translate } from '@@/i18n/runtime'
|
||||
|
||||
// 每次挂载使用独立容器;超时或停用后的异步渲染只能修改已脱离页面的容器。
|
||||
// Each mount gets its own container; asynchronous rendering after timeout or disable can only modify a detached container.
|
||||
export function mountContent(
|
||||
parent: HTMLElement,
|
||||
mount: (context: MountContext) => Awaitable<void | AsyncCleanup>,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// 写入剪贴板;Clipboard API 只在 HTTPS/localhost 可用,局域网 HTTP 访问时退回 execCommand
|
||||
// Write to the clipboard; Clipboard API requires HTTPS/localhost, so LAN HTTP falls back to execCommand.
|
||||
export async function copyText(text: string): Promise<boolean> {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch {
|
||||
// 权限被拒时继续尝试旧方法
|
||||
// Try the legacy method if permission is denied.
|
||||
}
|
||||
}
|
||||
const ta = document.createElement('textarea')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t as translate } from '@@/i18n/runtime'
|
||||
// 命令注册表:命令面板列出的操作与全局快捷键都来自这里;插件也通过 registerCommand 添加命令
|
||||
// Command registry: command-palette actions and global shortcuts originate here; plugins also add commands through registerCommand.
|
||||
import { shallowReactive } from 'vue'
|
||||
import { pinia } from '@/pinia'
|
||||
import { useSettingsStore } from '@/pinia/stores/settings'
|
||||
@@ -12,16 +12,16 @@ export { formatKeys, matchKeys } from './keys'
|
||||
export interface Command {
|
||||
id: string
|
||||
title: string
|
||||
/** 快捷键,如 'Mod+Shift+F'、'F1';Mod 在 Mac 上为 Cmd,其他平台为 Ctrl。第一个用于显示 */
|
||||
/** Shortcuts such as 'Mod+Shift+F' or 'F1'; Mod is Cmd on Mac and Ctrl elsewhere. The first binding is displayed. */
|
||||
keys?: string[]
|
||||
/** 返回 false 时命令不可用:不在面板中列出,快捷键也不触发 */
|
||||
/** Returning false disables the command in both the palette and shortcut dispatch. */
|
||||
when?: () => boolean
|
||||
run: () => void
|
||||
}
|
||||
|
||||
const commands = shallowReactive(new Map<string, Command>())
|
||||
|
||||
/** 注册命令,返回注销函数;id 重复时后注册的覆盖先注册的 */
|
||||
/** Register a command and return its disposer; later registrations replace earlier ones with the same id. */
|
||||
export function registerCommand(cmd: Command): () => void {
|
||||
commands.set(cmd.id, cmd)
|
||||
return () => {
|
||||
@@ -29,11 +29,11 @@ export function registerCommand(cmd: Command): () => void {
|
||||
}
|
||||
}
|
||||
|
||||
/** 全部已注册的命令(含当前不可用的),用于快捷键一览 */
|
||||
/** All registered commands, including unavailable ones, for the shortcut overview. */
|
||||
export function allCommands(): Command[] {
|
||||
const all = [...commands.values()]
|
||||
const plugins = all.filter(command => command.id.startsWith('plugin:'))
|
||||
// 动态启停不能改变快捷键优先级;同一插件内仍保留注册顺序。
|
||||
// Dynamic enable/disable must not change shortcut priority; retain registration order within each plugin.
|
||||
plugins.sort((a, b) => {
|
||||
const left = a.id.split(':')[1]; const right = b.id.split(':')[1]
|
||||
return left < right ? -1 : left > right ? 1 : 0
|
||||
@@ -65,14 +65,14 @@ export function shortcutConflict(id: string, keys: string[]): Command | undefine
|
||||
return allCommands().find(command => command.id !== id && commandKeys(command).some(existing => keys.some(key => physicalKeys(existing) === physicalKeys(key))))
|
||||
}
|
||||
|
||||
/** 全局快捷键分发;编辑器等已处理的按键(defaultPrevented)和输入法组字中的按键不处理 */
|
||||
/** Dispatch global shortcuts, ignoring defaultPrevented keys already handled by editors and keys during IME composition. */
|
||||
export function handleShortcut(e: KeyboardEvent) {
|
||||
if (e.defaultPrevented || e.isComposing || e.getModifierState('AltGraph') || (e.target instanceof Element && e.target.closest('[data-shortcut-recorder]'))) return
|
||||
for (const cmd of availableCommands()) {
|
||||
if (cmd.id.startsWith('editor.') && !(e.target instanceof Element && e.target.closest('.cm-content'))) continue
|
||||
if (commandKeys(cmd).some(k => matchKeys(e, k))) {
|
||||
e.preventDefault()
|
||||
// 捕获阶段已执行命令,不再让编辑器或弹窗重复处理同一次按键。
|
||||
// The capture phase already ran the command; prevent editors or dialogs from handling the same key again.
|
||||
e.stopPropagation()
|
||||
cmd.run()
|
||||
return
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ElMessageBox } from 'element-plus'
|
||||
import { pinia } from '@/pinia'
|
||||
import { useUserStore } from '@/pinia/stores/user'
|
||||
|
||||
/** 确认等待期间换会话时,旧操作视为取消。 */
|
||||
/** Treat the old operation as canceled if the session changes while awaiting confirmation. */
|
||||
export async function confirmAction(message: string, title = translate('common.confirm_action_f8347f')): Promise<boolean> {
|
||||
const user = useUserStore(pinia)
|
||||
const version = user.getSessionVersion()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 线性图标(24×24 视框,描边绘制)
|
||||
// Outline icons (24x24 viewBox, stroke-based drawing).
|
||||
export const iconPaths = {
|
||||
plus: 'M12 5v14M5 12h14',
|
||||
eye: 'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { t as translate } from '@@/i18n/runtime'
|
||||
// 应用、编辑器和快捷键录入共用同一套按键表示。
|
||||
// The application, editor, and shortcut recorder share one key representation.
|
||||
export const isMac = /Mac|iPhone|iPad/.test(navigator.platform)
|
||||
const punctuation: Record<string, string> = { BracketLeft: '[', BracketRight: ']', Backquote: '`' }
|
||||
const modifiers = ['Mod', 'Ctrl', 'Meta', 'Alt', 'Shift']
|
||||
|
||||
function physicalCharacter(event: KeyboardEvent): string | undefined {
|
||||
if (/^Digit\d$/.test(event.code)) return event.code.slice(5)
|
||||
// macOS Option 会把 Q/O/U 等变成其他字符,快捷键仍按原物理字母匹配。
|
||||
// macOS Option transforms letters such as Q/O/U; shortcuts still match the original physical letter.
|
||||
if (isMac && event.altKey && /^Key[A-Z]$/.test(event.code)) return event.code.slice(3)
|
||||
return punctuation[event.code]
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export function matchKeys(event: KeyboardEvent, value: string): boolean {
|
||||
export function recordedKeys(event: KeyboardEvent): string | null {
|
||||
if (['Control', 'Meta', 'Alt', 'Shift', 'AltGraph', 'Dead', 'Process', 'Unidentified'].includes(event.key) || event.isComposing) return null
|
||||
const parts: string[] = []
|
||||
// 同时按下两种修饰键时保留物理组合,避免同步后 Mod 与另一键重合。
|
||||
// Preserve physical combinations when both modifiers are held so synchronized Mod bindings do not collapse onto the other key.
|
||||
if (event.ctrlKey) parts.push(isMac || event.metaKey ? 'Ctrl' : 'Mod')
|
||||
if (event.metaKey) parts.push(!isMac || event.ctrlKey ? 'Meta' : 'Mod')
|
||||
if (event.altKey) parts.push('Alt')
|
||||
|
||||
@@ -29,7 +29,7 @@ export function noteTitle(path: string): string {
|
||||
return basename(path).replace(/\.md$/, '')
|
||||
}
|
||||
|
||||
// 把笔记中的图片地址解析为可加载的 URL:绝对地址原样返回,相对地址基于笔记所在目录走 /api/v1/raw
|
||||
// Resolve note image sources to loadable URLs: preserve absolute URLs and resolve relative paths against the note folder through the raw-resource endpoint.
|
||||
export function resolveAssetUrl(notePath: string, src: string): string {
|
||||
if (/^[a-z][a-z\d+.-]*:/i.test(src) || src.startsWith('/') || src.startsWith('#')) return src
|
||||
const parts = dirname(notePath) ? dirname(notePath).split('/') : []
|
||||
@@ -42,7 +42,7 @@ export function resolveAssetUrl(notePath: string, src: string): string {
|
||||
try {
|
||||
decoded = decodeURIComponent(seg)
|
||||
} catch {
|
||||
// 非法的百分号序列按原样使用
|
||||
// Preserve invalid percent-escape sequences as written.
|
||||
}
|
||||
parts.push(decoded)
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export function resolveAssetUrl(notePath: string, src: string): string {
|
||||
return `/api/v1/raw/${encodePath(parts.join('/'))}`
|
||||
}
|
||||
|
||||
// 导航隐藏普通共享附件与笔记附件,不改变这些目录的读写契约。
|
||||
// Hide shared and note-specific attachment folders from navigation without changing their read/write contracts.
|
||||
export function hiddenAssetPath(path: string): boolean {
|
||||
return path.split('/').some(part => part === 'assets' || part.endsWith('.assets'))
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function installApp() {
|
||||
}
|
||||
|
||||
export async function registerPwa() {
|
||||
// 开发服务器不注册,避免外壳缓存干扰热更新。
|
||||
// Do not register on the development server, where shell caching would interfere with hot updates.
|
||||
if (!import.meta.env.PROD || !('serviceWorker' in navigator) || !window.isSecureContext) return
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' })
|
||||
@@ -40,7 +40,7 @@ export async function registerPwa() {
|
||||
trackInstalling()
|
||||
registration.addEventListener('updatefound', trackInstalling)
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
// 其他标签页激活新版时,本页也保留手动更新入口。
|
||||
// Keep the manual update entry point when another tab activates the new version.
|
||||
waitingWorker.value = navigator.serviceWorker.controller
|
||||
})
|
||||
const checkUpdate = () => {
|
||||
@@ -50,7 +50,7 @@ export async function registerPwa() {
|
||||
}
|
||||
document.addEventListener('visibilitychange', checkUpdate)
|
||||
window.addEventListener('online', checkUpdate)
|
||||
// 新旧页面报告入口哈希,确认所有页面均已更新后才回收旧资源。
|
||||
// Old and new pages report entry hashes; reclaim old resources only after every page updates.
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
if (event.data === 'SHELL_ENTRY') {
|
||||
const script = document.querySelector<HTMLScriptElement>('script[type="module"][src]')!
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function prepareResources() {
|
||||
pending = request
|
||||
void request.finally(() => {
|
||||
if (pending === request) pending = undefined
|
||||
}).catch(() => { /* 由等待初始化的调用方处理失败。 */ })
|
||||
}).catch(() => { /* The caller awaiting initialization handles failures. */ })
|
||||
}
|
||||
await pending
|
||||
user.assertSession(current)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isMac } from './keys'
|
||||
|
||||
// https://support.typora.io/Shortcut-Keys/;仅为已有功能提供默认值。
|
||||
// https://support.typora.io/Shortcut-Keys/; provide defaults only for existing actions.
|
||||
export function typoraKeys(id: string, mac = isMac): string[] {
|
||||
if (/^editor\.heading[1-6]$/.test(id)) return [`Mod+${id.at(-1)}`]
|
||||
const common: Record<string, string[]> = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 软键盘弹出时,iOS Safari 与新版 Android Chrome 只缩小可视区域、页面高度不变,
|
||||
// 编辑器底部(光标、键盘上方的工具条)会被键盘挡住。把应用高度设为可视区域的高度。
|
||||
// 双指缩放也会改变可视区域,缩放时不调整
|
||||
// When software keyboards open, iOS Safari and recent Android Chrome shrink the visual viewport without changing page height,
|
||||
// covering the editor bottom, cursor, and toolbar. Set application height to the visual viewport height.
|
||||
// Pinch zoom also changes the visual viewport; do not resize while zooming.
|
||||
export function trackVisualViewport(onShrink: () => void) {
|
||||
const vv = window.visualViewport
|
||||
if (!vv) return
|
||||
@@ -8,7 +8,7 @@ export function trackVisualViewport(onShrink: () => void) {
|
||||
const update = () => {
|
||||
const height = vv.scale > 1.01 ? null : vv.height
|
||||
document.documentElement.style.setProperty('--rm-app-height', height === null ? '100%' : `${height}px`)
|
||||
// iOS 会把页面整体上推以露出输入位置,应用高度已经适配,推回原位
|
||||
// iOS shifts the page to reveal the input; application height already accounts for the keyboard, so restore its position.
|
||||
if (height !== null && vv.offsetTop > 0) window.scrollTo(0, 0)
|
||||
if (height !== null && height < last) requestAnimationFrame(onShrink)
|
||||
last = vv.height
|
||||
|
||||
Reference in new issue
Block a user