Compare commits
74 Commits
docs
...
7a92935dc6
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a92935dc6 | |||
| 6e49516962 | |||
| b6c325198d | |||
| 532d30aa93 | |||
| aae86d8b4e | |||
| 0b91447b05 | |||
| 4b1fb765b0 | |||
|
|
aa5ce2b038 | ||
| 533f732c53 | |||
| 009274e4ad | |||
| 76f6c30b9d | |||
| 9ec22add55 | |||
| 272227e4e3 | |||
| ec8f8c1e2d | |||
| 1c14092068 | |||
| 00bdafc621 | |||
| 78422899e4 | |||
| c47f7de5b8 | |||
| 37aae9e03c | |||
| fa134d31d6 | |||
| d035dcd531 | |||
| c50bf452ca | |||
| dace5ce2b0 | |||
| ef145169aa | |||
| 7b746155f7 | |||
|
|
b289f4054d | ||
|
|
bdee1fdf84 | ||
| 541e4e96cf | |||
| 401eb3ab39 | |||
| d3eba96a29 | |||
| 81c02db00d | |||
|
|
9cb2ccbb4e | ||
| 8a10b8fe0f | |||
| 8fce8bdca4 | |||
| 1ab934cee9 | |||
| 6659ac6fad | |||
| 3a5ab1c614 | |||
| 1e07e1f833 | |||
| e1e91a3683 | |||
| c30d95a3e0 | |||
| 97f6fa843c | |||
| f43fc47539 | |||
|
|
c330de52fa | ||
| 67d35626cb | |||
| cc4c2189dc | |||
| d16905c0a3 | |||
| 4e611db349 | |||
| 7e9fc0ac3f | |||
| ff072d1a93 | |||
| a9c81c878e | |||
| 3660d13d7d | |||
| 281f53c049 | |||
| 71ca541f78 | |||
| 91f4f4afac | |||
| fc5639d7bd | |||
|
|
6668c11846 | ||
| 17f3351cea | |||
| dd3dd4ddb2 | |||
| 60d1494d45 | |||
| 1ef5350b3f | |||
| 3521e5787b | |||
| 8d9bcdad7e | |||
| ac086db1ed | |||
| 6dff0181d2 | |||
| ad24d3a140 | |||
| 4b0f39d747 | |||
| 096cc1da94 | |||
| 2d3200ad97 | |||
| 4e82e2f6f7 | |||
| 339ed53c2e | |||
| fc7c162e2f | |||
|
|
24f1549730 | ||
| 5584a46ca2 | |||
| 4471441d6f |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
3
.github/workflows/build-release.yml
vendored
@@ -28,6 +28,7 @@ env:
|
||||
jobs:
|
||||
# 准备构建配置
|
||||
prepare:
|
||||
permissions: {}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
@@ -84,6 +85,8 @@ jobs:
|
||||
fi
|
||||
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: prepare
|
||||
if: ${{ fromJson(needs.prepare.outputs.matrix).include[0] != null }}
|
||||
strategy:
|
||||
|
||||
109
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '29 8 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: c-cpp
|
||||
build-mode: none
|
||||
- language: go
|
||||
build-mode: autobuild
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
- language: python
|
||||
build-mode: none
|
||||
- language: rust
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- name: Run manual build steps
|
||||
if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -61,5 +61,3 @@ export class ServiceOptions {
|
||||
return new ServiceOptions($$parsedSource as Partial<ServiceOptions>);
|
||||
}
|
||||
}
|
||||
|
||||
export type Window = any;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
17
frontend/bindings/voidraft/internal/common/helper/models.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* CancelFunc 取消订阅函数
|
||||
* 调用此函数可以取消对配置的监听
|
||||
*/
|
||||
export type CancelFunc = any;
|
||||
|
||||
/**
|
||||
* ObserverCallback 观察者回调函数
|
||||
*/
|
||||
export type ObserverCallback = any;
|
||||
4
frontend/bindings/voidraft/internal/models/ent/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
370
frontend/bindings/voidraft/internal/models/ent/models.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as theme$0 from "./theme/models.js";
|
||||
|
||||
/**
|
||||
* Document is the model entity for the Document schema.
|
||||
*/
|
||||
export class Document {
|
||||
/**
|
||||
* ID of the ent.
|
||||
*/
|
||||
"id"?: number;
|
||||
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
*/
|
||||
"created_at": string;
|
||||
|
||||
/**
|
||||
* update time
|
||||
*/
|
||||
"updated_at": string;
|
||||
|
||||
/**
|
||||
* deleted at
|
||||
*/
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* document title
|
||||
*/
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* document content
|
||||
*/
|
||||
"content": string;
|
||||
|
||||
/**
|
||||
* document locked status
|
||||
*/
|
||||
"locked": boolean;
|
||||
|
||||
/** Creates a new Document instance. */
|
||||
constructor($$source: Partial<Document> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = "";
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
}
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("title" in $$source)) {
|
||||
this["title"] = "";
|
||||
}
|
||||
if (!("content" in $$source)) {
|
||||
this["content"] = "";
|
||||
}
|
||||
if (!("locked" in $$source)) {
|
||||
this["locked"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Document instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Document {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Document($$parsedSource as Partial<Document>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension is the model entity for the Extension schema.
|
||||
*/
|
||||
export class Extension {
|
||||
/**
|
||||
* ID of the ent.
|
||||
*/
|
||||
"id"?: number;
|
||||
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
*/
|
||||
"created_at": string;
|
||||
|
||||
/**
|
||||
* update time
|
||||
*/
|
||||
"updated_at": string;
|
||||
|
||||
/**
|
||||
* deleted at
|
||||
*/
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* extension name
|
||||
*/
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* extension enabled or not
|
||||
*/
|
||||
"enabled": boolean;
|
||||
|
||||
/**
|
||||
* extension config
|
||||
*/
|
||||
"config": { [_: string]: any };
|
||||
|
||||
/** Creates a new Extension instance. */
|
||||
constructor($$source: Partial<Extension> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = "";
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
}
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
if (!("config" in $$source)) {
|
||||
this["config"] = {};
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Extension instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Extension {
|
||||
const $$createField7_0 = $$createType0;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("config" in $$parsedSource) {
|
||||
$$parsedSource["config"] = $$createField7_0($$parsedSource["config"]);
|
||||
}
|
||||
return new Extension($$parsedSource as Partial<Extension>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyBinding is the model entity for the KeyBinding schema.
|
||||
*/
|
||||
export class KeyBinding {
|
||||
/**
|
||||
* ID of the ent.
|
||||
*/
|
||||
"id"?: number;
|
||||
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
*/
|
||||
"created_at": string;
|
||||
|
||||
/**
|
||||
* update time
|
||||
*/
|
||||
"updated_at": string;
|
||||
|
||||
/**
|
||||
* deleted at
|
||||
*/
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* command identifier
|
||||
*/
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* keybinding type: standard or emacs
|
||||
*/
|
||||
"type": string;
|
||||
|
||||
/**
|
||||
* universal keybinding (cross-platform)
|
||||
*/
|
||||
"key"?: string;
|
||||
|
||||
/**
|
||||
* macOS specific keybinding
|
||||
*/
|
||||
"macos"?: string;
|
||||
|
||||
/**
|
||||
* Windows specific keybinding
|
||||
*/
|
||||
"windows"?: string;
|
||||
|
||||
/**
|
||||
* Linux specific keybinding
|
||||
*/
|
||||
"linux"?: string;
|
||||
|
||||
/**
|
||||
* extension name (functional category)
|
||||
*/
|
||||
"extension": string;
|
||||
|
||||
/**
|
||||
* whether this keybinding is enabled
|
||||
*/
|
||||
"enabled": boolean;
|
||||
|
||||
/**
|
||||
* prevent browser default behavior
|
||||
*/
|
||||
"preventDefault": boolean;
|
||||
|
||||
/**
|
||||
* keybinding scope (default: editor)
|
||||
*/
|
||||
"scope"?: string;
|
||||
|
||||
/** Creates a new KeyBinding instance. */
|
||||
constructor($$source: Partial<KeyBinding> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = "";
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
}
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = "";
|
||||
}
|
||||
if (!("extension" in $$source)) {
|
||||
this["extension"] = "";
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
if (!("preventDefault" in $$source)) {
|
||||
this["preventDefault"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new KeyBinding instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): KeyBinding {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new KeyBinding($$parsedSource as Partial<KeyBinding>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme is the model entity for the Theme schema.
|
||||
*/
|
||||
export class Theme {
|
||||
/**
|
||||
* ID of the ent.
|
||||
*/
|
||||
"id"?: number;
|
||||
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
*/
|
||||
"created_at": string;
|
||||
|
||||
/**
|
||||
* update time
|
||||
*/
|
||||
"updated_at": string;
|
||||
|
||||
/**
|
||||
* deleted at
|
||||
*/
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* theme name
|
||||
*/
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* theme type
|
||||
*/
|
||||
"type": theme$0.Type;
|
||||
|
||||
/**
|
||||
* theme colors
|
||||
*/
|
||||
"colors": { [_: string]: any };
|
||||
|
||||
/** Creates a new Theme instance. */
|
||||
constructor($$source: Partial<Theme> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = "";
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
}
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = ("" as theme$0.Type);
|
||||
}
|
||||
if (!("colors" in $$source)) {
|
||||
this["colors"] = {};
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Theme instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Theme {
|
||||
const $$createField7_0 = $$createType0;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("colors" in $$parsedSource) {
|
||||
$$parsedSource["colors"] = $$createField7_0($$parsedSource["colors"]);
|
||||
}
|
||||
return new Theme($$parsedSource as Partial<Theme>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
||||
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
@@ -0,0 +1,22 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* Type defines the type for the "type" enum field.
|
||||
*/
|
||||
export enum Type {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Type values.
|
||||
*/
|
||||
TypeDark = "dark",
|
||||
TypeLight = "light",
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* BackupService 提供基于Git的备份功能
|
||||
* BackupService 提供基于Git的备份同步功能
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* HandleConfigChange 处理备份配置变更
|
||||
* HandleConfigChange 处理配置变更
|
||||
*/
|
||||
export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(395287784, config) as any;
|
||||
@@ -34,15 +34,7 @@ export function Initialize(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* PushToRemote 推送本地更改到远程仓库
|
||||
*/
|
||||
export function PushToRemote(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(262644139) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize 重新初始化备份服务,用于响应配置变更
|
||||
* Reinitialize 重新初始化
|
||||
*/
|
||||
export function Reinitialize(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(301562543) as any;
|
||||
@@ -50,7 +42,7 @@ export function Reinitialize(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 服务关闭时的清理工作
|
||||
* ServiceShutdown 服务关闭
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(422131801) as any;
|
||||
@@ -63,7 +55,7 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
}
|
||||
|
||||
/**
|
||||
* StartAutoBackup 启动自动备份定时器
|
||||
* StartAutoBackup 启动自动备份
|
||||
*/
|
||||
export function StartAutoBackup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3035755449) as any;
|
||||
@@ -77,3 +69,11 @@ export function StopAutoBackup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2641894021) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync 执行完整的同步流程:导出 -> commit -> pull -> 解决冲突 -> push -> 导入
|
||||
*/
|
||||
export function Sync(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3420383211) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
import * as helper$0 from "../common/helper/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* Get 获取配置项
|
||||
@@ -50,7 +49,7 @@ export function MigrateConfig(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetConfig 强制重置所有配置为默认值
|
||||
* ResetConfig 重置所有配置为默认值
|
||||
*/
|
||||
export function ResetConfig(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3593047389) as any;
|
||||
@@ -66,7 +65,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
* ServiceStartup 服务启动时初始化
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3311949428, options) as any;
|
||||
@@ -84,7 +83,7 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
|
||||
/**
|
||||
* Watch 注册配置变更监听器
|
||||
*/
|
||||
export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } {
|
||||
export function Watch(path: string, callback: helper$0.ObserverCallback): Promise<helper$0.CancelFunc> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
@@ -92,7 +91,7 @@ export function Watch(path: string, callback: $models.ObserverCallback): Promise
|
||||
/**
|
||||
* WatchWithContext 使用 Context 注册监听器
|
||||
*/
|
||||
export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise<void> & { cancel(): void } {
|
||||
export function WatchWithContext(path: string, callback: helper$0.ObserverCallback): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1454973098, path, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* DatabaseService provides shared database functionality
|
||||
* DatabaseService 数据库服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -15,15 +15,7 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* RegisterModel 注册模型与表的映射关系
|
||||
*/
|
||||
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown shuts down the service when the application closes
|
||||
* ServiceShutdown 服务关闭
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3907893632) as any;
|
||||
@@ -31,7 +23,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2067840771, options) as any;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* DocumentService provides document management functionality
|
||||
* DocumentService 文档服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -15,12 +15,12 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* CreateDocument creates a new document and returns the created document with ID
|
||||
* CreateDocument 创建文档
|
||||
*/
|
||||
export function CreateDocument(title: string): Promise<models$0.Document | null> & { cancel(): void } {
|
||||
export function CreateDocument(title: string): Promise<ent$0.Document | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3360680842, title) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
@@ -30,7 +30,7 @@ export function CreateDocument(title: string): Promise<models$0.Document | null>
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
|
||||
* DeleteDocument 删除文档
|
||||
*/
|
||||
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(412287269, id) as any;
|
||||
@@ -38,9 +38,9 @@ export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDocumentByID gets a document by ID
|
||||
* GetDocumentByID 根据ID获取文档
|
||||
*/
|
||||
export function GetDocumentByID(id: number): Promise<models$0.Document | null> & { cancel(): void } {
|
||||
export function GetDocumentByID(id: number): Promise<ent$0.Document | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3468193232, id) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
@@ -50,9 +50,9 @@ export function GetDocumentByID(id: number): Promise<models$0.Document | null> &
|
||||
}
|
||||
|
||||
/**
|
||||
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
|
||||
* ListAllDocumentsMeta 列出所有文档
|
||||
*/
|
||||
export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
|
||||
export function ListAllDocumentsMeta(): Promise<(ent$0.Document | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3073950297) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
@@ -62,19 +62,7 @@ export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> &
|
||||
}
|
||||
|
||||
/**
|
||||
* ListDeletedDocumentsMeta lists all deleted document metadata
|
||||
*/
|
||||
export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(490143787) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* LockDocument 锁定文档,防止删除
|
||||
* LockDocument 锁定文档
|
||||
*/
|
||||
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1889494473, id) as any;
|
||||
@@ -82,15 +70,7 @@ export function LockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* RestoreDocument restores a deleted document
|
||||
*/
|
||||
export function RestoreDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(784200778, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1474135487, options) as any;
|
||||
@@ -106,7 +86,7 @@ export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateDocumentContent updates the content of a document
|
||||
* UpdateDocumentContent 更新文档内容
|
||||
*/
|
||||
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
|
||||
@@ -114,7 +94,7 @@ export function UpdateDocumentContent(id: number, content: string): Promise<void
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateDocumentTitle updates the title of a document
|
||||
* UpdateDocumentTitle 更新文档标题
|
||||
*/
|
||||
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
|
||||
@@ -122,6 +102,6 @@ export function UpdateDocumentTitle(id: number, title: string): Promise<void> &
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Document.createFrom;
|
||||
const $$createType0 = ent$0.Document.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* ExtensionService 扩展管理服务
|
||||
* ExtensionService 扩展服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -16,12 +16,15 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetAllExtensions 获取所有扩展配置
|
||||
* GetDefaultExtensions 获取默认扩展配置(用于前端绑定生成)
|
||||
*/
|
||||
export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3094292124) as any;
|
||||
export function GetDefaultExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4036328166) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
@@ -30,23 +33,51 @@ export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel():
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetAllExtensionsToDefault 重置所有扩展到默认状态
|
||||
* GetExtensionByID 根据ID获取扩展
|
||||
*/
|
||||
export function ResetAllExtensionsToDefault(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(270611949) as any;
|
||||
export function GetExtensionByID(id: number): Promise<ent$0.Extension | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1521424252, id) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType3($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetExtensionConfig 获取扩展配置
|
||||
*/
|
||||
export function GetExtensionConfig(id: number): Promise<{ [_: string]: any }> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1629559882, id) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType4($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetExtensions 获取所有扩展
|
||||
*/
|
||||
export function GetExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3179289021) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType5($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetExtensionConfig 重置单个扩展到默认状态
|
||||
*/
|
||||
export function ResetExtensionConfig(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3990780299, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetExtensionToDefault 重置扩展到默认状态
|
||||
*/
|
||||
export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(868308101, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 启动时调用
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(40324057, options) as any;
|
||||
@@ -54,21 +85,33 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateExtensionEnabled 更新扩展启用状态
|
||||
* SyncExtensions 同步扩展配置
|
||||
*/
|
||||
export function UpdateExtensionEnabled(id: models$0.ExtensionID, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
|
||||
export function SyncExtensions(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(167560004) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateExtensionState 更新扩展状态
|
||||
* UpdateExtensionConfig 更新扩展配置
|
||||
*/
|
||||
export function UpdateExtensionState(id: models$0.ExtensionID, enabled: boolean, config: models$0.ExtensionConfig): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2917946454, id, enabled, config) as any;
|
||||
export function UpdateExtensionConfig(id: number, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3184142503, id, config) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateExtensionEnabled 更新扩展启用状态
|
||||
*/
|
||||
export function UpdateExtensionEnabled(id: number, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Extension.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType2 = ent$0.Extension.createFrom;
|
||||
const $$createType3 = $Create.Nullable($$createType2);
|
||||
const $$createType4 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType5 = $Create.Array($$createType3);
|
||||
|
||||
@@ -18,12 +18,12 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* GetCurrentHotkey 获取当前热键
|
||||
* GetSupportedKeys 返回系统支持的快捷键列表
|
||||
*/
|
||||
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2572811187) as any;
|
||||
export function GetSupportedKeys(): Promise<string[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1511528650) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
@@ -86,5 +86,4 @@ export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.HotkeyCombo.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
|
||||
@@ -17,7 +17,6 @@ import * as SystemService from "./systemservice.js";
|
||||
import * as TestService from "./testservice.js";
|
||||
import * as ThemeService from "./themeservice.js";
|
||||
import * as TranslationService from "./translationservice.js";
|
||||
import * as TrayService from "./trayservice.js";
|
||||
import * as WindowService from "./windowservice.js";
|
||||
export {
|
||||
BackupService,
|
||||
@@ -36,7 +35,6 @@ export {
|
||||
TestService,
|
||||
ThemeService,
|
||||
TranslationService,
|
||||
TrayService,
|
||||
WindowService
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* KeyBindingService 快捷键管理服务
|
||||
* KeyBindingService 快捷键服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -16,12 +16,15 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetAllKeyBindings 获取所有快捷键配置
|
||||
* GetDefaultKeyBindings 获取默认快捷键配置
|
||||
*/
|
||||
export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1633502882) as any;
|
||||
export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3843471588) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
@@ -30,13 +33,80 @@ export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 启动时调用
|
||||
* GetKeyBindingByID 根据ID获取快捷键
|
||||
*/
|
||||
export function GetKeyBindingByID(id: number): Promise<ent$0.KeyBinding | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1578192526, id) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType3($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetKeyBindings 根据类型获取快捷键
|
||||
*/
|
||||
export function GetKeyBindings(kbType: models$0.KeyBindingType): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4253885163, kbType) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType4($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetKeyBindings 重置所有快捷键到默认值
|
||||
*/
|
||||
export function ResetKeyBindings(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4251626010) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2057121990, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncKeyBindings 同步快捷键配置
|
||||
*/
|
||||
export function SyncKeyBindings(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1522202638) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateKeyBindingEnabled 更新快捷键启用状态
|
||||
*/
|
||||
export function UpdateKeyBindingEnabled(id: number, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(843626124, id, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateKeyBindingKeys 更新快捷键绑定
|
||||
*/
|
||||
export function UpdateKeyBindingKeys(id: number, key: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3432755175, id, key) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateKeyBindingPreventDefault 更新快捷键 PreventDefault 状态
|
||||
*/
|
||||
export function UpdateKeyBindingPreventDefault(id: number, preventDefault: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(202386744, id, preventDefault) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.KeyBinding.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType2 = ent$0.KeyBinding.createFrom;
|
||||
const $$createType3 = $Create.Nullable($$createType2);
|
||||
const $$createType4 = $Create.Array($$createType3);
|
||||
|
||||
@@ -12,12 +12,6 @@ import * as http$0 from "../../../net/http/models.js";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../../../time/models.js";
|
||||
|
||||
/**
|
||||
* CancelFunc 取消订阅函数
|
||||
* 调用此函数可以取消对配置的监听
|
||||
*/
|
||||
export type CancelFunc = any;
|
||||
|
||||
/**
|
||||
* HttpRequest HTTP请求结构
|
||||
*/
|
||||
@@ -191,15 +185,14 @@ export class MemoryStats {
|
||||
* MigrationProgress 迁移进度信息
|
||||
*/
|
||||
export class MigrationProgress {
|
||||
"status": MigrationStatus;
|
||||
/**
|
||||
* 0-100
|
||||
*/
|
||||
"progress": number;
|
||||
"error"?: string;
|
||||
|
||||
/** Creates a new MigrationProgress instance. */
|
||||
constructor($$source: Partial<MigrationProgress> = {}) {
|
||||
if (!("status" in $$source)) {
|
||||
this["status"] = ("" as MigrationStatus);
|
||||
}
|
||||
if (!("progress" in $$source)) {
|
||||
this["progress"] = 0;
|
||||
}
|
||||
@@ -216,20 +209,6 @@ export class MigrationProgress {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MigrationStatus 迁移状态
|
||||
*/
|
||||
export enum MigrationStatus {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
MigrationStatusMigrating = "migrating",
|
||||
MigrationStatusCompleted = "completed",
|
||||
MigrationStatusFailed = "failed",
|
||||
};
|
||||
|
||||
/**
|
||||
* OSInfo 操作系统信息
|
||||
*/
|
||||
@@ -266,11 +245,6 @@ export class OSInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ObserverCallback 观察者回调函数
|
||||
*/
|
||||
export type ObserverCallback = any;
|
||||
|
||||
/**
|
||||
* SelfUpdateResult 自我更新结果
|
||||
*/
|
||||
@@ -311,7 +285,7 @@ export class SelfUpdateResult {
|
||||
"error": string;
|
||||
|
||||
/**
|
||||
* 更新源(github/gitea)
|
||||
* 更新源(github)
|
||||
*/
|
||||
"source": string;
|
||||
|
||||
|
||||
@@ -15,26 +15,13 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetAllThemes 获取所有主题
|
||||
* GetThemeByName 根据Key获取主题
|
||||
*/
|
||||
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2425053076) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetThemeByID 根据ID或名称获取主题
|
||||
* 如果 id > 0,按ID查询;如果 id = 0,按名称查询
|
||||
*/
|
||||
export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<models$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(127385338, id, name) as any;
|
||||
export function GetThemeByName(name: string): Promise<ent$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1938954770, name) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
@@ -43,23 +30,15 @@ export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<model
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetTheme 重置主题为预设配置
|
||||
* ResetTheme 删除主题
|
||||
*/
|
||||
export function ResetTheme(id: number, ...name: string[]): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1806334457, id, name) as any;
|
||||
export function ResetTheme(key: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1806334457, key) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 服务关闭
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1676749034) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 服务启动时初始化
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2915959937, options) as any;
|
||||
@@ -67,14 +46,13 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateTheme 更新主题
|
||||
* UpdateTheme 保存或更新主题
|
||||
*/
|
||||
export function UpdateTheme(id: number, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(70189749, id, colors) as any;
|
||||
export function UpdateTheme(key: string, colors: { [_: string]: any }): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(70189749, key, colors) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Theme.createFrom;
|
||||
const $$createType0 = ent$0.Theme.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* TrayService 系统托盘服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* AutoShowHide 自动显示/隐藏主窗口
|
||||
*/
|
||||
export function AutoShowHide(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4044219428) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* HandleWindowClose 处理窗口关闭事件
|
||||
*/
|
||||
export function HandleWindowClose(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1824247204) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* HandleWindowMinimize 处理窗口最小化事件
|
||||
*/
|
||||
export function HandleWindowMinimize(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(178686624) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* MinimizeButtonClicked 处理标题栏最小化按钮点击
|
||||
*/
|
||||
export function MinimizeButtonClicked(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2477618539) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShouldMinimizeToTray 检查是否应该最小化到托盘
|
||||
*/
|
||||
export function ShouldMinimizeToTray(): Promise<boolean> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3403884012) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShowWindow 显示主窗口
|
||||
*/
|
||||
export function ShowWindow(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1315913255) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
@@ -14,18 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* GetOpenWindows 获取所有打开的文档窗口
|
||||
*/
|
||||
export function GetOpenWindows(): Promise<application$0.Window[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1464997251) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开
|
||||
*/
|
||||
@@ -57,6 +45,3 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
let $resultPromise = $Call.ByID(2432987694, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
|
||||
4
frontend/components.d.ts
vendored
@@ -11,6 +11,8 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AccordionContainer: typeof import('./src/components/accordion/AccordionContainer.vue')['default']
|
||||
AccordionItem: typeof import('./src/components/accordion/AccordionItem.vue')['default']
|
||||
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
|
||||
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
|
||||
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
|
||||
@@ -22,6 +24,8 @@ declare module 'vue' {
|
||||
TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default']
|
||||
TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default']
|
||||
TabItem: typeof import('./src/components/tabs/TabItem.vue')['default']
|
||||
Toast: typeof import('./src/components/toast/Toast.vue')['default']
|
||||
ToastContainer: typeof import('./src/components/toast/ToastContainer.vue')['default']
|
||||
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
|
||||
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
|
||||
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']
|
||||
|
||||
@@ -93,32 +93,13 @@ export default defineConfig({
|
||||
items: [
|
||||
{text: '简介', link: '/zh/guide/introduction'},
|
||||
{text: '安装', link: '/zh/guide/installation'},
|
||||
{text: '快速开始', link: '/zh/guide/getting-started'},
|
||||
{text: '界面总览', link: '/zh/guide/ui-overview'},
|
||||
{text: '块语法与结构', link: '/zh/guide/block-syntax'}
|
||||
{text: '快速开始', link: '/zh/guide/getting-started'}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '编辑与效率',
|
||||
text: '功能特性',
|
||||
items: [
|
||||
{text: '键盘快捷键', link: '/zh/guide/keyboard-shortcuts'},
|
||||
{text: '多窗口与标签页', link: '/zh/guide/multiwindow-tabs'},
|
||||
{text: '扩展与插件', link: '/zh/guide/extensions'},
|
||||
{text: 'HTTP 客户端', link: '/zh/guide/http-client'}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '个性化与数据',
|
||||
items: [
|
||||
{text: '设置与配置', link: '/zh/guide/settings'},
|
||||
{text: '主题与外观', link: '/zh/guide/themes'},
|
||||
{text: '备份与更新', link: '/zh/guide/backup-update'}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '问题处理',
|
||||
items: [
|
||||
{text: '常见问题与故障排查', link: '/zh/guide/troubleshooting'}
|
||||
{text: '功能概览', link: '/zh/guide/features'}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,60 +0,0 @@
|
||||
# 备份与更新
|
||||
|
||||

|
||||
> 替换为备份设置、推送状态、更新提示的截图。
|
||||
|
||||
## Git 备份
|
||||
`BackupService` 将 `dataPath` 转化为 Git 仓库,并提供自动/手动推送。
|
||||
|
||||
### 初始化
|
||||
1. 在设置 > 备份中开启「启用备份」。
|
||||
2. 填写远程仓库 URL(HTTPS 或 SSH)。
|
||||
3. 选择认证方式:
|
||||
- **Token**:适用于 GitHub/Gitea https 仓库。
|
||||
- **SSH Key**:指定私钥路径和 passphrase。
|
||||
- **用户名/密码**:适合自建 HTTP 仓库。
|
||||
4. 点击“测试连接”(按钮在计划中,可先在终端测试)。
|
||||
|
||||
### 自动备份
|
||||
- 勾选 “自动备份” + 设置 `BackupInterval`(分钟)。
|
||||
- 服务会创建 ticker 定时 `git add -> commit -> push`。
|
||||
- Commit 消息形如 `Auto backup <timestamp>`,包含 `voidraft.db`, `extensions.json`, `config.json`, `voidraft_data.bin`。
|
||||
|
||||
### 手动推送
|
||||
- 打开工具栏消息中心或设置页点击“立即推送”。
|
||||
- `backupStore.pushToRemote` 会显示状态气泡(成功/失败提示 3~5 秒)。
|
||||
|
||||
### 常见问题
|
||||
| 提示 | 解决 |
|
||||
| --- | --- |
|
||||
| `repository not found` | 检查 Repo URL 与权限,必要时创建空仓库 |
|
||||
| `authentication required` | 选择正确认证方式,确认 token scope(需 repo 权限) |
|
||||
| `auto backup stopped` | 查看日志,可能是网络不通或凭据失效;修改配置后服务会自动重启 |
|
||||
|
||||
## 自动更新
|
||||
`SelfUpdateService` 负责检测、下载、应用新版。
|
||||
|
||||
### 检查更新
|
||||
- 启动时若勾选 “自动更新” 会自动检查。
|
||||
- 也可在设置 > 更新点击 “检查更新” 或在工具栏更新图标处触发。
|
||||
- 服务优先访问 `primarySource`,失败时回退 `backupSource`。
|
||||
|
||||
### 下载与应用
|
||||
1. 检测到更新后,界面提示版本号与变更信息(从 Release Notes 获取)。
|
||||
2. 点击 “下载并安装” 后,后台执行下载,完成后提示“准备重启”。
|
||||
3. 选择 “立即重启” 将调用 `RestartApplication`,自动重新打开上次的文档。
|
||||
4. 若启用了 “更新前备份”,在下载前会触发一次 Git push。
|
||||
|
||||
### 失败处理
|
||||
| 场景 | 建议 |
|
||||
| --- | --- |
|
||||
| 下载失败 | 检查网络/代理,切换至备用源 |
|
||||
| 校验失败 | 删除 `%LOCALAPPDATA%/voidraft/update-cache` 再重试 |
|
||||
| 应用后无法启动 | 从 Git 备份回滚数据,下载旧版本安装包覆盖 |
|
||||
|
||||
## 发布渠道
|
||||
- 官方 GitHub Releases:`https://github.com/landaiqing/voidraft/releases`
|
||||
- 自建 Gitea:`https://git.landaiqing.cn/landaiqing/voidraft`
|
||||
- 可在设置中替换 Owner/Repo/BaseURL,以指向企业私有镜像。
|
||||
|
||||
> 建议将备份仓库设为私有,并在更新前后验证数据完整性。若要接入 S3/OSS 等备份方式,可关注 roadmap 或自行扩展。
|
||||
@@ -1,78 +0,0 @@
|
||||
# 块语法与结构
|
||||
|
||||

|
||||
> 替换为展示分隔符(`∞∞∞language`)、块内容、语言标签的截图。
|
||||
|
||||
## 结构定义
|
||||
每个块都由 **分隔符 + 内容** 组成:
|
||||
|
||||
```
|
||||
∞∞∞language[-a]\n
|
||||
<内容>
|
||||
```
|
||||
|
||||
- `language`:`lang-parser/languages.ts` 中的 token,例如 `text`、`javascript`、`python`、`md`、`http`、`math`、`sql`。
|
||||
- `-a`:可选自动检测后缀,表示忽略显式语言,由 `lang-detect/autodetect.ts` 根据内容猜测。
|
||||
- 内容允许空行;块之间无需额外空格。
|
||||
|
||||
`parser.ts` 会将块解析为:
|
||||
```ts
|
||||
{
|
||||
language: { name: "javascript", auto: false },
|
||||
delimiter: { from, to },
|
||||
content: { from, to },
|
||||
range: { from, to }
|
||||
}
|
||||
```
|
||||
这些字段供格式化、HTTP 运行、块操作等扩展示例使用。
|
||||
|
||||
## 快捷命令
|
||||
| 命令 | 默认快捷键 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| blockAddAfterCurrent | `Ctrl+Enter` | 插入新块(下方) |
|
||||
| blockAddBeforeCurrent | `Ctrl+Shift+Enter` | 插入新块(上方) |
|
||||
| blockGotoPrevious/Next | `Alt+↑ / Alt+↓` | 在块之间跳转 |
|
||||
| blockSelectAll | `Ctrl+Shift+A` | 选中当前块(含分隔符) |
|
||||
| blockDelete | `Alt+Delete` | 删除整个块 |
|
||||
| blockMoveUp/Down | `Ctrl+Shift+↑ / Ctrl+Shift+↓` | 重排块顺序 |
|
||||
| blockFormat | `Ctrl+Shift+F` | 针对当前块执行 Prettier |
|
||||
|
||||
## 语言与能力矩阵
|
||||
| 语言 | 适配特性 |
|
||||
| --- | --- |
|
||||
| `text`/`note` | 基础文本,没有特殊扩展 |
|
||||
| `md` | Markdown 预览、checkbox、高亮 |
|
||||
| `javascript`/`typescript`/`json` | Prettier 格式化、彩虹括号、折叠、颜色选择 |
|
||||
| `go`/`rust`/`python`/`java` | 高亮、折叠、自动缩进、语法跳转 |
|
||||
| `http` | HTTP DSL、变量、响应插入(详见 [HTTP 客户端](/zh/guide/http-client))|
|
||||
| `math` | `mathBlock` 运行器,支持 `prev` 引用上一次结果 |
|
||||
| `sql`/`yaml`/`toml` | 语法高亮、格式化(由 Prettier/插件支持) |
|
||||
|
||||
> 若需要不在列表中的语言,可先使用 `text` 块输入,再在工具栏搜索语言名;也可以在 `lang-parser/languages.ts` 中添加条目。
|
||||
|
||||
## 自动检测策略
|
||||
- 当分隔符以 `∞∞∞text-a` 写成时,`AUTO_DETECT_SUFFIX` 生效,`lang-detect` 会基于内容统计 + Levenshtein 距离预测语言。
|
||||
- 自动结果会写入块状态,但不会覆盖分隔符原文,因此可通过工具栏明确指定。
|
||||
|
||||
## 特殊块
|
||||
### HTTP 块
|
||||
- 语法位于 `extensions/httpclient/language`,支持 `@var/@json/@form/@multipart` 等指令。
|
||||
- 运行器会在块尾生成 `### Response`,包含状态码、耗时、headers、body。
|
||||
|
||||
### 数学块
|
||||
- 语言设为 `math`,逐行计算。
|
||||
- `prev` 变量表示上一行结果,可完成链式运算。
|
||||
- 结果挂件可点击复制,或显示格式化/定点值。
|
||||
|
||||
### Markdown 块
|
||||
- 工具栏中的 Preview 按钮会调用 `toggleMarkdownPreview`,右侧打开面板。
|
||||
- 预览状态按文档隔离,不会影响其他文档。
|
||||
|
||||
## 分隔符校验
|
||||
- `parser.ts` 暴露 `isValidDelimiter` 方法,格式错误时分隔符会以红色底纹标记。
|
||||
- 复制/剪切操作会自动扩选到整个块,确保分隔符完整。
|
||||
|
||||
## 维护建议
|
||||
- 保持每个逻辑主题占用一个块,并用 `md` 块写标题。
|
||||
- 大量多语言内容时,可用 `text` + 自动检测,待语言确定后再改分隔符。
|
||||
- 若文档出现“无法解析块”提示,可运行 `格式化文档` 或在命令面板触发“重建语法树”。
|
||||
@@ -1,42 +0,0 @@
|
||||
# 扩展与插件
|
||||
|
||||

|
||||
> 替换为展示扩展设置面板或功能合集(小地图、搜索、翻译)的截图。
|
||||
|
||||
voidraft 的扩展系统由 `internal/models/extensions.go` + 前端 `ExtensionManager` 驱动。扩展配置存储在 `%USERPROFILE%/.voidraft/data/extensions.json`,可在设置页面勾选启用或调整参数。
|
||||
|
||||
## 核心扩展
|
||||
| 扩展 ID | 功能 | 关键文件 |
|
||||
| --- | --- | --- |
|
||||
| `editor` | 基础 CodeMirror 行为、光标保护、滚轮缩放 | `frontend/src/views/editor/basic/*` |
|
||||
| `codeblock` | 块解析、拖拽、复制、格式化、数学、HTTP DSL | `extensions/codeblock` |
|
||||
| `vscodeSearch` | VSCode 风格搜索替换面板 | `extensions/vscodeSearch` |
|
||||
| `markdownPreview` | Markdown 实时预览 | `extensions/markdownPreview` |
|
||||
|
||||
## 编辑增强
|
||||
- **Rainbow Brackets (`rainbowBrackets`)**:彩虹色括号匹配。
|
||||
- **Fold (`fold`)**:代码折叠/展开,支持 `Ctrl+Alt+[`/`]`。
|
||||
- **Hyperlink (`hyperlink`)**:识别 URL/邮箱,`Ctrl+Click` 打开。
|
||||
- **Color Selector (`colorSelector`)**:悬浮配色器,支持 HEX/RGB/HSL。
|
||||
- **Checkbox (`checkbox`)**:Markdown 任务列表交互式勾选。
|
||||
- **Text Highlight (`textHighlight`)**:`Mod+Shift+H` 快速标记重点,可自定义颜色/透明度。
|
||||
|
||||
## 工具扩展
|
||||
- **Translator (`translator`)**:选区翻译;配置项包括默认翻译器、最短/最长字符数。后端集成 Bing/Google/Youdao/DeepL/TartuNLP。
|
||||
- **Minimap (`minimap`)**:右侧迷你地图,支持悬浮/常驻、显示字符/块,突出当前选区。
|
||||
- **Search (`search`)**:补充 VSCode 风格搜索,暴露命令给快捷键系统。
|
||||
- **HTTP Client (`httpclient`)**:DSL + 运行器,详见 [HTTP 客户端](/zh/guide/http-client)。
|
||||
|
||||
## 未来扩展(欢迎参与)
|
||||
- Vim / Emacs 键位层(正在计划)。
|
||||
- 自定义命令面板(Command Palette)。
|
||||
- 代码片段/模板库扩展。
|
||||
- AI 助手(文生代码/注释)。
|
||||
|
||||
## 开发者指南概述
|
||||
1. **注册扩展**:在 `extensionManager.registerFactory` 中添加自定义扩展工厂。
|
||||
2. **配置项**:在 `extensions.json` 中声明默认配置,并在设置面板暴露 UI(Vue 组件)。
|
||||
3. **热更新**:调用 `manager.updateExtensionImmediate(id, enabled, config)` 实时切换,无需刷新窗口。
|
||||
4. **后端交互**:通过 `ExtensionService.UpdateExtensionState` 将配置写入 SQLite。
|
||||
|
||||
> 如果需要编写自用扩展,可 fork 项目在 `frontend/src/views/editor/extensions` 中添加文件,再通过 PR 贡献给社区。
|
||||
@@ -1,86 +1,163 @@
|
||||
# 功能特性
|
||||
|
||||

|
||||
> 替换为展示彩虹括号、小地图、搜索工具条等扩展组合的截图。
|
||||
探索 voidraft 的强大功能,让它成为开发者的优秀工具。
|
||||
|
||||
## 1. 编辑体验
|
||||
### 块状编辑全流程
|
||||
- `∞∞∞language[-a]` 语法由 `codeblock/lang-parser` 解析,支持自动检测、分隔符校验、块范围缓存。
|
||||
- `blockState` 暴露 API(`getActiveBlock/getFirstBlock/getLastBlock`),供格式化、重排、复制、HTTP 执行等插件共享。
|
||||
- `mathBlock` 可在块尾展示计算结果,点击可复制;`CURRENCIES_LOADED` 注解在汇率更新时刷新缓存。
|
||||
## 块状编辑
|
||||
|
||||
### 语言支持
|
||||
- 内建 30+ 语言模版(`lang-parser/languages.ts`),覆盖 JS/TS/HTML/CSS/Go/Rust/Python/SQL/YAML/HTTP/Markdown/Plain/Text/Math。
|
||||
- 语言切换下拉实时更新分隔符;支持自定义别名(例如 `∞∞∞shell`)。
|
||||
voidraft 的核心功能是其块状编辑系统:
|
||||
|
||||
### 语法高亮与主题
|
||||
- `rainbowBracket`、`fold`、`hyperlink`、`colorSelector` 等扩展组合提供接近 VSCode 的体验。
|
||||
- `ThemeService` 预置 12+ 暗/亮主题,可在设置中克隆、修改 JSON 色板,并立即生效。
|
||||
- 每个块可以有不同的编程语言
|
||||
- 块之间由分隔符分隔(`∞∞∞语言`)
|
||||
- 快速在块之间导航
|
||||
- 独立格式化每个块
|
||||
|
||||
### 文本统计与滚轮缩放
|
||||
- `statsExtension` 实时统计行数、字符数和选区,展示在状态栏。
|
||||
- `wheelZoomExtension` 让 `Ctrl + 鼠标滚轮` 调整字体大小,同时同步 `configStore`。
|
||||
## 语法高亮
|
||||
|
||||
## 2. 高效工具箱
|
||||
### VSCode 式搜索替换
|
||||
- `extensions/vscodeSearch` 提供悬浮面板,支持大小写/整词/正则、向上/向下跳转、批量替换。
|
||||
- 对应快捷键:`Ctrl+F`、`Ctrl+H`、`Alt+Enter`(替换全部)。
|
||||
支持 30+ 种语言的专业语法高亮:
|
||||
|
||||
### Markdown 预览
|
||||
- `panelStore` 为每个文档维护预览状态,保证不同文档互不影响。
|
||||
- 选中 Markdown 块后点击工具栏预览按钮即可在右侧展开实时渲染面板。
|
||||
- 自动语言检测
|
||||
- 可自定义配色方案
|
||||
- 支持嵌套语言
|
||||
- 代码折叠支持
|
||||
|
||||
### HTTP 客户端
|
||||
- Request DSL + 运行器在 [专章](/zh/guide/http-client) 详细说明。
|
||||
- 支持变量、响应插入、多种请求体、定制 header、复制 cURL。
|
||||
## HTTP 客户端
|
||||
|
||||
### 翻译助手
|
||||
- `translator` 扩展监听选区,符合长度阈值后显示按钮;由 `TranslationService` 调用 Bing/Google/Youdao/DeepL/TartuNLP。
|
||||
- 支持语种缓存、复制译文、切换译文方向。
|
||||
用于 API 测试的内置 HTTP 客户端:
|
||||
|
||||
### 颜色与高亮
|
||||
- `colorSelector` 识别 `#fff/rgba/hsl`、打开取色器;`textHighlight` 用 `Mod+Shift+H` 标记重要行。
|
||||
### 请求类型
|
||||
- GET、POST、PUT、DELETE、PATCH
|
||||
- 自定义请求头
|
||||
- 多种请求体格式:JSON、FormData、URL 编码、XML、文本
|
||||
|
||||
## 3. 复杂布局能力
|
||||
### 多窗口
|
||||
- `WindowService` 允许为任意文档创建独立 WebView,URL 自动携带 `?documentId=`。
|
||||
- `WindowSnapService` 根据主窗口位置吸附子窗口(上下左右+四角),并缓存尺寸、位置。
|
||||
- 支持全局热键(默认 `Alt+X`)一键显示或隐藏所有窗口。
|
||||
### 请求变量
|
||||
定义和重用变量:
|
||||
|
||||
### 标签页
|
||||
- `tabStore` 通过 `enableTabs` 控制;支持拖拽排序、关闭其他/左侧/右侧标签。
|
||||
- 与多窗口互斥:当文档被新窗口接管后会从标签栏移除,避免重复。
|
||||
```http
|
||||
@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
token: "your-api-token"
|
||||
}
|
||||
|
||||
### 系统托盘与置顶
|
||||
- `TrayService` 控制关闭时隐藏到托盘或直接退出。
|
||||
- 工具栏提供图钉按钮,可即时切换 `AlwaysOnTop`(支持临时置顶和永久置顶)。
|
||||
GET "{{baseUrl}}/users" {
|
||||
authorization: "Bearer {{token}}"
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 数据守护
|
||||
### SQLite + 自动迁移
|
||||
- `DatabaseService` 启动时执行 PRAGMA + 表结构校验,缺失字段自动 `ALTER TABLE`。
|
||||
- 默认生成 `documents/extensions/key_bindings/themes` 等表,支持软删除与锁定。
|
||||
### 响应处理
|
||||
- 查看格式化的 JSON 响应
|
||||
- 查看响应时间和大小
|
||||
- 检查响应头
|
||||
- 保存响应以供日后使用
|
||||
|
||||
### Git 备份
|
||||
- `BackupService` 将 `dataPath` 初始化为 Git 仓库,支持 Token/SSHKey/用户名密码三种方式。
|
||||
- 自动任务按分钟运行(`BackupInterval`),包括 add/commit/push;也可从 UI 触发一次性 push。
|
||||
## 代码格式化
|
||||
|
||||
### 配置快照
|
||||
- 所有设置存于 `config.json`,包含 `metadata.version/lastUpdated`,方便手工回滚。
|
||||
- `ConfigService.Watch` 为窗口吸附、托盘、热键等服务提供实时响应。
|
||||
集成 Prettier 支持:
|
||||
|
||||
### 自动更新
|
||||
- `SelfUpdateService` 先检查主源(Gitea),失败再回退到 GitHub;下载完成后可一键「重启并更新」。
|
||||
- 更新前可选自动触发 Git 备份(`backupBeforeUpdate`)。
|
||||
- 保存时格式化(可选)
|
||||
- 格式化选区或整个块
|
||||
- 支持 JavaScript、TypeScript、CSS、HTML、JSON 等
|
||||
- 可自定义格式化规则
|
||||
|
||||
## 5. 自动化与集成
|
||||
- **启动时动作**:可开启开机自启(`StartupService`)、默认最小化至托盘。
|
||||
- **HTTP 运行挂钩**:`response-inserter` 可在响应块尾部插入 `// @timestamp` 等自定义标记。
|
||||
- **Math/汇率**:`mathBlock` 可引用上一次结果 (`prev`),配合 `CURRENCIES_LOADED` 注解支撑货币换算。
|
||||
- **系统信息**:`SystemService` 暴露内存、GC、Goroutine 数量,可在调试面板查看。
|
||||
## 编辑器扩展
|
||||
|
||||
## 6. 可配置的快捷键
|
||||
- 详见 [键盘快捷键](/zh/guide/keyboard-shortcuts)。默认绑定定义在 `internal/models/key_bindings.go`,前端设置页可逐项修改、禁用。
|
||||
### VSCode 风格搜索
|
||||
- 查找和替换,支持正则表达式
|
||||
- 区分大小写和全字匹配选项
|
||||
- 跨所有块搜索
|
||||
|
||||
### 小地图
|
||||
- 文档的鸟瞰图
|
||||
- 快速导航
|
||||
- 可自定义大小和位置
|
||||
|
||||
### 彩虹括号
|
||||
- 彩色括号配对
|
||||
- 更容易匹配括号
|
||||
- 可自定义颜色
|
||||
|
||||
### 颜色选择器
|
||||
- 可视化颜色选择
|
||||
- 支持 hex、RGB、HSL
|
||||
- 实时预览
|
||||
|
||||
### 翻译工具
|
||||
- 翻译选定的文本
|
||||
- 支持多种语言
|
||||
- 快速键盘访问
|
||||
|
||||
### 文本高亮
|
||||
- 高亮重要文本
|
||||
- 多种高亮颜色
|
||||
- 持久化高亮
|
||||
|
||||
## 多窗口支持
|
||||
|
||||
高效使用多个窗口:
|
||||
|
||||
- 每个窗口都是独立的
|
||||
- 独立的文档
|
||||
- 同步的设置
|
||||
- 窗口状态持久化
|
||||
|
||||
## 主题自定义
|
||||
|
||||
完全控制编辑器外观:
|
||||
|
||||
### 内置主题
|
||||
- 深色模式
|
||||
- 浅色模式
|
||||
- 根据系统自动切换
|
||||
|
||||
### 自定义主题
|
||||
- 创建你自己的主题
|
||||
- 自定义每种颜色
|
||||
- 保存和分享主题
|
||||
- 导入社区主题
|
||||
|
||||
## 自动更新系统
|
||||
|
||||
通过自动更新保持最新:
|
||||
|
||||
- 后台更新检查
|
||||
- 新版本通知
|
||||
- 一键更新
|
||||
- 更新历史
|
||||
- 支持多个更新源(GitHub、Gitea)
|
||||
|
||||
## 数据备份
|
||||
|
||||
使用基于 Git 的备份保护你的数据:
|
||||
|
||||
- 自动备份
|
||||
- 手动触发备份
|
||||
- 支持 GitHub 和 Gitea
|
||||
- 多种认证方式(SSH、Token、密码)
|
||||
- 可配置备份间隔
|
||||
|
||||
## 键盘快捷键
|
||||
|
||||
广泛的键盘支持:
|
||||
|
||||
- 可自定义快捷键
|
||||
- Vim/Emacs 按键绑定(计划中)
|
||||
- 快速命令面板
|
||||
- 上下文感知快捷键
|
||||
|
||||
## 性能
|
||||
|
||||
专为速度而构建:
|
||||
|
||||
- 快速启动时间
|
||||
- 流畅滚动
|
||||
- 高效内存使用
|
||||
- 支持大文件
|
||||
|
||||
## 隐私与安全
|
||||
|
||||
你的数据是安全的:
|
||||
|
||||
- 本地优先存储
|
||||
- 可选云备份
|
||||
- 无遥测或跟踪
|
||||
- 开源代码库
|
||||
|
||||
## 7. 文档 & 帮助
|
||||
- 文档站以 VitePress 构建(`frontend/docs`),内置中英双语导航,可一键部署到 GitHub Pages。
|
||||
- `README` 与本文档同步介绍核心功能;建议将常用工作流截图补充到每个「图片占位」中。
|
||||
|
||||
@@ -1,82 +1,107 @@
|
||||
# 快速开始
|
||||
|
||||

|
||||
> 替换为展示块分隔符、语言标签、内容的截图,帮助读者直观理解 `∞∞∞language` 结构。
|
||||
学习使用 voidraft 的基础知识并创建你的第一个文档。
|
||||
|
||||
## 5 分钟上手流程
|
||||
1. **启动应用**:等待加载动画结束,默认会打开 `default` 文档。
|
||||
2. **新建文档**:点击工具栏的文档列表按钮,输入标题后创建;也可在设置里开启标签页,以便同时挂载多个文档。
|
||||
3. **创建首个块**:
|
||||
- 在空白处输入 `∞∞∞javascript` 并回车。
|
||||
- 输入代码或文本,`CodeBlockExtension` 会自动匹配语法高亮。
|
||||
4. **格式化与预览**:
|
||||
- 选中块后点击工具栏的「Format」或使用 `Ctrl+Shift+F`。
|
||||
- 如果块语言是 `md`,可点击「Preview」按钮开启 Markdown 侧栏。
|
||||
5. **运行 HTTP 请求**:创建 `∞∞∞http` 块,填写请求,再点击行号旁的 Run 按钮即可获取响应。
|
||||
6. **打开第二窗口**:在文档列表中右键文档 -> “在新窗口中打开”。`WindowService` 会创建无边框窗口并自动贴靠主窗口。
|
||||
## 编辑器界面
|
||||
|
||||
## 界面导览
|
||||
- **主编辑区**:CodeMirror 视图,支持鼠标滚轮 + `Ctrl` 缩放(`wheelZoomExtension`)。
|
||||
- **右侧小地图**:`extensions/minimap` 提供鸟瞰和选区同步。
|
||||
- **底部状态**:`editorStore.documentStats` 实时展示行数/字符/选区。
|
||||
- **工具栏**(`Toolbar.vue`):包含文档切换、块语言下拉、窗口置顶、格式化、Markdown 预览、更新提示、进入设置等。
|
||||
当你打开 voidraft 时,你将看到:
|
||||
|
||||
## 块的基本操作
|
||||
| 操作 | 快捷键 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 新建块(下方) | `Ctrl+Enter` | 在当前块后插入 `∞∞∞text-a` 分隔符 |
|
||||
| 新建块(上方) | `Ctrl+Shift+Enter` | 在当前块前插入 |
|
||||
| 跳到上/下一个块 | `Alt+Up / Alt+Down` | 通过 `blockGotoPrevious/Next` 命令 |
|
||||
| 删除块 | `Alt+Delete` | 仅删除块内容,不影响其他块 |
|
||||
| 块排序 | `Ctrl+Shift+↑/↓` | `moveLines` 结合块范围移动 |
|
||||
| 复制块 | `Ctrl+C`(光标在块上即可) | `copyPaste.ts` 自动扩展选区至整个块 |
|
||||
- **主编辑器**:编写和编辑的中心区域
|
||||
- **工具栏**:快速访问常用操作
|
||||
- **状态栏**:显示当前块的语言和其他信息
|
||||
|
||||
## 自动语言与格式化
|
||||
- 当分隔符写成 `∞∞∞text-a` 时,会触发语言自动检测(`lang-detect/autodetect.ts`),常用于粘贴未知代码。
|
||||
- `formatCode.ts` 调用 Prettier,自动选择 parser;若语言不支持,会提示不可格式化。
|
||||
- 块语言可在工具栏下拉中修改,列表由 `lang-parser/languages.ts` 提供。
|
||||
## 创建代码块
|
||||
|
||||
## Markdown / 待办
|
||||
1. 使用 `∞∞∞md` 分隔符。
|
||||
2. 在块内写 Markdown,点击工具栏预览按钮。
|
||||
3. 勾选/取消 Checkbox(`extensions/checkbox`)即可同步更新文本。
|
||||
voidraft 使用基于块的编辑系统。每个块可以有不同的语言:
|
||||
|
||||
## 翻译与文本标注
|
||||
- 选中文本后会浮现翻译入口(`translator` 扩展),点击即可在块内查看结果、复制、切换目标语言。
|
||||
- `textHighlight` 扩展提供 `Mod+Shift+H` 高亮当前选区,颜色可在扩展设置中调整。
|
||||
1. 按 `Ctrl+Enter` 创建新块
|
||||
2. 输入 `∞∞∞` 后跟语言名称(例如 `∞∞∞javascript`)
|
||||
3. 在该块中开始编码
|
||||
|
||||
### 支持的语言
|
||||
|
||||
voidraft 支持 30+ 种编程语言,包括:
|
||||
- JavaScript、TypeScript
|
||||
- Python、Go、Rust
|
||||
- HTML、CSS、Sass
|
||||
- SQL、YAML、JSON
|
||||
- 以及更多...
|
||||
|
||||
## 基本操作
|
||||
|
||||
### 导航
|
||||
|
||||
- `Ctrl+Up/Down`:在块之间移动
|
||||
- `Ctrl+Home/End`:跳转到第一个/最后一个块
|
||||
- `Ctrl+F`:在文档中搜索
|
||||
|
||||
### 编辑
|
||||
|
||||
- `Ctrl+D`:复制当前行
|
||||
- `Ctrl+/`:切换注释
|
||||
- `Alt+Up/Down`:向上/向下移动行
|
||||
- `Ctrl+Shift+F`:格式化代码(如果语言支持 Prettier)
|
||||
|
||||
### 块管理
|
||||
|
||||
- `Ctrl+Enter`:创建新块
|
||||
- `Ctrl+Shift+Enter`:在上方创建块
|
||||
- `Alt+Delete`:删除当前块
|
||||
|
||||
## 使用 HTTP 客户端
|
||||
|
||||
voidraft 包含用于测试 API 的内置 HTTP 客户端:
|
||||
|
||||
1. 创建一个 HTTP 语言的块
|
||||
2. 编写你的 HTTP 请求:
|
||||
|
||||
## HTTP 客户端概览
|
||||
```http
|
||||
∞∞∞http
|
||||
@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
token: "{{secrets.token}}"
|
||||
}
|
||||
|
||||
POST "{{baseUrl}}/users" {
|
||||
authorization: "Bearer {{token}}"
|
||||
POST "https://api.example.com/users" {
|
||||
content-type: "application/json"
|
||||
|
||||
@json {
|
||||
name: "voidraft",
|
||||
role: "developer"
|
||||
name: "张三",
|
||||
email: "zhangsan@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `parser/request-parser.ts` 会将变量与请求体解析为结构化对象。
|
||||
- 点击 gutter Run 获取响应,`response-inserter.ts` 会将结果写入 `### Response` 区块。
|
||||
|
||||
## 自动保存与版本安全
|
||||
- `editorStore` 为每个文档维护 `autoSaveTimer`,默认 2000 ms,可在设置 > 编辑 调整。
|
||||
- `documentStates` 记录每个文档的光标位置,切换文档或重启应用都会恢复。
|
||||
- 若开启 Git 备份,可在工具栏或设置中查看最近一次 `push` 是否成功。
|
||||
3. 点击运行按钮执行请求
|
||||
4. 内联查看响应
|
||||
|
||||
## 最佳实践
|
||||
- 使用 Markdown 块为每组代码加标题/注释,便于导航。
|
||||
- 重要文档启用“锁定”以避免被删除(文档右键菜单)。
|
||||
- 多窗口 + 吸附用于常驻参考资料,标签页用于在一个窗口内快速切换。
|
||||
- 善用「窗口置顶」图钉,让 voidraft 叠放在 VSCode/浏览器之上。
|
||||
## 多窗口支持
|
||||
|
||||
同时处理多个文档:
|
||||
|
||||
1. 转到 `文件 > 新建窗口`(或 `Ctrl+Shift+N`)
|
||||
2. 每个窗口都是独立的
|
||||
3. 更改会自动保存
|
||||
|
||||
## 自定义主题
|
||||
|
||||
个性化你的编辑器:
|
||||
|
||||
1. 打开设置(`Ctrl+,`)
|
||||
2. 转到外观
|
||||
3. 选择主题或创建自己的主题
|
||||
4. 根据你的偏好自定义颜色
|
||||
|
||||
## 键盘快捷键
|
||||
|
||||
学习基本快捷键:
|
||||
|
||||
| 操作 | 快捷键 |
|
||||
|-----|--------|
|
||||
| 新建窗口 | `Ctrl+Shift+N` |
|
||||
| 搜索 | `Ctrl+F` |
|
||||
| 替换 | `Ctrl+H` |
|
||||
| 格式化代码 | `Ctrl+Shift+F` |
|
||||
| 切换主题 | `Ctrl+Shift+T` |
|
||||
| 命令面板 | `Ctrl+Shift+P` |
|
||||
|
||||
## 下一步
|
||||
|
||||
现在你已经了解了基础知识:
|
||||
|
||||
- 详细探索[功能特性](/zh/guide/features)
|
||||
|
||||
接下来:
|
||||
- [界面总览](/zh/guide/ui-overview)
|
||||
- [块语法与结构](/zh/guide/block-syntax)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# HTTP 客户端
|
||||
|
||||

|
||||
> 替换为 HTTP 块 + 响应卡片的截图。
|
||||
|
||||
voidraft 将 HTTP 测试写成块(`∞∞∞http`),语法与 JetBrains Http Client 类似。解析与执行由 `frontend/src/views/editor/extensions/httpclient` 完成。
|
||||
|
||||
## 基本语法
|
||||
```http
|
||||
∞∞∞http
|
||||
GET "https://api.example.com/users" {
|
||||
accept: "application/json"
|
||||
}
|
||||
```
|
||||
- 方法 + URL 必须用双引号包裹。
|
||||
- Header 以 `key: "value"` 格式编写。
|
||||
- 请求体使用内联指令(见下文)。
|
||||
|
||||
## 变量与环境
|
||||
```http
|
||||
@var {
|
||||
baseUrl: "https://api.example.com",
|
||||
token: "{{secrets.token}}"
|
||||
}
|
||||
|
||||
GET "{{baseUrl}}/users" {
|
||||
authorization: "Bearer {{token}}"
|
||||
}
|
||||
```
|
||||
- `@var` 块使用 JSON 语法。
|
||||
- 变量在任意请求中以 `{{name}}` 引用。
|
||||
- `variable-resolver.ts` 支持嵌套、默认值、外部 secrets 映射。
|
||||
|
||||
## 请求体助手
|
||||
| 指令 | 示例 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `@json` | `@json { "name": "voidraft" }` | 自动 `content-type: application/json` 并格式化 |
|
||||
| `@form` | `@form { username: "demo" }` | 转为 `application/x-www-form-urlencoded` |
|
||||
| `@multipart` | `@multipart { file: @"C:\tmp\a.txt" }` | 读取文件、多段表单 |
|
||||
| `@text` | `@text <raw body>` | 自由文本 |
|
||||
|
||||
## 运行与响应
|
||||
1. 将光标置于 HTTP 块内。
|
||||
2. 点击行号左侧的 Run(三角形图标),或按下自定义快捷键。
|
||||
3. 运行结果会插入到块尾的 `### Response` 中,包含:
|
||||
- 状态行 + 响应时间 + 体积。
|
||||
- Headers(可折叠)。
|
||||
- 响应体(自动格式化 JSON / XML / HTML / Text)。
|
||||
- 复制、另存为、再次发送等快捷按钮。
|
||||
|
||||
## 多请求文档
|
||||
- 每个 `∞∞∞http` 块被视为独立请求。
|
||||
- `request-parser.ts` 会解析同一块内的多个请求(以 `###` 分隔)。
|
||||
- 使用 Markdown 块写注释或分组标题。
|
||||
|
||||
## 变量注入顺序
|
||||
1. 块内 `@var`。
|
||||
2. 文档级变量(计划中)。
|
||||
3. 环境变量(`EnvironmentService` 预留)。
|
||||
|
||||
## 调试技巧
|
||||
- 运行器会在控制台打印完整请求信息,可通过 `wails3 dev` 查看。
|
||||
- 如果响应过大,可右键响应块选择“折叠正文”或“导出到文件”。
|
||||
- 网络错误会在响应卡片顶部以红条展示,内容来自 `HttpClientService`。
|
||||
- 需要代理时确保系统代理已设置,voidraft 会自动继承。
|
||||
|
||||
## 与其他功能配合
|
||||
- 运行结果可直接与 Markdown/代码块混排,形成 API 使用手册。
|
||||
- 配合 Git 备份可版本化 API 调试记录。
|
||||
- 可将响应复制到其他块(例如 JSON → Prettier 之后用于 mock)。
|
||||
|
||||
> 欢迎在 Issue 中提交你希望支持的额外 DSL 指令(例如 GraphQL、WebSocket、gRPC)。
|
||||
@@ -1,77 +1,63 @@
|
||||
# 安装
|
||||
|
||||

|
||||
> 替换为安装向导或设置页截图,展示关键开关(置顶、数据目录、自动更新等)。
|
||||
本指南将帮助你在系统上安装 voidraft。
|
||||
|
||||
## 系统要求(2025.11)
|
||||
| 项目 | 最低配置 | 推荐配置 |
|
||||
| --- | --- | --- |
|
||||
| 操作系统 | Windows 10 19045 / Windows 11 21H2 | Windows 11 23H2(macOS/Linux 版本开发中) |
|
||||
| CPU | x86_64 双核 | 4 核以上 |
|
||||
| 内存 | 4 GB | ≥ 8 GB |
|
||||
| 磁盘空间 | 200 MB(含 SQLite 数据) | 1 GB 以上以保存附件/备份 |
|
||||
| 运行环境 | Go 1.21+, Node.js 18+(仅开发者编译时需要) | 同左 + pnpm 8 用于前端 |
|
||||
## 系统要求
|
||||
|
||||
## 获取发行版
|
||||
1. 打开 [GitHub Releases](https://github.com/landaiqing/voidraft/releases) 或自建 Gitea 镜像。
|
||||
2. 下载 `voidraft-windows-amd64-installer.exe`(安装版)或 `voidraft-portable.zip`(绿色版)。
|
||||
3. (可选)验证 SHA256:
|
||||
```powershell
|
||||
Get-FileHash .\voidraft-windows-amd64-installer.exe -Algorithm SHA256
|
||||
```
|
||||
4. 双击安装包,按向导完成安装;或解压绿色版至任意目录并创建快捷方式。
|
||||
- **操作系统**:Windows 10 或更高版本(macOS 和 Linux 支持计划中)
|
||||
- **内存**:最低 4GB,推荐 8GB
|
||||
- **磁盘空间**:200MB 可用空间
|
||||
|
||||
## 首次启动流程
|
||||
1. 启动后将创建数据目录:`%USERPROFILE%\.voidraft\data`(含 `voidraft.db`、`config.json`、`extensions.json`)。
|
||||
2. 默认会生成 `default` 文档和一段示例块 `∞∞∞text-a`。
|
||||
3. 若检测到旧版本数据,`ConfigMigrationService` 会自动迁移字段;`DataMigrationService` 确保表结构一致。
|
||||
4. 首次运行建议立刻打开「设置 > 备份」配置远程 Git 仓库。
|
||||
## 下载
|
||||
|
||||
## 开发者手动构建
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/landaiqing/voidraft.git
|
||||
cd voidraft
|
||||
访问[发布页面](https://github.com/landaiqing/voidraft/releases)并下载适合你平台的最新版本:
|
||||
|
||||
# 安装前端依赖
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
cd ..
|
||||
- **Windows**:`voidraft-windows-amd64-installer.exe`
|
||||
|
||||
# 构建/运行桌面应用
|
||||
wails3 dev # 启动调试
|
||||
wails3 package # 生成安装包(输出位于 bin/)
|
||||
```
|
||||
> 若遇到 `wails3` 未找到,请先执行 `go install github.com/wailsapp/wails/v3/cmd/wails3@latest`。
|
||||
## 安装步骤
|
||||
|
||||
## 数据目录与可执行文件
|
||||
| 类型 | 默认位置 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 安装目录 | `C:\Program Files\voidraft` | 包含主程序与嵌入式前端资源 |
|
||||
| 数据目录 | `C:\Users\<you>\.voidraft\data` | 可在设置 > 通用修改 `dataPath`,修改后需重启 |
|
||||
| 备份仓库 | `dataPath/.git` | `BackupService` 初始化或使用现有仓库 |
|
||||
| 日志 | `%LOCALAPPDATA%/voidraft/logs/*.log` | 通过 Wails `application.Log` 输出 |
|
||||
### Windows
|
||||
|
||||
## 常用 CLI 检查
|
||||
```powershell
|
||||
# 查看版本
|
||||
& "C:\Program Files\voidraft\voidraft.exe" --version
|
||||
1. 从发布页面下载安装程序
|
||||
2. 运行 `voidraft-windows-amd64-installer.exe` 文件
|
||||
3. 按照安装向导操作
|
||||
4. 从开始菜单或桌面快捷方式启动 voidraft
|
||||
|
||||
# 清理缓存(若前端异常)
|
||||
Remove-Item "$env:APPDATA\voidraft\Cache" -Recurse -Force
|
||||
```
|
||||
## 首次启动
|
||||
|
||||
## 防火墙与代理
|
||||
- voidraft 仅在使用 HTTP 客户端、更新检测、REST 翻译器时发起网络请求。
|
||||
- 若处于企业代理,请在系统代理中放行 `voidraft.exe` 或设置环境变量 `HTTP(S)_PROXY`,HTTP 客户端会继承系统代理。
|
||||
首次启动 voidraft 时:
|
||||
|
||||
## 常见安装问题
|
||||
| 症状 | 处理方案 |
|
||||
| --- | --- |
|
||||
| 安装向导被安全策略阻止 | 使用签名哈希进行白名单设置或改用便携版 |
|
||||
| 启动后白屏 | 删除 `%APPDATA%/voidraft/Cache`,确保显卡驱动支持 WebView2 |
|
||||
| `wails3 dev` 报错缺少 WebView2 | 安装 [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) |
|
||||
| 便携版无法写入 | 检查解压目录是否具有写权限,或在设置内切换 `dataPath` 至可写分区 |
|
||||
1. 应用程序将创建一个数据目录来存储你的文档
|
||||
2. 你将看到带有欢迎块的主编辑器界面
|
||||
3. 开始输入或创建你的第一个代码块!
|
||||
|
||||
## 配置
|
||||
|
||||
voidraft 将其配置和数据存储在:
|
||||
|
||||
- **Windows**:`%APPDATA%/voidraft/`
|
||||
|
||||
你可以自定义各种设置,包括:
|
||||
- 编辑器主题(深色/浅色模式)
|
||||
- 代码格式化偏好
|
||||
- 备份设置
|
||||
- 键盘快捷键
|
||||
|
||||
## 更新
|
||||
|
||||
voidraft 包含自动更新功能,会在有新版本时通知你。你可以:
|
||||
|
||||
- 从设置中手动检查更新
|
||||
- 启用自动更新
|
||||
- 选择首选的更新源
|
||||
|
||||
## 故障排除
|
||||
|
||||
如果在安装过程中遇到任何问题:
|
||||
|
||||
1. 确保你有管理员权限
|
||||
2. 检查杀毒软件是否阻止了安装
|
||||
3. 访问我们的 [GitHub issues](https://github.com/landaiqing/voidraft/issues) 页面寻求帮助
|
||||
|
||||
下一步:[快速开始 →](/zh/guide/getting-started)
|
||||
|
||||
> 继续阅读:[快速开始](/zh/guide/getting-started)
|
||||
|
||||
@@ -1,73 +1,50 @@
|
||||
# 简介
|
||||
|
||||
> voidraft 是一款面向开发者的「块式工作台」,用 CodeMirror 6 打造 Heynote 风格的体验,并结合 Wails3 + Go 后端提供系统托盘、全局热键、自动备份等桌面级能力。
|
||||
欢迎使用 voidraft —— 一个专为开发者设计的优雅文本片段记录工具。
|
||||
|
||||

|
||||
> 将 `/img/placeholder-main-ui.png` 替换为真实的应用主界面截图,演示数据面板、工具栏和右侧小地图。
|
||||
## 什么是 voidraft?
|
||||
|
||||
## 产品定位
|
||||
- **核心诉求**:在一处快速记录代码/配置/API 响应/待办清单,并能随时重排、格式化、运行或搜索。
|
||||
- **目标用户**:需要跨项目管理零碎文本的开发者、DevOps、测试或产品技术写作者。
|
||||
- **设计理念**:所有内容都拆成可重排的块(`∞∞∞language`);每个块拥有独立语言、格式化器与扩展;多窗口/多标签保证同一份数据的不同视角。
|
||||
voidraft 是一个现代化的桌面应用程序,帮助开发者管理文本片段、代码块、API 响应、会议笔记和日常待办事项。它为开发工作流程提供了流畅而优雅的编辑体验和强大的功能。
|
||||
|
||||
## 面向场景
|
||||
1. **临时代码/脚本草稿**:支持 30+ 语言高亮、Prettier 格式化、彩虹括号、文本高亮。
|
||||
2. **API 调试台**:HTTP 块内置运行器、变量解析、响应插入;请求和响应始终和文档共存。
|
||||
3. **会议 & 需求记录**:Markdown 块 + Checkbox 扩展 + 颜色标注快速整理想法。
|
||||
4. **翻译与研究**:选中文本即可调 Bing/Google/DeepL/TartuNLP/有道翻译,结果内联呈现。
|
||||
5. **多窗口资料墙**:重要文档可弹出独立无边框窗口,依附(Snap)在主窗口侧边。
|
||||
## 核心特性
|
||||
|
||||
## 核心概念
|
||||
### 块式编辑器
|
||||
- 解析器位于 `frontend/src/views/editor/extensions/codeblock`,依赖自研 Lezer 语法树确保 `∞∞∞` 分隔符稳定。
|
||||
- 块结构(语言、是否自动检测、正文范围)存入 `blockState`,供格式化、移动、复制、HTTP 执行等扩展复用。
|
||||
- `math` 块使用 `math.js` 运行器,`http` 块调用 request parser + gutter run widget。
|
||||
### 块状编辑模式
|
||||
|
||||
### 扩展驱动
|
||||
- 后端通过 `internal/models/extensions.go` 定义扩展 ID/配置,`ExtensionService` 负责持久化。
|
||||
- 前端 `ExtensionManager` 根据扩展配置动态拼装 CodeMirror Extension pipeline(小地图、VSCode Search、Translator、Color Picker 等)。
|
||||
- 所有扩展都可在设置页热切换,立即同步到当前与所有已打开的编辑器实例。
|
||||
voidraft 使用受 Heynote 启发的独特块状编辑系统。你可以将内容分割为独立的代码块,每个块具有:
|
||||
- 不同的编程语言设置
|
||||
- 语法高亮
|
||||
- 独立格式化
|
||||
- 轻松在块之间导航
|
||||
|
||||
### 数据与安全
|
||||
- SQLite 数据保存在 `%USERPROFILE%/.voidraft/data/voidraft.db`(可在设置中自定义 dataPath)。
|
||||
- `DatabaseService` 自动迁移表结构,`DocumentService` 提供软删除/锁定机制避免误删默认草稿。
|
||||
- `BackupService` 基于 go-git(SSH/Token/用户名密码)把 dataPath git 化,可按分钟全量提交、推送到 GitHub/Gitea 等。
|
||||
- `SelfUpdateService` 同时轮询 GitHub/Gitea Release,支持自动下载 + 一键重启。
|
||||
### 开发者工具
|
||||
|
||||
## 系统架构概览
|
||||
| 层级 | 说明 | 关键路径 |
|
||||
| --- | --- | --- |
|
||||
| 桌面容器 | Wails3 + Go 1.21,负责窗口、托盘、热键、服务注入 | `main.go`, `internal/services` |
|
||||
| 后端服务 | Config/Document/Extension/Theme/Backup/Window/Hotkey/Translation 等 | `internal/services/*.go` |
|
||||
| 数据模型 | Document、Theme、KeyBinding、GitBackup、Config | `internal/models` |
|
||||
| 前端应用 | Vue 3 + Vite + Pinia + vue-router | `frontend/src` |
|
||||
| 编辑器内核 | CodeMirror 6 扩展及自研块解析、HTTP DSL、Markdown 预览 | `frontend/src/views/editor` |
|
||||
| 文档站点 | VitePress,多语言导航 | `frontend/docs` |
|
||||
- **HTTP 客户端**:直接在编辑器中测试 API
|
||||
- **代码格式化**:内置 Prettier 支持多种语言
|
||||
- **语法高亮**:支持 30+ 种编程语言
|
||||
- **自动语言检测**:自动识别代码块语言类型
|
||||
|
||||
## 模块速览
|
||||
- **文档存储**:`DocumentService` 支持创建/重命名/软删除/恢复、多窗口并发打开同一文档。
|
||||
- **编辑器实例管理**:`editorStore` 使用 LRU 缓存 + 自动保存计时器,确保在多文档切换时保留光标位置、未保存内容。
|
||||
- **HTTP 客户端**:`extensions/httpclient` 包括 Lezer 语法、变量解析、响应插入与运行 gutter;支持 JSON/FormData/GraphQL 等多体格式。
|
||||
- **Markdown 预览**:`panelStore` 管理逐文档的预览状态,可随块实时刷新。
|
||||
- **多窗口/吸附**:`WindowService` + `WindowSnapService` 根据主窗口位置智能吸附子窗口、自动记忆尺寸。
|
||||
- **全局热键**:`HotkeyService` 监听系统级组合键,切换窗口显隐(默认 Alt+X,可配置)。
|
||||
- **系统托盘**:`systray.SetupSystemTray` 注入显示/隐藏、退出、开机启动等操作。
|
||||
- **翻译生态**:`TranslationService` 聚合 Bing/Google/Youdao/DeepL/TartuNLP,前端 `translator` 扩展提供 Tooltip + 复制。
|
||||
- **主题与外观**:`ThemeService` 预置 12+ 主题,可重置/克隆;前端 `createThemeExtension` 即时应用。
|
||||
### 自定义
|
||||
|
||||
## 数据流(从键盘到持久化)
|
||||
1. 用户按键 -> CodeMirror extensions 更新文档。
|
||||
2. `contentChangeExtension` 记录脏状态并刷新 `documentStats`(行数、字符数、选区字符数)。
|
||||
3. 触发自动保存计时器(默认 2s) -> `DocumentService.UpdateDocumentContent` 写入 SQLite。
|
||||
4. 若开启 Git 自动备份,每次 Commit 会序列化数据库 + 附带 `voidraft_data.bin`。
|
||||
5. 配置变更(Pinia store)通过 `ConfigService.Set` 传回 Go,并触发观察者(如 WindowSnap/Hotkey/Backup)。
|
||||
- **自定义主题**:创建并保存你自己的编辑器主题
|
||||
- **扩展功能**:丰富的编辑器扩展,包括小地图、彩虹括号、颜色选择器等
|
||||
- **多窗口**:同时处理多个文档
|
||||
|
||||
## 版本节奏与路线图
|
||||
- ✅ 当前实现:多窗口、标签页、HTTP 客户端、Markdown Preview、数学块、彩虹括号、翻译、Git 备份、自动更新。
|
||||
- 🚧 进行中:自定义扩展导入、键位模版、Linux/macOS 原生打包。
|
||||
- 🗺️ 规划中:剪贴板历史、团队同步、云端模板市场。
|
||||
### 数据管理
|
||||
|
||||
- **Git 备份**:使用 Git 仓库自动备份
|
||||
- **云同步**:跨设备同步你的数据
|
||||
- **自动更新**:及时获取最新功能
|
||||
|
||||
## 为什么选择 voidraft?
|
||||
|
||||
- **专注开发者**:考虑开发者需求而构建
|
||||
- **现代技术栈**:使用前沿技术(Wails3、Vue 3、CodeMirror 6)
|
||||
- **跨平台**:支持 Windows(macOS 和 Linux 支持计划中)
|
||||
- **开源**:MIT 许可证,社区驱动开发
|
||||
|
||||
## 开始使用
|
||||
|
||||
准备好开始了吗?从我们的[发布页面](https://github.com/landaiqing/voidraft/releases)下载最新版本,或继续阅读文档了解更多。
|
||||
|
||||
下一步:[安装 →](/zh/guide/installation)
|
||||
|
||||
## 下一步
|
||||
- [安装 voidraft](/zh/guide/installation)
|
||||
- [界面总览](/zh/guide/ui-overview)
|
||||
- [快速开始](/zh/guide/getting-started)
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# 键盘快捷键
|
||||
|
||||

|
||||
> 替换为展示快捷键设置界面或常用快捷键速查表的截图。
|
||||
|
||||
快捷键定义源自 `internal/models/key_bindings.go`,在设置 > 键位 中可以启用/禁用或改写。下表列出常用组合:
|
||||
|
||||
## 块管理
|
||||
| 功能 | 默认快捷键 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| 新建块(下方) | `Ctrl+Enter` | `blockAddAfterCurrent` |
|
||||
| 新建块(上方) | `Ctrl+Shift+Enter` | `blockAddBeforeCurrent` |
|
||||
| 跳到上/下一个块 | `Alt+↑ / Alt+↓` | `blockGotoPrevious/Next` |
|
||||
| 选择当前块 | `Ctrl+Shift+A` | `blockSelectAll` |
|
||||
| 删除块 | `Alt+Delete` | `blockDelete` |
|
||||
| 块上移/下移 | `Ctrl+Shift+↑ / Ctrl+Shift+↓` | `blockMoveUp/Down` |
|
||||
| 复制块 | `Ctrl+C`(块获得焦点) | `blockCopy` |
|
||||
| 剪切块 | `Ctrl+X` | `blockCut` |
|
||||
| 粘贴块 | `Ctrl+V` | `blockPaste` |
|
||||
|
||||
## 行与文本编辑
|
||||
| 功能 | 快捷键 |
|
||||
| --- | --- |
|
||||
| 行复制(上/下) | `Shift+Alt+↑ / Shift+Alt+↓` |
|
||||
| 行移动(上/下) | `Alt+↑ / Alt+↓`(在块内部) |
|
||||
| 插入空行 | `Ctrl+Enter`(块尾后仍可插入) |
|
||||
| 选择整行 | `Alt+L` |
|
||||
| 语法级跳转 | `Ctrl+Alt+Left / Ctrl+Alt+Right` |
|
||||
| 匹配括号 | `Shift+Ctrl+\` |
|
||||
| 注释/块注释 | `Ctrl+/` / `Shift+Alt+A` |
|
||||
| Tab 缩进/反向缩进 | `Ctrl+]` / `Ctrl+[` |
|
||||
| 删除单词(向前/向后) | `Ctrl+Backspace` / `Ctrl+Delete` |
|
||||
|
||||
## 搜索与替换
|
||||
| 功能 | 快捷键 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| 打开搜索 | `Ctrl+F` | `showSearch` |
|
||||
| 打开替换 | `Ctrl+H` | `searchShowReplace` |
|
||||
| 切换大小写/整词/正则 | `Alt+C / Alt+W / Alt+R` | `searchToggleCase/Word/Regex` |
|
||||
| 替换全部 | `Alt+Enter` | `searchReplaceAll` |
|
||||
|
||||
## Markdown/预览/格式化
|
||||
| 功能 | 快捷键 |
|
||||
| --- | --- |
|
||||
| 格式化块 | `Ctrl+Shift+F` |
|
||||
| 打开 Markdown 预览 | 工具栏按钮(建议映射到 `Ctrl+Shift+M`) |
|
||||
| 高亮文本 | `Mod+Shift+H` |
|
||||
|
||||
## 窗口与系统
|
||||
| 功能 | 快捷键 |
|
||||
| --- | --- |
|
||||
| 新建窗口 | `Ctrl+Shift+N`(命令面板) |
|
||||
| 全局显示/隐藏所有窗口 | 默认 `Alt+X`(可在设置 > 通用 > 全局热键中修改) |
|
||||
| 打开设置 | `Ctrl+,` |
|
||||
| 切换主题 | `Ctrl+Shift+T`(可自定义) |
|
||||
|
||||
## HTTP 客户端
|
||||
| 功能 | 快捷键 |
|
||||
| --- | --- |
|
||||
| 运行请求 | 点击行号旁 Run 或自定义 `Ctrl+Alt+R` |
|
||||
| 复制响应正文 | `Ctrl+Alt+C`(响应块聚焦时) |
|
||||
|
||||
## 翻译工具
|
||||
| 功能 | 快捷键 |
|
||||
| --- | --- |
|
||||
| 显示翻译浮层 | 选中 ≥ `minSelectionLength` 的文本后按 `Ctrl+'`(可自定义) |
|
||||
| 复制译文 | 在浮层中按 `Ctrl+C` |
|
||||
|
||||
## 自定义与导出
|
||||
1. 打开设置 > 键位,列表会加载来自 `ExtensionService.GetAllKeyBindings()` 的数据。
|
||||
2. 可单独禁用某个绑定或录入新组合;存储在 `%USERPROFILE%/.voidraft/data/key_bindings.json`。
|
||||
3. 需要与系统级快捷键冲突时,可勾选“忽略系统修饰键”。
|
||||
|
||||
> 建议将以上表格打印贴在工作区,或在文档中保留常用组合,方便新同事查阅。
|
||||
@@ -1,67 +0,0 @@
|
||||
# 多窗口与标签页
|
||||
|
||||

|
||||
> 替换为展示主窗口 + 侧边浮窗(子窗口)或标签页齐开的截图。
|
||||
|
||||
## 多窗口工作流
|
||||
- `WindowService.OpenDocumentWindow` 会根据文档 ID 创建新 WebView 窗口,URL 自动附加 `?documentId=<id>`。
|
||||
- `windowStore` 通过查询字符串判断当前是否为子窗口(非主窗口)。
|
||||
- 子窗口具备:
|
||||
- 独立的 CodeMirror 实例与扩展栈。
|
||||
- 与主窗口共享的 Document/Config Store,因此编辑内容实时同步(SQLite 数据库为唯一来源)。
|
||||
- `WindowSnapService` 提供吸附:拖动靠近主窗口边缘时自动贴靠;支持上下左右以及四个角。
|
||||
- 关闭时自动注销吸附状态,避免悬挂引用。
|
||||
|
||||
### 操作步骤
|
||||
1. 打开文档列表(工具栏图标或 `Ctrl+Shift+O`)。
|
||||
2. 右键目标文档 → 选择 “在新窗口中打开”。
|
||||
3. 若文档已在标签页打开,会自动从标签栏移除,防止重复。
|
||||
4. 关闭窗口:
|
||||
- 点击自定义标题栏关闭按钮。
|
||||
- 系统托盘菜单选择退出。
|
||||
|
||||
### 使用建议
|
||||
- 将参考资料或检查清单放在子窗口中,配合“窗口置顶”保持常驻。
|
||||
- 通过 Windows Snap + voidraft Snap 组合,可快速排版 2-4 个窗口。
|
||||
- 若想在多窗口之间同步滚动,可尝试启用“共享视图状态”扩展(计划中)。
|
||||
|
||||
## 标签页模式
|
||||
- 在设置 > 通用中开启“启用标签页”(`config.general.enableTabs`)。
|
||||
- `tabStore` 维护 `tabsMap` + `tabOrder`,支持:
|
||||
- 拖拽排序(拖动标签即可)。
|
||||
- 关闭单个/其他/左侧/右侧标签。
|
||||
- 检测当前文档是否已存在标签。
|
||||
- 标签栏位于主窗口顶部,紧贴工具栏下方。
|
||||
|
||||
### 常用操作
|
||||
| 操作 | 方法 |
|
||||
| --- | --- |
|
||||
| 关闭标签 | 点击标签上的叉号或中键 |
|
||||
| 关闭其他 | 右键标签 → “关闭其他标签” |
|
||||
| 关闭右侧/左侧 | 右键标签 → 选择对应菜单 |
|
||||
| 固定标签(计划中) | 将在后续版本中提供 pin 功能 |
|
||||
|
||||
### Tabs vs 窗口
|
||||
| 项目 | 标签页 | 新窗口 |
|
||||
| --- | --- | --- |
|
||||
| UI 占用 | 集成在一个窗口中 | 独立操作系统窗口 |
|
||||
| 跨屏 | 不方便 | 可拖到其他显示器 |
|
||||
| 独立置顶 | 不可单独置顶 | 每个窗口可单独置顶 |
|
||||
| 推荐场景 | 同一背景的多个文档 | 跨项目/跨显示器对比 |
|
||||
|
||||
## 系统托盘与热键
|
||||
- 勾选 “启用系统托盘” 后,关闭窗口默认隐藏至托盘。
|
||||
- 全局热键(默认 `Alt+X`)由 `HotkeyService` 控制:
|
||||
- 若 main window 可见 → 隐藏所有 voidraft 窗口。
|
||||
- 若 main window 隐藏 → Show + Restore + Focus。
|
||||
- `TrayService` 还提供“最小化到托盘”“显示主窗口”等菜单项。
|
||||
|
||||
## 常见问题
|
||||
| 问题 | 原因 | 解决 |
|
||||
| --- | --- | --- |
|
||||
| 新窗口无法打开 | 文档 ID 不存在或被锁定 | 在文档列表确认状态,必要时解锁 |
|
||||
| 子窗口未吸附 | WindowSnap 未启用 | 设置 > 通用 → 勾选“窗口吸附” |
|
||||
| 关闭窗口直接退出应用 | 未启用托盘模式 | 设置 > 通用 → 启用系统托盘 |
|
||||
| 标签页切换慢 | 同时开启标签 + 多窗口导致资源占用 | 关闭暂不需要的窗口或减少标签 |
|
||||
|
||||
> 如果需要更复杂的布局(如平铺窗口、快捷布局),欢迎在 Issue 中提出建议。
|
||||
@@ -1,71 +0,0 @@
|
||||
# 设置与配置
|
||||
|
||||

|
||||
> 替换为设置页截图,突出通用/编辑/外观/更新/备份等分栏。
|
||||
|
||||
所有设置都映射到 `internal/models/config.go`,持久化文件位于 `%USERPROFILE%/.voidraft/data/config.json`。前端 `configStore` 负责与后端 `ConfigService` 同步。
|
||||
|
||||
## 通用(General)
|
||||
| 选项 | 说明 | 后端键 |
|
||||
| --- | --- | --- |
|
||||
| 窗口置顶 (`alwaysOnTop`) | 永久置顶主窗口 | `general.alwaysOnTop` |
|
||||
| 数据目录 (`dataPath`) | SQLite + 备份所在目录,修改后需重启 | `general.dataPath` |
|
||||
| 系统托盘 (`enableSystemTray`) | 关闭窗口后隐藏到托盘而非退出 | `general.enableSystemTray` |
|
||||
| 开机自启 (`startAtLogin`) | 调用 `StartupService` 注册 | `general.startAtLogin` |
|
||||
| 窗口吸附 (`enableWindowSnap`) | `WindowSnapService` 是否启用 | `general.enableWindowSnap` |
|
||||
| 全局热键 (`enableGlobalHotkey` + `globalHotkey`) | 默认 Alt+X,控制显隐 | `general.globalHotkey` |
|
||||
| 标签页 (`enableTabs`) | 启用多标签界面 | `general.enableTabs` |
|
||||
| 加载动画 (`enableLoadingAnimation`) | 切换文档时显示动画 | `general.enableLoadingAnimation` |
|
||||
|
||||
## 编辑(Editing)
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| Font Size/Family/Weight/Line Height | 立即作用于所有编辑器实例 |
|
||||
| Tab Size/Tab Type/Enable Tab Indent | 映射 `tabExtension` 行为 |
|
||||
| Auto Save Delay | ms,影响 `editorStore` 自动保存周期 |
|
||||
|
||||
## 外观(Appearance)
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| Language | UI 语言(`zh-CN`/`en-US`) |
|
||||
| System Theme | 深色/浅色/跟随系统 |
|
||||
| Current Theme | 选择预设或自定义主题(详见 [主题与外观](/zh/guide/themes)) |
|
||||
|
||||
## 更新(Updates)
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| Auto Update | 启动时自动检查更新 |
|
||||
| Primary/Backup Source | `github` 或 `gitea`,对应 `UpdatesConfig` |
|
||||
| Backup Before Update | 下载更新前执行 Git 备份 |
|
||||
| Update Timeout | HTTP 请求超时 |
|
||||
| GitHub/Gitea 仓库 | owner/repo/baseURL,可指向自建镜像 |
|
||||
|
||||
## 备份(Backup)
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| Enabled | 开关 Git 备份 |
|
||||
| Repo URL | 远程仓库地址(HTTPS 或 SSH) |
|
||||
| Auth Method | `token` / `ssh_key` / `user_pass` |
|
||||
| Username/Password/Token/SSH Key Path | 根据认证方式填写 |
|
||||
| Backup Interval | 自动备份间隔(分钟) |
|
||||
| Auto Backup | 是否按间隔自动推送 |
|
||||
|
||||
## 键位(Key Bindings)
|
||||
- 列表由 `ExtensionService.GetAllKeyBindings()` 提供。可搜索命令 ID 或组合。
|
||||
- 允许将命令禁用(关闭开关)或录入新组合。
|
||||
- 更改立即影响所有编辑器实例。
|
||||
|
||||
## 扩展(Extensions)
|
||||
- 显示 `ExtensionSettings` 中的所有扩展。
|
||||
- 每项可开关并展示 JSON 配置(背景色、最小选区、最小化提示等)。
|
||||
- 修改后调用 `ExtensionService.UpdateExtensionState` 并通知 `ExtensionManager` 热更新。
|
||||
|
||||
## 配置文件备份
|
||||
- 每次修改配置都会更新 `metadata.lastUpdated`,可用 Git 备份追踪历史。
|
||||
- 若出现配置损坏,可删除 `config.json`,应用会写入 `NewDefaultAppConfig`。
|
||||
|
||||
## 导入/导出(建议)
|
||||
- 目前可手动复制 `config.json`/`extensions.json`/`key_bindings.json`。
|
||||
- 计划提供 UI 层面的导入导出按钮,便于跨设备同步。
|
||||
|
||||
> 修改高级选项(如 dataPath)后建议重启,以确保后台服务(数据库、备份、窗口吸附等)读取到最新配置。
|
||||
@@ -1,44 +0,0 @@
|
||||
# 主题与外观
|
||||
|
||||

|
||||
> 替换为主题切换界面或自定义主题编辑器的截图。
|
||||
|
||||
voidraft 的主题由后端 `ThemeService` 管理,存储在 `themes` 表。前端通过 `themeStore` + `createThemeExtension` 应用色板。
|
||||
|
||||
## 预设主题
|
||||
| 名称 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| default-dark | Dark | 默认暗色,适合低光环境 |
|
||||
| default-light | Light | 默认亮色 |
|
||||
| dracula | Dark | 高对比度紫色系 |
|
||||
| aura | Dark | 柔和霓虹风 |
|
||||
| github-dark / github-light | Dark/Light | 与 GitHub 主题接近 |
|
||||
| material-dark / light | Dark/Light | Material Design 色板 |
|
||||
| one-dark | Dark | VSCode 经典主题 |
|
||||
| solarized-dark / light | Dark/Light | Solarized 配色 |
|
||||
| tokyo-night / storm / day | Dark/Light | Tokyo Night 三件套 |
|
||||
|
||||
## 自定义主题
|
||||
1. 打开设置 > 外观,选择「创建主题」。
|
||||
2. 颜色字段对应 `ThemeColorConfig`,包含 `editor.background`, `editor.foreground`, `gutter`, `selection`, `bracket`, `keyword`, `string`, `comment`, `accent` 等。
|
||||
3. 保存后立即写入数据库,可通过 `Reset` 按钮恢复为预设值。
|
||||
4. 前端 `themeExtension` 会向 CodeMirror 注入新的 `EditorView.theme`。
|
||||
|
||||
## 动态切换
|
||||
- 切换主题会立即影响所有已打开的编辑器实例;`updateEditorTheme` 逐个更新 `EditorView`。
|
||||
- `SystemTheme` 设为 `auto` 时,voidraft 会监听操作系统深浅模式并自动切换到 `default-dark` 或 `default-light`。
|
||||
|
||||
## 字体与行高
|
||||
- 字体配置来自设置 > 编辑,`createFontExtensionFromBackend` 会同步 `fontFamily/fontSize/fontWeight/lineHeight`。
|
||||
- 可在通用设置中的“滚轮缩放”手势下临时调整字号。
|
||||
|
||||
## 小地图/装饰色
|
||||
- `minimap` 扩展读取主题中的 `accent` 颜色,用于高亮当前视区。
|
||||
- `textHighlight` 扩展的默认背景色可在扩展设置中配置。
|
||||
|
||||
## 截图建议
|
||||
- 展示暗/亮主题对比。
|
||||
- 展示主题编辑对话框,标出关键字段。
|
||||
- 展示自定义主题应用后的编辑器界面。
|
||||
|
||||
> 如果希望导入 VSCode `.json` 主题,可将颜色映射到 `ThemeColorConfig` 后写入数据库,或等待官方导入工具上线。
|
||||
@@ -1,53 +0,0 @@
|
||||
# 常见问题与故障排查
|
||||
|
||||

|
||||
> 替换为错误提示或日志查看界面的截图。
|
||||
|
||||
## 安装与启动
|
||||
| 问题 | 可能原因 | 解决步骤 |
|
||||
| --- | --- | --- |
|
||||
| 启动白屏 | WebView2 缺失或缓存损坏 | 安装 WebView2 Runtime;删除 `%APPDATA%/voidraft/Cache` 后重启 |
|
||||
| 双击无反应 | 被安全策略拦截 | 以管理员运行或使用便携版;验证 SHA256 后加入白名单 |
|
||||
| `wails3 dev` 报错 | Go/Node 版本不符或缺少 WebView2 | 确保 Go 1.21+、Node 18+,安装 WebView2 |
|
||||
|
||||
## 编辑器
|
||||
| 问题 | 原因 | 解决 |
|
||||
| --- | --- | --- |
|
||||
| 格式化按钮灰色 | 当前块语言无对应 Prettier parser | 更换语言或安装支持的语言扩展(未来版本) |
|
||||
| 块解析错误 | 分隔符格式不正确 | 确认 `∞∞∞language` 后跟换行;可用自动重建语法树命令 |
|
||||
| 翻译浮层不出现 | 选区过短或超过最大长度 | 在设置 > 扩展 > translator 中调整阈值 |
|
||||
| 小地图不同步 | 编辑器实例未刷新 | 切换文档或重开应用,检查扩展是否被禁用 |
|
||||
|
||||
## 窗口与多实例
|
||||
| 问题 | 原因 | 解决 |
|
||||
| --- | --- | --- |
|
||||
| 子窗口未吸附 | WindowSnap 未启用 | 设置 > 通用 → 勾选“窗口吸附” |
|
||||
| 全局热键无效 | 与系统/其他软件冲突 | 在设置 > 通用改用非系统占用组合,比如 `Ctrl+Alt+Space` |
|
||||
| 关闭窗口直接退出 | 未启用托盘模式 | 设置 > 通用 → 启用系统托盘 |
|
||||
|
||||
## HTTP 客户端
|
||||
| 问题 | 可能原因 | 解决 |
|
||||
| --- | --- | --- |
|
||||
| 发送失败,提示 `proxy` | 系统代理配置异常 | 在系统代理或环境变量中设置 HTTP(S)_PROXY,或关闭代理再试 |
|
||||
| 响应乱码 | 服务器未声明编码 | 手动在请求头中加 `accept-charset: utf-8`,或在响应视图切换编码(计划) |
|
||||
| 变量未替换 | 变量名拼写或作用域错误 | 确认 `@var` 定义位置,使用 `{{name}}` 语法 |
|
||||
|
||||
## 数据与备份
|
||||
| 问题 | 可能原因 | 解决 |
|
||||
| --- | --- | --- |
|
||||
| 自动备份停在 “未初始化” | Repo URL/认证缺失 | 补全备份配置或关闭自动备份 |
|
||||
| Push 失败 | Token 权限不足或网络问题 | 为 Token 开启 `repo` scope;检查代理;稍后再试 |
|
||||
| 数据目录迁移后文件缺失 | 未重启或权限不足 | 修改 `dataPath` 后重启应用;确保目标目录可写 |
|
||||
|
||||
## 更新
|
||||
| 问题 | 原因 | 解决 |
|
||||
| --- | --- | --- |
|
||||
| 检查更新超时 | 主源不可达 | 切换到备用源或关闭代理重试 |
|
||||
| 下载完成但未重启 | 权限或文件被占用 | 以管理员运行,关闭杀毒软件后重试 |
|
||||
|
||||
## 收集日志
|
||||
- Wails 日志:`%LOCALAPPDATA%/voidraft/logs/*.log`。
|
||||
- 终端调试:运行 `wails3 dev` 并观察控制台输出。
|
||||
- 若提交 Issue,请附上:系统版本、voidraft 版本、日志片段、复现步骤。
|
||||
|
||||
> 仍未解决?请到 GitHub Issues 提交反馈,并尽可能附上截图和日志。
|
||||
@@ -1,46 +0,0 @@
|
||||
# 界面总览
|
||||
|
||||

|
||||
> 替换为包含顶部工具栏、块区域、右侧小地图、底部状态栏的完整截图。
|
||||
|
||||
voidraft 的主窗口由四个区域组成:
|
||||
|
||||
| 区域 | 位置 | 作用 | 相关代码 |
|
||||
| --- | --- | --- | --- |
|
||||
| 工具栏 | 顶部浮层 | 文档切换、块语言选择、格式化、Markdown 预览、窗口置顶、更新提示、进入设置 | `frontend/src/components/toolbar/Toolbar.vue` |
|
||||
| 编辑器主体 | 中央 | CodeMirror 6 视图,承载块编辑、HTTP 运行器、翻译按钮等 | `frontend/src/views/editor/Editor.vue` + `extensions` |
|
||||
| 导航辅助 | 右侧 | 小地图、滚动条、块徽标、HTTP 运行按钮 | `extensions/minimap`, `codeblock/decorations.ts` |
|
||||
| 底部状态 | 左下角 | 行数、字符数、选区统计、文档脏状态 | `editorStore.documentStats` |
|
||||
|
||||
## 工具栏详解
|
||||
| 项 | 说明 | 快捷入口 |
|
||||
| --- | --- | --- |
|
||||
| 文档切换器 | 展开后列出全部文档,支持搜索、创建、在新窗口打开 | 同步 `DocumentService.ListAllDocumentsMeta` |
|
||||
| 块语言下拉 | 当前块语言,列表取自 `lang-parser/languages.ts`,支持搜索 | 鼠标选择或输入语言 token |
|
||||
| Pin(窗口置顶) | 临时 / 永久置顶切换,调用 `SystemService.SetWindowOnTop` 与 `config.general.alwaysOnTop` | Alt+Space(自定义) |
|
||||
| Format / Preview | 对当前块执行 Prettier 或打开 Markdown 预览 | `Ctrl+Shift+F` / 工具栏按钮 |
|
||||
| 更新提示 | 轮询 `SelfUpdateService`,有更新时显示小点,可直接“检查/下载/重启” | 设置 > 更新 |
|
||||
| 设置入口 | 跳转到 Vue Router 的 `/settings` 页面 | `Ctrl+,` |
|
||||
|
||||
## 多文档视图
|
||||
- **标签页(可选)**:在设置 > 通用中启用“标签页模式”,`tabStore` 将当前文档加入 tab bar,支持拖拽、批量关闭。
|
||||
- **多窗口**:以文档列表右键「在新窗口中打开」或命令面板为入口。`WindowService` 会根据文档 ID 命名窗口,`WindowSnapService` 自动吸附。
|
||||
- **系统托盘**:关闭窗口时默认最小化到托盘,可在托盘图标中重新唤醒或彻底退出。
|
||||
|
||||
## 面板与浮层
|
||||
- **Markdown 预览**:针对选中的 Markdown 块,面板会贴在右侧,支持实时滚动同步、关闭动画。
|
||||
- **HTTP 响应**:运行后在块底部自动插入 `### Response`,可展开查看头部/体/耗时。
|
||||
- **翻译浮层**:选中文本后自动出现按钮,点击后显示结果卡片,附带复制、语种切换。
|
||||
|
||||
## 快捷状态
|
||||
- **底部统计**:
|
||||
- `Ln`:当前块内行号。
|
||||
- `Ch`:字符数。
|
||||
- `Sel`:选区字符数。
|
||||
- **右上角加载动画**:当编辑器实例加载或切换文档时显示,遵循 `enableLoadingAnimation` 设置。
|
||||
|
||||
## 建议截图
|
||||
1. 默认深色主题 + 多块示例。
|
||||
2. 打开 Markdown 预览 + 小地图。
|
||||
3. 展示 HTTP 块运行按钮与响应卡片。
|
||||
4. 展示标签页或多窗口。
|
||||
3849
frontend/package-lock.json
generated
@@ -22,8 +22,8 @@
|
||||
"app:generate": "cd .. && wails3 generate bindings -ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.19.1",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@codemirror/lang-lezer": "^6.0.2",
|
||||
"@codemirror/lang-liquid": "^6.3.0",
|
||||
"@codemirror/lang-liquid": "^6.3.1",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
@@ -44,65 +44,62 @@
|
||||
"@codemirror/lang-vue": "^0.1.3",
|
||||
"@codemirror/lang-wast": "^6.0.2",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.6",
|
||||
"@codemirror/state": "^6.5.3",
|
||||
"@codemirror/view": "^6.39.8",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/lr": "^1.4.3",
|
||||
"@mdit/plugin-katex": "^0.23.2",
|
||||
"@mdit/plugin-tasklist": "^0.22.2",
|
||||
"@lezer/lr": "^1.4.5",
|
||||
"@prettier/plugin-xml": "^3.4.2",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
"@toml-tools/lexer": "^1.0.0",
|
||||
"@toml-tools/parser": "^1.0.0",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@toml-tools/lexer": "^1.0.1",
|
||||
"@toml-tools/parser": "^1.0.1",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@zumer/snapdom": "^2.0.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"colors-named": "^1.0.4",
|
||||
"colors-named-hex": "^1.0.3",
|
||||
"groovy-beautify": "^0.0.17",
|
||||
"highlight.js": "^11.11.1",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"java-parser": "^3.0.1",
|
||||
"linguist-languages": "^9.1.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"mermaid": "^11.12.1",
|
||||
"npm": "^11.6.2",
|
||||
"katex": "^0.16.27",
|
||||
"linguist-languages": "^9.1.11",
|
||||
"marked": "^17.0.1",
|
||||
"mermaid": "^11.12.2",
|
||||
"php-parser": "^3.2.5",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.94.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"prettier": "^3.7.4",
|
||||
"sass": "^1.97.1",
|
||||
"vue": "^3.5.26",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^24.9.2",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@wailsio/runtime": "latest",
|
||||
"@types/node": "^25.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.77",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"globals": "^16.5.0",
|
||||
"happy-dom": "^20.0.10",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"typescript-eslint": "^8.51.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitepress": "^2.0.0-alpha.12",
|
||||
"vitest": "^4.0.8",
|
||||
"vitest": "^4.0.16",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.1.3"
|
||||
"vue-tsc": "^3.2.1"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@latest"
|
||||
|
||||
1
frontend/public/images/blockImage.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366893329" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="106998" width="200" height="200"><path d="M344.064 36.571429v95.085714H155.428571a23.771429 23.771429 0 0 0-23.259428 19.017143l-0.512 4.754285-0.073143 509.952L306.176 505.417143a95.085714 95.085714 0 0 1 121.270857-5.997714l4.827429 3.876571 152.137143 130.413714 102.546285-102.4a95.085714 95.085714 0 0 1 119.003429-12.507428l5.266286 3.657143 81.042285 60.781714 0.073143-118.784 0.512-7.021714a47.542857 47.542857 0 0 1 94.061714 0l0.512 7.021714v404.114286c0 62.171429-47.762286 113.225143-108.617142 118.418285l-10.24 0.438857h-713.142858l-10.24-0.438857a118.857143 118.857143 0 0 1-108.105142-107.52L36.571429 868.498286v-713.142857l0.438857-10.24A118.857143 118.857143 0 0 1 155.428571 36.571429h188.708572z m26.331429 538.916571L131.657143 794.404571l0.073143 74.166858c0 11.483429 8.118857 21.065143 19.017143 23.259428l4.754285 0.512h713.142857a23.771429 23.771429 0 0 0 23.259429-19.017143l0.512-4.754285-0.073143-166.473143-138.093714-103.570286-97.353143 97.28 76.288 65.316571a47.542857 47.542857 0 0 1-58.002286 75.190858l-3.876571-2.925715-300.836572-257.901714zM649.069714 60.269714a47.542857 47.542857 0 0 1 3.291429 63.634286l-3.291429 3.657143-61.44 61.44 61.44 61.44a47.542857 47.542857 0 0 1 3.291429 63.634286l-3.291429 3.657142a47.542857 47.542857 0 0 1-63.634285 3.218286l-3.584-3.291428-95.085715-95.085715a47.542857 47.542857 0 0 1-3.291428-63.634285l3.291428-3.584 95.085715-95.085715a47.542857 47.542857 0 0 1 67.291428 0zM855.259429 57.051429l3.584 3.218285 95.085714 95.085715a47.542857 47.542857 0 0 1 3.291428 63.634285l-3.291428 3.657143-95.085714 95.085714a47.542857 47.542857 0 0 1-70.509715-63.634285l3.291429-3.657143 61.44-61.44-61.44-61.44a47.542857 47.542857 0 0 1-3.291429-63.634286l3.291429-3.657143a47.542857 47.542857 0 0 1 63.634286-3.218285zM344.210286 36.571429a47.542857 47.542857 0 0 1 7.021714 94.573714l-7.021714 0.512V36.571429z" p-id="106999" fill="#e0620d"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
frontend/public/images/colorSelector.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767367606621" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15611" width="200" height="200"><path d="M511.966509 0.066982c96.036384 0 185.886507 26.451603 262.724147 72.443261L767.949763 68.670494l-127.948963 221.638835A254.788666 254.788666 0 0 0 511.966509 256.050237V0.066982z" fill="#E70212" p-id="15612"></path><path d="M767.949763 68.670494a509.577332 509.577332 0 0 1 191.304819 194.120635L955.329506 256.050237l-221.638835 127.991627a257.263171 257.263171 0 0 0-93.689871-93.689871L767.949763 68.670494z" fill="#EA6101" p-id="15613"></path><path d="M955.329506 256.050237A509.577332 509.577332 0 0 1 1023.933018 519.798317V512.033491h-255.983255a254.788666 254.788666 0 0 0-34.259092-127.991627l221.638835-127.991627z" fill="#F39801" p-id="15614"></path><path d="M1023.933018 512.033491c0 90.020778-23.251812 174.665907-64.038478 248.175765l-4.565034 7.80749-221.638835-127.948964c21.758577-37.672202 34.259092-81.402675 34.259092-128.034291v-0.042664L1023.933018 512.033491z" fill="#FCC902" p-id="15615"></path><path d="M733.690671 640.025118l221.638835 127.991628a509.66266 509.66266 0 0 1-179.52959 182.900035l-7.850153 4.479707-127.991627-221.638835A257.263171 257.263171 0 0 0 733.690671 640.025118z" fill="#FEF200" p-id="15616"></path><path d="M640.0008 733.757653L767.949763 955.396488A509.66266 509.66266 0 0 1 521.011251 1024H511.966509v-255.983254a254.788666 254.788666 0 0 0 128.034291-34.259093z" fill="#90C320" p-id="15617"></path><path d="M511.966509 768.016746v255.983254c-90.020778 0-174.665907-23.251812-248.175765-64.038477L255.983254 955.396488l127.991628-221.638835c37.672202 21.758577 81.402675 34.259092 128.034291 34.259093z" fill="#019A44" p-id="15618"></path><path d="M383.974882 733.757653l-127.991628 221.638835a509.66266 509.66266 0 0 1-182.900035-179.529589L68.603512 768.016746l221.638835-127.991628A257.263171 257.263171 0 0 0 383.974882 733.757653z" fill="#019E97" p-id="15619"></path><path d="M255.983254 512.033491c0 46.631616 12.457852 90.362089 34.259093 128.034291L68.603512 768.016746A509.66266 509.66266 0 0 1 0 521.078233V512.033491h255.983254z" fill="#0169B8" p-id="15620"></path><path d="M68.603512 256.050237l221.638835 127.991627A254.788666 254.788666 0 0 0 255.983254 512.033491H0c0-90.020778 23.251812-174.665907 64.038477-248.175765L68.603512 256.050237z" fill="#1C2089" p-id="15621"></path><path d="M262.681483 64.745418L255.983254 68.670494l127.991628 221.638835A257.263171 257.263171 0 0 0 290.242347 384.041864L68.603512 256.050237a509.577332 509.577332 0 0 1 194.120635-191.304819z" fill="#621988" p-id="15622"></path><path d="M519.731334 0.066982H511.966509v255.983255a254.788666 254.788666 0 0 0-128.034291 34.259092L255.983254 68.670494A509.577332 509.577332 0 0 1 519.731334 0.066982z" fill="#910783" p-id="15623"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
frontend/public/images/contextMenu.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366808037" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="103196" width="200" height="200"><path d="M878.921143 96c27.099429 0 49.078857 21.942857 49.078857 49.078857v349.842286c0 27.099429-21.942857 49.078857-49.078857 49.078857h-221.842286c-27.099429 0-49.078857-21.942857-49.078857-49.078857V145.078857c0-27.099429 21.942857-49.078857 49.078857-49.078857z m-14.921143 320h-192v64h192v-64z m0-128h-192v64h192v-64z m0-128h-192v64h192v-64zM384 309.321143A202.678857 202.678857 0 0 1 586.678857 512v213.321143a202.678857 202.678857 0 0 1-202.678857 202.678857H298.678857a202.678857 202.678857 0 0 1-202.678857-202.678857V512a202.678857 202.678857 0 0 1 202.678857-202.678857z m138.642286 298.642286H160v117.394285a138.678857 138.678857 0 0 0 131.547429 138.459429l7.131428 0.182857H384a138.678857 138.678857 0 0 0 138.678857-138.678857l-0.036571-117.357714z m-213.321143-234.642286h-10.642286A138.678857 138.678857 0 0 0 160 512v31.963429h149.321143v-170.642286z" p-id="103197" fill="#8992c8"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
frontend/public/images/fold.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366707029" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="100833" width="200" height="200"><path d="M533.333 54c16.138 0 29.75 12.016 31.753 28.03l17.828 142.569L896 224.6c17.496 0 31.713 14.042 31.996 31.47l0.004 0.53V939c0 17.673-14.327 32-32 32H469.735a31.94 31.94 0 0 1-3.051-0.102l-0.09-0.009c-10.834-0.868-19.434-6.86-24.45-14.999a31.766 31.766 0 0 1-3.374-7.39 32.348 32.348 0 0 1-1.405-7.246L419.752 800.4H128c-17.496 0-31.713-14.042-31.996-31.47L96 768.4V86c0-17.673 14.327-32 32-32zM864 288.599H590.917l13.33 106.6L704 395.2c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004-91.75-0.001 13.331 106.6L704 565.8c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004-70.416-0.001 16.548 132.332 0.287 2.298c0.024 0.188 0.046 0.376 0.066 0.565 0.986 9.171-2.002 17.793-7.523 24.232l-0.217 0.25L539.872 907H864v-618.4zM548.127 800.4H484.25l7.985 63.851 55.892-63.851zM505.085 118H160v618.4h287.598c0.302-0.004 0.603-0.004 0.904 0h133.913l-0.001-0.004L569.01 629.21l-3.386-27.076c-0.03-0.225-0.058-0.45-0.084-0.676l-21.256-169.977c-0.03-0.219-0.056-0.438-0.081-0.659L505.085 118zM448 565.8c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h192z m-21.333-170.6c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.471 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h170.667z m-21.334-170.6c17.673 0 32 14.327 32 32 0 17.496-14.041 31.713-31.47 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h149.333z" p-id="100834" fill="#1aaba8"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
frontend/public/images/highlightTrailingWhitespace.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767367226608" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5585" width="200" height="200"><path d="M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z" fill="#a4579d" p-id="5586"></path></svg>
|
||||
|
After Width: | Height: | Size: 330 B |
1
frontend/public/images/highlightWhitespace.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767366207284" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="95138" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M753.5 130.4v763.3h70.1v66.4H477v-66.4h70.1V547.1H443.9c-44.2 0-84.8-11.1-121.7-33.2-36.9-22.1-66.4-51.6-88.5-88.5s-33.2-76.8-33.2-119.8 11.1-83.6 33.2-121.7c22.1-38.1 51.6-67.6 88.5-88.5s77.4-31.3 121.7-31.3h379.8v66.4h-70.1z m-206.5 0H443.8c-49.2 0-90.3 17.2-123.5 51.6-33.2 34.4-49.8 75.6-49.8 123.5s16.6 88.5 49.8 121.7c33.2 33.2 74.4 49.8 123.5 49.8H547V130.4z m140.1 0H617v763.3h70.1V130.4z" p-id="95139"></path></svg>
|
||||
|
After Width: | Height: | Size: 758 B |
1
frontend/public/images/httpClient.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365935803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="81379" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M896 64a64 64 0 0 1 64 64v613.952l-121.28-95.68a83.2 83.2 0 0 0-110.848 7.04l-6.016 6.784-5.312 7.616a83.2 83.2 0 0 0-12.032 34.56l-0.064 1.728H595.2A83.2 83.2 0 0 0 512 787.2v44.8H128a64 64 0 0 1-64-64V128a64 64 0 0 1 64-64h768z" fill="#B5E3CC" p-id="81380"></path><path d="M640 256a32 32 0 1 1 0 64h-32v224a32 32 0 0 1-64 0V320h-64v224a32 32 0 0 1-64 0V320H384a32 32 0 0 1 0-64h256z m160 0a96 96 0 0 1 0 192H768v96a32 32 0 0 1-26.24 31.488L736 576a32 32 0 0 1-32-32v-256a32 32 0 0 1 32-32h64zM768 384h32a32 32 0 1 0 0-64H768v64zM288 256a32 32 0 0 1 32 32v256a32 32 0 0 1-64 0V448H192v96a32 32 0 0 1-64 0v-256a32 32 0 0 1 64 0V384h64V288a32 32 0 0 1 32-32zM772.096 699.712a19.2 19.2 0 0 0-4.096 11.904v56.32L595.2 768a19.2 19.2 0 0 0-19.2 19.2v89.6a19.2 19.2 0 0 0 19.2 19.2H768v56.384a19.2 19.2 0 0 0 31.104 15.104l152.576-120.384a19.2 19.2 0 0 0 0-30.208l-152.576-120.32a19.2 19.2 0 0 0-27.008 3.136z" fill="#129250" p-id="81381"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/images/hyperlink.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365120766" class="icon" viewBox="0 0 1160 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12589" xmlns:xlink="http://www.w3.org/1999/xlink" width="226.5625" height="200"><path d="M398.5408 736.142222l435.768889-384.568889a44.009244 44.009244 0 0 0 0-67.675022 59.483022 59.483022 0 0 0-76.731733 0l-435.768889 384.568889a43.872711 43.872711 0 0 0 0 67.675022 59.164444 59.164444 0 0 0 76.731733 0z m143.7696-66.582755a39.867733 39.867733 0 0 1 9.102222 25.031111 38.866489 38.866489 0 0 1-13.653333 30.128355L308.929422 926.151111a52.8384 52.8384 0 0 1-68.266666 0L111.047111 811.781689a39.139556 39.139556 0 0 1 0-60.302222l228.192711-201.9328a50.335289 50.335289 0 0 1 34.178845-11.969423 53.748622 53.748622 0 0 1 22.755555 4.551112l69.632-60.848356c-57.617067-41.824711-141.7216-38.365867-194.696533 7.964444L42.052267 690.631111c-56.069689 50.3808-56.069689 131.117511 0 181.4528l130.207289 114.323911a158.651733 158.651733 0 0 0 204.8 0L605.297778 785.066667c54.613333-48.196267 57.025422-125.474133 5.779911-176.355556zM1117.980444 151.916089L988.410311 37.546667c-56.797867-50.062222-148.821333-50.062222-205.664711 0L554.552889 238.933333c-52.519822 46.739911-56.388267 120.968533-9.102222 171.804445L614.4 349.889422a41.688178 41.688178 0 0 1-5.142756-20.48 38.866489 38.866489 0 0 1 13.653334-30.128355l228.875378-201.386667a50.335289 50.335289 0 0 1 34.178844-12.515556 52.383289 52.383289 0 0 1 34.178844 12.515556l129.570134 114.323911a39.139556 39.139556 0 0 1 0 60.302222l-228.192711 201.9328a50.335289 50.335289 0 0 1-34.178845 11.969423 57.7536 57.7536 0 0 1-28.353422-7.418312l-68.266667 60.302223c57.389511 45.101511 144.543289 43.099022 199.202134-4.551111l228.192711-201.9328a117.418667 117.418667 0 0 0 0-180.906667z" fill="#1A97F0" p-id="12590"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
frontend/public/images/lineNumbers.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366749042" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="102155" width="200" height="200"><path d="M426.7008 256c0-23.552 19.0976-42.6496 42.6496-42.6496h384a42.6496 42.6496 0 1 1 0 85.2992h-384A42.6496 42.6496 0 0 1 426.7008 256zM426.7008 512c0-23.552 19.0976-42.6496 42.6496-42.6496h384a42.6496 42.6496 0 1 1 0 85.2992h-384A42.6496 42.6496 0 0 1 426.7008 512zM469.2992 768c0-23.552 19.1488-42.6496 42.7008-42.6496h341.2992a42.6496 42.6496 0 0 1 0 85.2992H512A42.6496 42.6496 0 0 1 469.2992 768zM256 640a42.6496 42.6496 0 0 0-42.6496 42.6496 42.6496 42.6496 0 0 1-85.3504 0 128 128 0 1 1 256 0c0 25.856-11.264 45.6192-22.4256 59.8528-8.0384 10.1888-18.5856 20.48-26.8288 28.5184l-5.888 5.8368a42.4448 42.4448 0 0 1-2.8672 2.56l-37.4784 31.232h52.8384a42.6496 42.6496 0 1 1 0 85.3504H170.6496a42.6496 42.6496 0 0 1-27.2896-75.4688l126.5152-105.472 6.5024-6.3488 0.4096-0.4096 7.2704-7.168c4.608-4.608 7.936-8.192 10.3936-11.3664a28.672 28.672 0 0 0 3.9424-6.0928c0.3072-0.7168 0.256-0.9728 0.256-1.024A42.6496 42.6496 0 0 0 256 640zM272.3328 131.2256a42.6496 42.6496 0 0 1 26.3168 39.424v256a42.6496 42.6496 0 0 1-85.2992 0V273.664l-12.4928 12.4928a42.6496 42.6496 0 1 1-60.3648-60.3136l85.3504-85.3504a42.6496 42.6496 0 0 1 46.4896-9.216z" p-id="102156" fill="#87c38f"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/images/markdown.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365625477" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="51837" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M98.649225 16.619164h734.636904v990.162784h-734.636904z" fill="#FF8A90" p-id="51838"></path><path d="M881.197231 128.411736h-31.940735V32.589531c0-17.642265-14.29847-31.940735-31.940735-31.940735H114.619592c-17.642265 0-31.940735 14.29847-31.940735 31.940735v958.222049c0 17.642265 14.29847 31.940735 31.940735 31.940735h702.696169c17.642265 0 31.940735-14.29847 31.940735-31.940735V415.878351h31.940735c17.642265 0 31.940735-14.29847 31.940735-31.940735V160.352471c0-17.642265-14.29847-31.940735-31.940735-31.940735z m-63.88147 0h-383.288819c-17.642265 0-31.940735 14.29847-31.940735 31.940735V383.937616c0 17.642265 14.29847 31.940735 31.940735 31.940735h383.288819v574.933229H114.619592v-958.222049h702.696169v95.822205z" fill="#2B3139" p-id="51839"></path><path d="M434.026942 160.352471h447.170289V383.937616h-447.170289z" fill="#FFFFFF" p-id="51840"></path><path d="M544.808889 215.175748h49.969783l16.294765 67.025636h0.311922l16.294765-67.025636h49.957306v113.963544h-33.18842v-73.101861h-0.336875l-19.80076 73.101861h-26.176431l-19.788283-73.101861h-0.336875v73.101861h-33.200897zM698.947889 215.175748h57.618092c37.992007 0 51.392143 28.097865 51.392143 56.819573 0 34.947656-18.490691 57.143971-58.241934 57.143971h-50.768301v-113.963544z" fill="#2B3139" p-id="51841"></path><path d="M726.584111 299.918511h13.712058c21.85944 0 25.065991-17.717126 25.065991-28.409787 0-7.174189-2.233356-27.112194-27.611269-27.112194h-11.154303v55.521981z" fill="#FFFFFF" p-id="51842"></path><path d="M630.512369 785.841895l-94.998733-129.709328h57.318647V544.888976h76.445658v111.243591h57.30617L630.487415 785.841895z" fill="#1EB9B0" p-id="51843"></path><path d="M508.139428 785.754557l-76.445657 0.087338v-120.439029l-57.331124 77.194268-57.343601-77.194268v120.439029H240.573389V544.976313h76.445657l57.343601 80.288528 57.331124-80.288528 76.445657-0.087337z" fill="#1EB9B0" p-id="51844"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
frontend/public/images/minimap.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767367796950" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18541" width="200" height="200"><path d="M880.489336 512H993.882278v455.107765A56.786824 56.786824 0 0 1 937.185807 1024h-56.681412V512z" fill="#4AC3BB" fill-opacity=".603" p-id="18542"></path><path d="M143.510513 1024a113.182118 113.182118 0 0 1-80.188235-33.325176A113.980235 113.980235 0 0 1 30.117572 910.215529V113.784471C30.117572 83.606588 42.059219 54.663529 63.322278 33.325176A113.182118 113.182118 0 0 1 143.510513 0h510.238118C778.977807 0 880.489336 101.872941 880.489336 227.553882v739.553883A56.786824 56.786824 0 0 0 937.185807 1024H143.510513z" fill="#4AC3BB" p-id="18543"></path><path d="M575.653572 335.329882c64.331294 65.024 66.529882 169.758118 4.954353 237.477647l-4.954353 5.240471-98.063059 122.473412a28.175059 28.175059 0 0 1-21.985882 10.586353 28.175059 28.175059 0 0 1-21.985883-10.586353l-98.078117-122.473412-4.412236-4.638118c-63.503059-68.487529-60.777412-175.841882 6.098824-240.941176a168.478118 168.478118 0 0 1 238.426353 2.861176z m-120.048941 64.150589a56.470588 56.470588 0 0 0-49.016471 28.611764 57.735529 57.735529 0 0 0 0 57.193412 56.470588 56.470588 0 0 0 49.016471 28.611765c31.247059-0.015059 56.576-25.630118 56.576-57.22353 0-31.578353-25.328941-57.193412-56.576-57.193411z" fill="#FFFFFF" fill-opacity=".95" p-id="18544"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/public/images/rainbowBrackets.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767364585914" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8006" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960 128c0-35.36-28.704-64.224-63.936-61.728A896.48 896.48 0 0 0 326.4 326.4a896.48 896.48 0 0 0-260.128 569.664C63.776 931.296 92.64 960 128 960c0 0 19.52 0.576 32 0 64-272 84.48-383.68 234.24-533.76C544.32 276.16 752 192 960 160V128z" fill="#F8312F" p-id="8007"></path><path d="M960 256V160a800 800 0 0 0-565.76 234.24A801.344 801.344 0 0 0 160 960h96c32-128 106.24-333.76 238.08-465.92C625.92 362.24 752 304 960 256z" fill="#FFB02E" p-id="8008"></path><path d="M960 256v96c-160 16-284.16 96-398.08 209.92C448 676.16 384 800 352 960H256c0-186.56 74.24-365.76 206.08-497.92A704.32 704.32 0 0 1 960 256z" fill="#FFF478" p-id="8009"></path><path d="M630.08 630.08C534.08 726.08 480 832 448 960h-96c0-161.28 64-315.84 177.92-430.08C643.84 416 798.72 352 960 352v96c-144 32-233.92 86.08-329.92 182.08z" fill="#00D26A" p-id="8010"></path><path d="M960 544v-96c-135.68 0-265.92 54.08-361.92 150.08-96 96-150.08 226.24-150.08 361.92h96c0-110.4 43.84-216.32 121.92-294.08C744 587.84 849.92 544 960 544z" fill="#3F5FFF" p-id="8011"></path><path d="M960 576c0 35.36-28.928 63.36-63.584 70.336a320.384 320.384 0 0 0-250.112 250.08C639.36 931.04 611.392 960 576 960h-32c0-110.4 43.84-216.32 121.92-294.08C744 587.84 849.92 544 960 544v32z" fill="#8D65C5" p-id="8012"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
frontend/public/images/search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1767366992790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="111373" width="200" height="200"><path d="M581.973333 846.933333a380.8 380.8 0 1 1 380.8-380.8A381.226667 381.226667 0 0 1 581.973333 846.933333z m0-688a307.2 307.2 0 1 0 307.2 307.2 307.413333 307.413333 0 0 0-307.2-307.2z" fill="#FA6302" p-id="111374"></path><path d="M146.56 938.666667a36.906667 36.906667 0 0 1-26.026667-64l192-190.933334a36.906667 36.906667 0 0 1 52.053334 52.266667l-192 192a37.333333 37.333333 0 0 1-26.026667 10.666667z" fill="#43D7B4" p-id="111375"></path><path d="M470.826667 274.773333m-49.066667 0a49.066667 49.066667 0 1 0 98.133333 0 49.066667 49.066667 0 1 0-98.133333 0Z" fill="#43D7B4" p-id="111376"></path><path d="M312.106667 684.8l-23.68 23.466667A388.693333 388.693333 0 0 0 341.333333 760.32l23.466667-23.253333a36.906667 36.906667 0 0 0-52.053333-52.266667z" fill="#425300" p-id="111377"></path></svg>
|
||||
|
After Width: | Height: | Size: 956 B |
1
frontend/public/images/translator.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
@@ -6,7 +6,10 @@ import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||
import {useThemeStore} from '@/stores/themeStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||
import ToastContainer from '@/components/toast/ToastContainer.vue';
|
||||
import {useTranslationStore} from "@/stores/translationStore";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {LanguageType} from "../bindings/voidraft/internal/models";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const systemStore = useSystemStore();
|
||||
@@ -14,18 +17,18 @@ const keybindingStore = useKeybindingStore();
|
||||
const themeStore = useThemeStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const translationStore = useTranslationStore();
|
||||
const {locale} = useI18n();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
// 并行初始化配置、系统信息和快捷键配置
|
||||
await Promise.all([
|
||||
configStore.initConfig(),
|
||||
systemStore.initializeSystemInfo(),
|
||||
systemStore.initSystemInfo(),
|
||||
keybindingStore.loadKeyBindings(),
|
||||
]);
|
||||
|
||||
// 初始化语言和主题
|
||||
await configStore.initializeLanguage();
|
||||
await themeStore.initializeTheme();
|
||||
locale.value = configStore.config.appearance.language || LanguageType.LangEnUS;
|
||||
await themeStore.initTheme();
|
||||
await translationStore.loadTranslators();
|
||||
|
||||
// 启动时检查更新
|
||||
@@ -39,6 +42,7 @@ onBeforeMount(async () => {
|
||||
<div class="app-content">
|
||||
<router-view/>
|
||||
</div>
|
||||
<ToastContainer/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
1
frontend/src/assets/images/translator.svg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
@@ -1,8 +1,9 @@
|
||||
/* 导入所有CSS文件 */
|
||||
@import 'normalize.css';
|
||||
@import 'variables.css';
|
||||
@import 'scrollbar.css';
|
||||
@import "harmony_fonts.css";
|
||||
@import 'hack_fonts.css';
|
||||
@import 'opensans_fonts.css';
|
||||
@import "monocraft_fonts.css";
|
||||
@import 'variables.css';
|
||||
@import 'scrollbar.css';
|
||||
@import 'styles.css';
|
||||
3
frontend/src/assets/styles/styles.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
@@ -1,255 +1,266 @@
|
||||
:root {
|
||||
/* 编辑器区域 */
|
||||
--text-primary: #9BB586; /* 内容区域字体颜色 */
|
||||
|
||||
/* 深色主题颜色变量 */
|
||||
--dark-toolbar-bg: #2d2d2d;
|
||||
--dark-toolbar-border: #404040;
|
||||
--dark-toolbar-text: #ffffff;
|
||||
--dark-toolbar-text-secondary: #cccccc;
|
||||
--dark-toolbar-button-hover: #404040;
|
||||
--dark-tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
|
||||
--dark-bg-secondary: #0E1217;
|
||||
--dark-text-secondary: #a0aec0;
|
||||
--dark-text-muted: #666;
|
||||
--dark-border-color: #2d3748;
|
||||
--dark-settings-bg: #2a2a2a;
|
||||
--dark-settings-card-bg: #333333;
|
||||
--dark-settings-text: #ffffff;
|
||||
--dark-settings-text-secondary: #cccccc;
|
||||
--dark-settings-border: #444444;
|
||||
--dark-settings-input-bg: #3a3a3a;
|
||||
--dark-settings-input-border: #555555;
|
||||
--dark-settings-hover: #404040;
|
||||
--dark-scrollbar-track: #2a2a2a;
|
||||
--dark-scrollbar-thumb: #555555;
|
||||
--dark-scrollbar-thumb-hover: #666666;
|
||||
--dark-selection-bg: rgba(181, 206, 168, 0.1);
|
||||
--dark-selection-text: #b5cea8;
|
||||
--dark-danger-color: #ff6b6b;
|
||||
--dark-bg-primary: #1a1a1a;
|
||||
--dark-bg-hover: #2a2a2a;
|
||||
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
|
||||
--dark-loading-color: #fff;
|
||||
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
|
||||
--dark-loading-done-color: #6f6;
|
||||
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
|
||||
|
||||
/* 浅色主题颜色变量 */
|
||||
--light-toolbar-bg: #f8f9fa;
|
||||
--light-toolbar-border: #e9ecef;
|
||||
--light-toolbar-text: #212529;
|
||||
--light-toolbar-text-secondary: #495057;
|
||||
--light-toolbar-button-hover: #e9ecef;
|
||||
--light-tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
|
||||
--light-bg-secondary: #f7fef7;
|
||||
--light-text-secondary: #374151;
|
||||
--light-text-muted: #6b7280;
|
||||
--light-border-color: #e5e7eb;
|
||||
--light-settings-bg: #ffffff;
|
||||
--light-settings-card-bg: #f8f9fa;
|
||||
--light-settings-text: #212529;
|
||||
--light-settings-text-secondary: #6c757d;
|
||||
--light-settings-border: #dee2e6;
|
||||
--light-settings-input-bg: #ffffff;
|
||||
--light-settings-input-border: #ced4da;
|
||||
--light-settings-hover: #e9ecef;
|
||||
--light-scrollbar-track: #f1f3f4;
|
||||
--light-scrollbar-thumb: #c1c1c1;
|
||||
--light-scrollbar-thumb-hover: #a8a8a8;
|
||||
--light-selection-bg: rgba(59, 130, 246, 0.15);
|
||||
--light-selection-text: #2563eb;
|
||||
--light-danger-color: #dc3545;
|
||||
--light-bg-primary: #ffffff;
|
||||
--light-bg-hover: #f1f3f4;
|
||||
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
|
||||
--light-loading-color: #1a3c1a;
|
||||
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
|
||||
--light-loading-done-color: #008800;
|
||||
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
|
||||
|
||||
/* 默认使用深色主题 */
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--tab-active-line: var(--dark-tab-active-line);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
|
||||
|
||||
color-scheme: light dark;
|
||||
--voidraft-font-mono: "HarmonyOS", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
/* 监听系统深色主题 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-theme="auto"] {
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--tab-active-line: var(--dark-tab-active-line);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
}
|
||||
/* 默认/暗色主题 */
|
||||
:root,
|
||||
:root[data-theme="dark"],
|
||||
:root[data-theme="auto"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--text-primary: #ffffff;
|
||||
|
||||
--toolbar-bg: #2d2d2d;
|
||||
--toolbar-border: #404040;
|
||||
--toolbar-text: #ffffff;
|
||||
--toolbar-text-secondary: #cccccc;
|
||||
--toolbar-button-hover: #404040;
|
||||
--toolbar-separator: #404040;
|
||||
|
||||
--tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
|
||||
--bg-secondary: #0e1217;
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-hover: #2a2a2a;
|
||||
|
||||
--text-secondary: #a0aec0;
|
||||
--text-muted: #666666;
|
||||
--text-danger: #ff6b6b;
|
||||
|
||||
--border-color: #2d3748;
|
||||
|
||||
--settings-bg: #2a2a2a;
|
||||
--settings-card-bg: #333333;
|
||||
--settings-text: #ffffff;
|
||||
--settings-text-secondary: #cccccc;
|
||||
--settings-border: #444444;
|
||||
--settings-input-bg: #3a3a3a;
|
||||
--settings-input-border: #555555;
|
||||
--settings-hover: #404040;
|
||||
|
||||
--scrollbar-track: #2a2a2a;
|
||||
--scrollbar-thumb: #555555;
|
||||
--scrollbar-thumb-hover: #666666;
|
||||
|
||||
--selection-bg: rgba(181, 206, 168, 0.1);
|
||||
--selection-text: #b5cea8;
|
||||
|
||||
--voidraft-bg-gradient: radial-gradient(#222922, #000500);
|
||||
--voidraft-loading-color: #ffffff;
|
||||
--voidraft-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
|
||||
--voidraft-loading-done-color: #66ff66;
|
||||
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
|
||||
|
||||
/* Markdown 代码块样式 - 暗色主题 */
|
||||
--cm-codeblock-bg: rgba(46, 51, 69, 0.8);
|
||||
--cm-codeblock-radius: 0.4rem;
|
||||
|
||||
|
||||
/* Markdown 内联代码样式 */
|
||||
--cm-inline-code-bg: oklch(28% 0.02 255);
|
||||
|
||||
/* Markdown 上标/下标样式 */
|
||||
--cm-superscript-color: inherit;
|
||||
--cm-subscript-color: inherit;
|
||||
|
||||
/* Markdown 高亮样式 */
|
||||
--cm-highlight-background: rgba(250, 204, 21, 0.35);
|
||||
|
||||
/* Markdown 表格样式 - 暗色主题 */
|
||||
--cm-table-bg: rgba(35, 40, 52, 0.5);
|
||||
--cm-table-header-bg: rgba(46, 51, 69, 0.7);
|
||||
--cm-table-border: rgba(75, 85, 99, 0.35);
|
||||
--cm-table-row-hover: rgba(55, 62, 78, 0.5);
|
||||
|
||||
/* Search Panel - Dark Theme */
|
||||
--search-panel-bg: #252526;
|
||||
--search-panel-text: #cccccc;
|
||||
--search-panel-border: #454545;
|
||||
--search-input-bg: #3c3c3c;
|
||||
--search-input-text: #cccccc;
|
||||
--search-input-border: #3c3c3c;
|
||||
--search-focus-border: #0078d4;
|
||||
--search-btn-hover: rgba(255, 255, 255, 0.1);
|
||||
--search-btn-active-bg: rgba(0, 120, 212, 0.4);
|
||||
--search-btn-active-text: #ffffff;
|
||||
--search-error-border: #f14c4c;
|
||||
--search-error-bg: #5a1d1d;
|
||||
|
||||
/* Search Match Highlight - Dark Theme (VSCode style) */
|
||||
--search-match-bg: rgba(250, 220, 81, 0.85);
|
||||
--search-match-selected-bg: rgba(81, 175, 255, 0.5);
|
||||
--search-match-selected-border: #74b0f4;
|
||||
}
|
||||
|
||||
/* 监听系统浅色主题 */
|
||||
/* 亮色主题 */
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
|
||||
--text-primary: #000000;
|
||||
|
||||
--toolbar-bg: #f8f9fa;
|
||||
--toolbar-border: #e9ecef;
|
||||
--toolbar-text: #212529;
|
||||
--toolbar-text-secondary: #495057;
|
||||
--toolbar-button-hover: #e9ecef;
|
||||
--toolbar-separator: #e9ecef;
|
||||
|
||||
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
|
||||
--bg-secondary: #f7fef7;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-hover: #f1f3f4;
|
||||
|
||||
--text-secondary: #374151;
|
||||
--text-muted: #6b7280;
|
||||
--text-danger: #dc3545;
|
||||
|
||||
--border-color: #e5e7eb;
|
||||
|
||||
--settings-bg: #ffffff;
|
||||
--settings-card-bg: #f8f9fa;
|
||||
--settings-text: #212529;
|
||||
--settings-text-secondary: #6c757d;
|
||||
--settings-border: #dee2e6;
|
||||
--settings-input-bg: #ffffff;
|
||||
--settings-input-border: #ced4da;
|
||||
--settings-hover: #e9ecef;
|
||||
|
||||
--scrollbar-track: #f1f3f4;
|
||||
--scrollbar-thumb: #c1c1c1;
|
||||
--scrollbar-thumb-hover: #a8a8a8;
|
||||
|
||||
--selection-bg: rgba(59, 130, 246, 0.15);
|
||||
--selection-text: #2563eb;
|
||||
|
||||
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
|
||||
--voidraft-loading-color: #1a3c1a;
|
||||
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
|
||||
--voidraft-loading-done-color: #008800;
|
||||
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
|
||||
|
||||
/* Markdown 代码块样式 - 亮色主题 */
|
||||
--cm-codeblock-bg: #f3f3f3;
|
||||
--cm-codeblock-radius: 0.4rem;
|
||||
|
||||
/* Markdown 内联代码样式 */
|
||||
--cm-inline-code-bg: oklch(92.9% 0.013 255.508);
|
||||
|
||||
/* Markdown 上标/下标样式 */
|
||||
--cm-superscript-color: inherit;
|
||||
--cm-subscript-color: inherit;
|
||||
|
||||
/* Markdown 高亮样式 */
|
||||
--cm-highlight-background: rgba(253, 224, 71, 0.45);
|
||||
|
||||
/* Markdown 表格样式 - 亮色主题 */
|
||||
--cm-table-bg: oklch(97.5% 0.006 255);
|
||||
--cm-table-header-bg: oklch(94% 0.01 255);
|
||||
--cm-table-border: oklch(88% 0.008 255);
|
||||
--cm-table-row-hover: oklch(95% 0.008 255);
|
||||
|
||||
/* Search Panel - Light Theme */
|
||||
--search-panel-bg: #f3f3f3;
|
||||
--search-panel-text: #616161;
|
||||
--search-panel-border: #c8c8c8;
|
||||
--search-input-bg: #ffffff;
|
||||
--search-input-text: #616161;
|
||||
--search-input-border: #cecece;
|
||||
--search-focus-border: #0078d4;
|
||||
--search-btn-hover: rgba(0, 0, 0, 0.1);
|
||||
--search-btn-active-bg: rgba(0, 120, 212, 0.2);
|
||||
--search-btn-active-text: #0078d4;
|
||||
--search-error-border: #e51400;
|
||||
--search-error-bg: #fdeceb;
|
||||
|
||||
/* Search Match Highlight - Light Theme (VSCode style) */
|
||||
--search-match-bg: rgba(250, 220, 81, 0.85);
|
||||
--search-match-selected-bg: rgba(38, 143, 255, 0.3);
|
||||
--search-match-selected-border: #268fff;
|
||||
}
|
||||
|
||||
/* 跟随系统的浅色偏好 */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root[data-theme="auto"] {
|
||||
--toolbar-bg: var(--light-toolbar-bg);
|
||||
--toolbar-border: var(--light-toolbar-border);
|
||||
--toolbar-text: var(--light-toolbar-text);
|
||||
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
||||
--toolbar-separator: var(--light-toolbar-button-hover);
|
||||
--tab-active-line: var(--light-tab-active-line);
|
||||
--bg-secondary: var(--light-bg-secondary);
|
||||
--text-secondary: var(--light-text-secondary);
|
||||
--text-muted: var(--light-text-muted);
|
||||
--border-color: var(--light-border-color);
|
||||
--settings-bg: var(--light-settings-bg);
|
||||
--settings-card-bg: var(--light-settings-card-bg);
|
||||
--settings-text: var(--light-settings-text);
|
||||
--settings-text-secondary: var(--light-settings-text-secondary);
|
||||
--settings-border: var(--light-settings-border);
|
||||
--settings-input-bg: var(--light-settings-input-bg);
|
||||
--settings-input-border: var(--light-settings-input-border);
|
||||
--settings-hover: var(--light-settings-hover);
|
||||
--scrollbar-track: var(--light-scrollbar-track);
|
||||
--scrollbar-thumb: var(--light-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--light-selection-bg);
|
||||
--selection-text: var(--light-selection-text);
|
||||
--text-danger: var(--light-danger-color);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-hover: var(--light-bg-hover);
|
||||
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--light-loading-color);
|
||||
--voidraft-loading-glow: var(--light-loading-glow);
|
||||
--voidraft-loading-done-color: var(--light-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--light-loading-overlay);
|
||||
color-scheme: light;
|
||||
|
||||
--text-primary: #000000;
|
||||
|
||||
--toolbar-bg: #f8f9fa;
|
||||
--toolbar-border: #e9ecef;
|
||||
--toolbar-text: #212529;
|
||||
--toolbar-text-secondary: #495057;
|
||||
--toolbar-button-hover: #e9ecef;
|
||||
--toolbar-separator: #e9ecef;
|
||||
|
||||
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
|
||||
--bg-secondary: #f7fef7;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-hover: #f1f3f4;
|
||||
|
||||
--text-secondary: #374151;
|
||||
--text-muted: #6b7280;
|
||||
--text-danger: #dc3545;
|
||||
|
||||
--border-color: #e5e7eb;
|
||||
|
||||
--settings-bg: #ffffff;
|
||||
--settings-card-bg: #f8f9fa;
|
||||
--settings-text: #212529;
|
||||
--settings-text-secondary: #6c757d;
|
||||
--settings-border: #dee2e6;
|
||||
--settings-input-bg: #ffffff;
|
||||
--settings-input-border: #ced4da;
|
||||
--settings-hover: #e9ecef;
|
||||
|
||||
--scrollbar-track: #f1f3f4;
|
||||
--scrollbar-thumb: #c1c1c1;
|
||||
--scrollbar-thumb-hover: #a8a8a8;
|
||||
|
||||
--selection-bg: rgba(59, 130, 246, 0.15);
|
||||
--selection-text: #2563eb;
|
||||
|
||||
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
|
||||
--voidraft-loading-color: #1a3c1a;
|
||||
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
|
||||
--voidraft-loading-done-color: #008800;
|
||||
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
|
||||
|
||||
/* Markdown 代码块样式 - 亮色主题 */
|
||||
--cm-codeblock-bg: oklch(92.9% 0.013 255.508);
|
||||
--cm-codeblock-radius: 0.4rem;
|
||||
|
||||
/* Markdown 内联代码样式 */
|
||||
--cm-inline-code-bg: oklch(92.9% 0.013 255.508);
|
||||
|
||||
/* Markdown 上标/下标样式 */
|
||||
--cm-superscript-color: inherit;
|
||||
--cm-subscript-color: inherit;
|
||||
|
||||
/* Markdown 高亮样式 */
|
||||
--cm-highlight-background: rgba(253, 224, 71, 0.45);
|
||||
|
||||
/* Markdown 表格样式 - 亮色主题 */
|
||||
--cm-table-bg: oklch(97.5% 0.006 255);
|
||||
--cm-table-header-bg: oklch(94% 0.01 255);
|
||||
--cm-table-border: oklch(88% 0.008 255);
|
||||
--cm-table-row-hover: oklch(95% 0.008 255);
|
||||
|
||||
/* Search Panel - Light Theme (auto) */
|
||||
--search-panel-bg: #f3f3f3;
|
||||
--search-panel-text: #616161;
|
||||
--search-panel-border: #c8c8c8;
|
||||
--search-input-bg: #ffffff;
|
||||
--search-input-text: #616161;
|
||||
--search-input-border: #cecece;
|
||||
--search-focus-border: #0078d4;
|
||||
--search-btn-hover: rgba(0, 0, 0, 0.1);
|
||||
--search-btn-active-bg: rgba(0, 120, 212, 0.2);
|
||||
--search-btn-active-text: #0078d4;
|
||||
--search-error-border: #e51400;
|
||||
--search-error-bg: #fdeceb;
|
||||
|
||||
/* Search Match Highlight - Light Theme auto (VSCode style) */
|
||||
--search-match-bg: rgba(250, 220, 81, 0.85);
|
||||
--search-match-selected-bg: rgba(38, 143, 255, 0.3);
|
||||
--search-match-selected-border: #268fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手动选择浅色主题 */
|
||||
:root[data-theme="light"] {
|
||||
--toolbar-bg: var(--light-toolbar-bg);
|
||||
--toolbar-border: var(--light-toolbar-border);
|
||||
--toolbar-text: var(--light-toolbar-text);
|
||||
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
||||
--toolbar-separator: var(--light-toolbar-button-hover);
|
||||
--tab-active-line: var(--light-tab-active-line);
|
||||
--bg-secondary: var(--light-bg-secondary);
|
||||
--text-secondary: var(--light-text-secondary);
|
||||
--text-muted: var(--light-text-muted);
|
||||
--border-color: var(--light-border-color);
|
||||
--settings-bg: var(--light-settings-bg);
|
||||
--settings-card-bg: var(--light-settings-card-bg);
|
||||
--settings-text: var(--light-settings-text);
|
||||
--settings-text-secondary: var(--light-settings-text-secondary);
|
||||
--settings-border: var(--light-settings-border);
|
||||
--settings-input-bg: var(--light-settings-input-bg);
|
||||
--settings-input-border: var(--light-settings-input-border);
|
||||
--settings-hover: var(--light-settings-hover);
|
||||
--scrollbar-track: var(--light-scrollbar-track);
|
||||
--scrollbar-thumb: var(--light-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--light-selection-bg);
|
||||
--selection-text: var(--light-selection-text);
|
||||
--text-danger: var(--light-danger-color);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-hover: var(--light-bg-hover);
|
||||
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--light-loading-color);
|
||||
--voidraft-loading-glow: var(--light-loading-glow);
|
||||
--voidraft-loading-done-color: var(--light-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--light-loading-overlay);
|
||||
}
|
||||
|
||||
/* 手动选择深色主题 */
|
||||
:root[data-theme="dark"] {
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--tab-active-line: var(--dark-tab-active-line);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
}
|
||||
@@ -1,43 +1,19 @@
|
||||
import {
|
||||
AppConfig,
|
||||
AppearanceConfig,
|
||||
EditingConfig,
|
||||
GeneralConfig,
|
||||
AuthMethod,
|
||||
KeyBindingType,
|
||||
LanguageType,
|
||||
SystemThemeType,
|
||||
TabType,
|
||||
UpdatesConfig,
|
||||
UpdateSourceType,
|
||||
GitBackupConfig,
|
||||
AuthMethod
|
||||
} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {FONT_OPTIONS} from './fonts';
|
||||
|
||||
// 配置键映射和限制的类型定义
|
||||
export type GeneralConfigKeyMap = {
|
||||
readonly [K in keyof GeneralConfig]: string;
|
||||
};
|
||||
|
||||
export type EditingConfigKeyMap = {
|
||||
readonly [K in keyof EditingConfig]: string;
|
||||
};
|
||||
|
||||
export type AppearanceConfigKeyMap = {
|
||||
readonly [K in keyof AppearanceConfig]: string;
|
||||
};
|
||||
|
||||
export type UpdatesConfigKeyMap = {
|
||||
readonly [K in keyof UpdatesConfig]: string;
|
||||
};
|
||||
|
||||
export type BackupConfigKeyMap = {
|
||||
readonly [K in keyof GitBackupConfig]: string;
|
||||
};
|
||||
|
||||
export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
|
||||
export type ConfigSection = 'general' | 'editing' | 'appearance' | 'updates' | 'backup';
|
||||
|
||||
// 配置键映射
|
||||
export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
||||
// 统一配置键映射(平级展开)
|
||||
export const CONFIG_KEY_MAP = {
|
||||
// general
|
||||
alwaysOnTop: 'general.alwaysOnTop',
|
||||
dataPath: 'general.dataPath',
|
||||
enableSystemTray: 'general.enableSystemTray',
|
||||
@@ -47,9 +23,8 @@ export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
||||
enableWindowSnap: 'general.enableWindowSnap',
|
||||
enableLoadingAnimation: 'general.enableLoadingAnimation',
|
||||
enableTabs: 'general.enableTabs',
|
||||
} as const;
|
||||
|
||||
export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||
enableMemoryMonitor: 'general.enableMemoryMonitor',
|
||||
// editing
|
||||
fontSize: 'editing.fontSize',
|
||||
fontFamily: 'editing.fontFamily',
|
||||
fontWeight: 'editing.fontWeight',
|
||||
@@ -57,16 +32,13 @@ export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||
enableTabIndent: 'editing.enableTabIndent',
|
||||
tabSize: 'editing.tabSize',
|
||||
tabType: 'editing.tabType',
|
||||
autoSaveDelay: 'editing.autoSaveDelay'
|
||||
} as const;
|
||||
|
||||
export const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||
keymapMode: 'editing.keymapMode',
|
||||
autoSaveDelay: 'editing.autoSaveDelay',
|
||||
// appearance
|
||||
language: 'appearance.language',
|
||||
systemTheme: 'appearance.systemTheme',
|
||||
currentTheme: 'appearance.currentTheme'
|
||||
} as const;
|
||||
|
||||
export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
currentTheme: 'appearance.currentTheme',
|
||||
// updates
|
||||
version: 'updates.version',
|
||||
autoUpdate: 'updates.autoUpdate',
|
||||
primarySource: 'updates.primarySource',
|
||||
@@ -74,10 +46,8 @@ export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
backupBeforeUpdate: 'updates.backupBeforeUpdate',
|
||||
updateTimeout: 'updates.updateTimeout',
|
||||
github: 'updates.github',
|
||||
gitea: 'updates.gitea'
|
||||
} as const;
|
||||
|
||||
export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
|
||||
gitea: 'updates.gitea',
|
||||
// backup
|
||||
enabled: 'backup.enabled',
|
||||
repo_url: 'backup.repo_url',
|
||||
auth_method: 'backup.auth_method',
|
||||
@@ -90,6 +60,8 @@ export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
|
||||
auto_backup: 'backup.auto_backup',
|
||||
} as const;
|
||||
|
||||
export type ConfigKey = keyof typeof CONFIG_KEY_MAP;
|
||||
|
||||
// 配置限制
|
||||
export const CONFIG_LIMITS = {
|
||||
fontSize: {min: 12, max: 28, default: 13},
|
||||
@@ -116,6 +88,7 @@ export const DEFAULT_CONFIG: AppConfig = {
|
||||
enableWindowSnap: true,
|
||||
enableLoadingAnimation: true,
|
||||
enableTabs: false,
|
||||
enableMemoryMonitor: true,
|
||||
},
|
||||
editing: {
|
||||
fontSize: CONFIG_LIMITS.fontSize.default,
|
||||
@@ -125,29 +98,23 @@ export const DEFAULT_CONFIG: AppConfig = {
|
||||
enableTabIndent: true,
|
||||
tabSize: CONFIG_LIMITS.tabSize.default,
|
||||
tabType: CONFIG_LIMITS.tabType.default,
|
||||
keymapMode: KeyBindingType.Standard,
|
||||
autoSaveDelay: 5000
|
||||
},
|
||||
appearance: {
|
||||
language: LanguageType.LangZhCN,
|
||||
systemTheme: SystemThemeType.SystemThemeAuto,
|
||||
systemTheme: SystemThemeType.SystemThemeDark,
|
||||
currentTheme: 'default-dark'
|
||||
},
|
||||
updates: {
|
||||
version: "1.0.0",
|
||||
autoUpdate: true,
|
||||
primarySource: UpdateSourceType.UpdateSourceGithub,
|
||||
backupSource: UpdateSourceType.UpdateSourceGitea,
|
||||
backupBeforeUpdate: true,
|
||||
updateTimeout: 30,
|
||||
updateTimeout: 120,
|
||||
github: {
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
},
|
||||
gitea: {
|
||||
baseURL: "https://git.landaiqing.cn",
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
}
|
||||
},
|
||||
backup: {
|
||||
enabled: false,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// 编辑器实例管理
|
||||
export const EDITOR_CONFIG = {
|
||||
/** 最多缓存的编辑器实例数量 */
|
||||
MAX_INSTANCES: 5,
|
||||
MAX_INSTANCES: 10,
|
||||
/** 语法树缓存过期时间(毫秒) */
|
||||
SYNTAX_TREE_CACHE_TIMEOUT: 30000,
|
||||
/** 加载状态延迟时间(毫秒) */
|
||||
|
||||
1945
frontend/src/common/constant/emojies.ts
Normal file
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* 默认翻译配置
|
||||
*/
|
||||
export const DEFAULT_TRANSLATION_CONFIG = {
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 翻译相关的错误消息
|
||||
*/
|
||||
export const TRANSLATION_ERRORS = {
|
||||
NO_TEXT: 'no text to translate',
|
||||
TRANSLATION_FAILED: 'translation failed',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 翻译结果接口
|
||||
*/
|
||||
export interface TranslationResult {
|
||||
translatedText: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言信息接口
|
||||
*/
|
||||
export interface LanguageInfo {
|
||||
Code: string; // 语言代码
|
||||
Name: string; // 语言名称
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译器扩展配置
|
||||
*/
|
||||
export interface TranslatorConfig {
|
||||
/** 最小选择字符数才显示翻译按钮 */
|
||||
minSelectionLength: number;
|
||||
/** 最大翻译字符数 */
|
||||
maxTranslationLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译图标SVG
|
||||
*/
|
||||
export const TRANSLATION_ICON_SVG = `
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
|
||||
</svg>`;
|
||||
@@ -1,159 +0,0 @@
|
||||
// Enclose abbreviations in <abbr> tags
|
||||
//
|
||||
import MarkdownIt, {StateBlock, StateCore, Token} from 'markdown-it';
|
||||
|
||||
/**
|
||||
* 环境接口,包含缩写定义
|
||||
*/
|
||||
interface AbbrEnv {
|
||||
abbreviations?: { [key: string]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-abbr 插件
|
||||
* 用于支持缩写语法
|
||||
*/
|
||||
export default function abbr_plugin(md: MarkdownIt): void {
|
||||
const escapeRE = md.utils.escapeRE;
|
||||
const arrayReplaceAt = md.utils.arrayReplaceAt;
|
||||
|
||||
// ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on;
|
||||
// you can check character classes here:
|
||||
// http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
|
||||
const OTHER_CHARS = ' \r\n$+<=>^`|~';
|
||||
|
||||
const UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source;
|
||||
const UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source;
|
||||
|
||||
function abbr_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
|
||||
let labelEnd: number;
|
||||
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
||||
const max = state.eMarks[startLine];
|
||||
|
||||
if (pos + 2 >= max) { return false; }
|
||||
|
||||
if (state.src.charCodeAt(pos++) !== 0x2A/* * */) { return false; }
|
||||
if (state.src.charCodeAt(pos++) !== 0x5B/* [ */) { return false; }
|
||||
|
||||
const labelStart = pos;
|
||||
|
||||
for (; pos < max; pos++) {
|
||||
const ch = state.src.charCodeAt(pos);
|
||||
if (ch === 0x5B /* [ */) {
|
||||
return false;
|
||||
} else if (ch === 0x5D /* ] */) {
|
||||
labelEnd = pos;
|
||||
break;
|
||||
} else if (ch === 0x5C /* \ */) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
if (labelEnd! < 0 || state.src.charCodeAt(labelEnd! + 1) !== 0x3A/* : */) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (silent) { return true; }
|
||||
|
||||
const label = state.src.slice(labelStart, labelEnd!).replace(/\\(.)/g, '$1');
|
||||
const title = state.src.slice(labelEnd! + 2, max).trim();
|
||||
if (label.length === 0) { return false; }
|
||||
if (title.length === 0) { return false; }
|
||||
|
||||
const env = state.env as AbbrEnv;
|
||||
if (!env.abbreviations) { env.abbreviations = {}; }
|
||||
// prepend ':' to avoid conflict with Object.prototype members
|
||||
if (typeof env.abbreviations[':' + label] === 'undefined') {
|
||||
env.abbreviations[':' + label] = title;
|
||||
}
|
||||
|
||||
state.line = startLine + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
function abbr_replace(state: StateCore): void {
|
||||
const blockTokens = state.tokens;
|
||||
|
||||
const env = state.env as AbbrEnv;
|
||||
if (!env.abbreviations) { return; }
|
||||
|
||||
const regSimple = new RegExp('(?:' +
|
||||
Object.keys(env.abbreviations).map(function (x: string) {
|
||||
return x.substr(1);
|
||||
}).sort(function (a: string, b: string) {
|
||||
return b.length - a.length;
|
||||
}).map(escapeRE).join('|') +
|
||||
')');
|
||||
|
||||
const regText = '(^|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE +
|
||||
'|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' +
|
||||
'(' + Object.keys(env.abbreviations).map(function (x: string) {
|
||||
return x.substr(1);
|
||||
}).sort(function (a: string, b: string) {
|
||||
return b.length - a.length;
|
||||
}).map(escapeRE).join('|') + ')' +
|
||||
'($|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE +
|
||||
'|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])'
|
||||
|
||||
const reg = new RegExp(regText, 'g');
|
||||
|
||||
for (let j = 0, l = blockTokens.length; j < l; j++) {
|
||||
if (blockTokens[j].type !== 'inline') { continue; }
|
||||
let tokens = blockTokens[j].children!;
|
||||
|
||||
// We scan from the end, to keep position when new tags added.
|
||||
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||
const currentToken = tokens[i];
|
||||
if (currentToken.type !== 'text') { continue; }
|
||||
|
||||
let pos = 0;
|
||||
const text = currentToken.content;
|
||||
reg.lastIndex = 0;
|
||||
const nodes: Token[] = [];
|
||||
|
||||
// fast regexp run to determine whether there are any abbreviated words
|
||||
// in the current token
|
||||
if (!regSimple.test(text)) { continue; }
|
||||
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = reg.exec(text))) {
|
||||
if (m.index > 0 || m[1].length > 0) {
|
||||
const token = new state.Token('text', '', 0);
|
||||
token.content = text.slice(pos, m.index + m[1].length);
|
||||
nodes.push(token);
|
||||
}
|
||||
|
||||
const token_o = new state.Token('abbr_open', 'abbr', 1);
|
||||
token_o.attrs = [['title', env.abbreviations[':' + m[2]]]];
|
||||
nodes.push(token_o);
|
||||
|
||||
const token_t = new state.Token('text', '', 0);
|
||||
token_t.content = m[2];
|
||||
nodes.push(token_t);
|
||||
|
||||
const token_c = new state.Token('abbr_close', 'abbr', -1);
|
||||
nodes.push(token_c);
|
||||
|
||||
reg.lastIndex -= m[3].length;
|
||||
pos = reg.lastIndex;
|
||||
}
|
||||
|
||||
if (!nodes.length) { continue; }
|
||||
|
||||
if (pos < text.length) {
|
||||
const token = new state.Token('text', '', 0);
|
||||
token.content = text.slice(pos);
|
||||
nodes.push(token);
|
||||
}
|
||||
|
||||
// replace current node
|
||||
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
md.block.ruler.before('reference', 'abbr_def', abbr_def, { alt: ['paragraph', 'reference'] });
|
||||
|
||||
md.core.ruler.after('linkify', 'abbr_replace', abbr_replace);
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
// Process definition lists
|
||||
//
|
||||
import MarkdownIt, { StateBlock, Token } from 'markdown-it';
|
||||
|
||||
/**
|
||||
* markdown-it-deflist 插件
|
||||
* 用于支持定义列表语法
|
||||
*/
|
||||
export default function deflist_plugin(md: MarkdownIt): void {
|
||||
const isSpace = md.utils.isSpace;
|
||||
|
||||
// Search `[:~][\n ]`, returns next pos after marker on success
|
||||
// or -1 on fail.
|
||||
function skipMarker(state: StateBlock, line: number): number {
|
||||
let start = state.bMarks[line] + state.tShift[line];
|
||||
const max = state.eMarks[line];
|
||||
|
||||
if (start >= max) { return -1; }
|
||||
|
||||
// Check bullet
|
||||
const marker = state.src.charCodeAt(start++);
|
||||
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1; }
|
||||
|
||||
const pos = state.skipSpaces(start);
|
||||
|
||||
// require space after ":"
|
||||
if (start === pos) { return -1; }
|
||||
|
||||
// no empty definitions, e.g. " : "
|
||||
if (pos >= max) { return -1; }
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
function markTightParagraphs(state: StateBlock, idx: number): void {
|
||||
const level = state.level + 2;
|
||||
|
||||
for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
||||
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
|
||||
state.tokens[i + 2].hidden = true;
|
||||
state.tokens[i].hidden = true;
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deflist(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
|
||||
if (silent) {
|
||||
// quirk: validation mode validates a dd block only, not a whole deflist
|
||||
if (state.ddIndent < 0) { return false; }
|
||||
return skipMarker(state, startLine) >= 0;
|
||||
}
|
||||
|
||||
let nextLine = startLine + 1;
|
||||
if (nextLine >= endLine) { return false; }
|
||||
|
||||
if (state.isEmpty(nextLine)) {
|
||||
nextLine++;
|
||||
if (nextLine >= endLine) { return false; }
|
||||
}
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { return false; }
|
||||
let contentStart = skipMarker(state, nextLine);
|
||||
if (contentStart < 0) { return false; }
|
||||
|
||||
// Start list
|
||||
const listTokIdx = state.tokens.length;
|
||||
let tight = true;
|
||||
|
||||
const token_dl_o: Token = state.push('dl_open', 'dl', 1);
|
||||
const listLines: [number, number] = [startLine, 0];
|
||||
token_dl_o.map = listLines;
|
||||
|
||||
//
|
||||
// Iterate list items
|
||||
//
|
||||
|
||||
let dtLine = startLine;
|
||||
let ddLine = nextLine;
|
||||
|
||||
// One definition list can contain multiple DTs,
|
||||
// and one DT can be followed by multiple DDs.
|
||||
//
|
||||
// Thus, there is two loops here, and label is
|
||||
// needed to break out of the second one
|
||||
//
|
||||
/* eslint no-labels:0,block-scoped-var:0 */
|
||||
OUTER:
|
||||
for (;;) {
|
||||
let prevEmptyEnd = false;
|
||||
|
||||
const token_dt_o: Token = state.push('dt_open', 'dt', 1);
|
||||
token_dt_o.map = [dtLine, dtLine];
|
||||
|
||||
const token_i: Token = state.push('inline', '', 0);
|
||||
token_i.map = [dtLine, dtLine];
|
||||
token_i.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim();
|
||||
token_i.children = [];
|
||||
|
||||
state.push('dt_close', 'dt', -1);
|
||||
|
||||
for (;;) {
|
||||
const token_dd_o: Token = state.push('dd_open', 'dd', 1);
|
||||
const itemLines: [number, number] = [nextLine, 0];
|
||||
token_dd_o.map = itemLines;
|
||||
|
||||
let pos = contentStart;
|
||||
const max = state.eMarks[ddLine];
|
||||
let offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]);
|
||||
|
||||
while (pos < max) {
|
||||
const ch = state.src.charCodeAt(pos);
|
||||
|
||||
if (isSpace(ch)) {
|
||||
if (ch === 0x09) {
|
||||
offset += 4 - offset % 4;
|
||||
} else {
|
||||
offset++;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
contentStart = pos;
|
||||
|
||||
const oldTight = state.tight;
|
||||
const oldDDIndent = state.ddIndent;
|
||||
const oldIndent = state.blkIndent;
|
||||
const oldTShift = state.tShift[ddLine];
|
||||
const oldSCount = state.sCount[ddLine];
|
||||
const oldParentType = state.parentType;
|
||||
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2;
|
||||
state.tShift[ddLine] = contentStart - state.bMarks[ddLine];
|
||||
state.sCount[ddLine] = offset;
|
||||
state.tight = true;
|
||||
state.parentType = 'deflist' as any;
|
||||
|
||||
state.md.block.tokenize(state, ddLine, endLine);
|
||||
|
||||
// If any of list item is tight, mark list as tight
|
||||
if (!state.tight || prevEmptyEnd) {
|
||||
tight = false;
|
||||
}
|
||||
// Item become loose if finish with empty line,
|
||||
// but we should filter last element, because it means list finish
|
||||
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1);
|
||||
|
||||
state.tShift[ddLine] = oldTShift;
|
||||
state.sCount[ddLine] = oldSCount;
|
||||
state.tight = oldTight;
|
||||
state.parentType = oldParentType;
|
||||
state.blkIndent = oldIndent;
|
||||
state.ddIndent = oldDDIndent;
|
||||
|
||||
state.push('dd_close', 'dd', -1);
|
||||
|
||||
itemLines[1] = nextLine = state.line;
|
||||
|
||||
if (nextLine >= endLine) { break OUTER; }
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { break OUTER; }
|
||||
contentStart = skipMarker(state, nextLine);
|
||||
if (contentStart < 0) { break; }
|
||||
|
||||
ddLine = nextLine;
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DD tag and repeat checking
|
||||
}
|
||||
|
||||
if (nextLine >= endLine) { break; }
|
||||
dtLine = nextLine;
|
||||
|
||||
if (state.isEmpty(dtLine)) { break; }
|
||||
if (state.sCount[dtLine] < state.blkIndent) { break; }
|
||||
|
||||
ddLine = dtLine + 1;
|
||||
if (ddLine >= endLine) { break; }
|
||||
if (state.isEmpty(ddLine)) { ddLine++; }
|
||||
if (ddLine >= endLine) { break; }
|
||||
|
||||
if (state.sCount[ddLine] < state.blkIndent) { break; }
|
||||
contentStart = skipMarker(state, ddLine);
|
||||
if (contentStart < 0) { break; }
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DT and DD tags and repeat checking
|
||||
}
|
||||
|
||||
// Finilize list
|
||||
state.push('dl_close', 'dl', -1);
|
||||
|
||||
listLines[1] = nextLine;
|
||||
|
||||
state.line = nextLine;
|
||||
|
||||
// mark paragraphs tight if needed
|
||||
if (tight) {
|
||||
markTightParagraphs(state, listTokIdx);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: ['paragraph', 'reference', 'blockquote'] });
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export { default as bare } from './lib/bare';
|
||||
export { default as light } from './lib/light';
|
||||
export { default as full } from './lib/full';
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import emoji_html from './render';
|
||||
import emoji_replace from './replace';
|
||||
import normalize_opts, { EmojiOptions } from './normalize_opts';
|
||||
|
||||
/**
|
||||
* Bare emoji 插件(不包含预定义的 emoji 数据)
|
||||
*/
|
||||
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
|
||||
const defaults: EmojiOptions = {
|
||||
defs: {},
|
||||
shortcuts: {},
|
||||
enabled: []
|
||||
};
|
||||
|
||||
const opts = normalize_opts(md.utils.assign({}, defaults, options || {}) as EmojiOptions);
|
||||
|
||||
md.renderer.rules.emoji = emoji_html;
|
||||
|
||||
md.core.ruler.after(
|
||||
'linkify',
|
||||
'emoji',
|
||||
emoji_replace(md, opts.defs, opts.shortcuts, opts.scanRE, opts.replaceRE)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
// Generated, don't edit
|
||||
import { EmojiDefs } from '../normalize_opts';
|
||||
|
||||
const emojies: EmojiDefs = {
|
||||
"grinning": "😀",
|
||||
"smiley": "😃",
|
||||
"smile": "😄",
|
||||
"grin": "😁",
|
||||
"laughing": "😆",
|
||||
"satisfied": "😆",
|
||||
"sweat_smile": "😅",
|
||||
"joy": "😂",
|
||||
"wink": "😉",
|
||||
"blush": "😊",
|
||||
"innocent": "😇",
|
||||
"heart_eyes": "😍",
|
||||
"kissing_heart": "😘",
|
||||
"kissing": "😗",
|
||||
"kissing_closed_eyes": "😚",
|
||||
"kissing_smiling_eyes": "😙",
|
||||
"yum": "😋",
|
||||
"stuck_out_tongue": "😛",
|
||||
"stuck_out_tongue_winking_eye": "😜",
|
||||
"stuck_out_tongue_closed_eyes": "😝",
|
||||
"neutral_face": "😐",
|
||||
"expressionless": "😑",
|
||||
"no_mouth": "😶",
|
||||
"smirk": "😏",
|
||||
"unamused": "😒",
|
||||
"relieved": "😌",
|
||||
"pensive": "😔",
|
||||
"sleepy": "😪",
|
||||
"sleeping": "😴",
|
||||
"mask": "😷",
|
||||
"dizzy_face": "😵",
|
||||
"sunglasses": "😎",
|
||||
"confused": "😕",
|
||||
"worried": "😟",
|
||||
"open_mouth": "😮",
|
||||
"hushed": "😯",
|
||||
"astonished": "😲",
|
||||
"flushed": "😳",
|
||||
"frowning": "😦",
|
||||
"anguished": "😧",
|
||||
"fearful": "😨",
|
||||
"cold_sweat": "😰",
|
||||
"disappointed_relieved": "😥",
|
||||
"cry": "😢",
|
||||
"sob": "😭",
|
||||
"scream": "😱",
|
||||
"confounded": "😖",
|
||||
"persevere": "😣",
|
||||
"disappointed": "😞",
|
||||
"sweat": "😓",
|
||||
"weary": "😩",
|
||||
"tired_face": "😫",
|
||||
"rage": "😡",
|
||||
"pout": "😡",
|
||||
"angry": "😠",
|
||||
"smiling_imp": "😈",
|
||||
"smiley_cat": "😺",
|
||||
"smile_cat": "😸",
|
||||
"joy_cat": "😹",
|
||||
"heart_eyes_cat": "😻",
|
||||
"smirk_cat": "😼",
|
||||
"kissing_cat": "😽",
|
||||
"scream_cat": "🙀",
|
||||
"crying_cat_face": "😿",
|
||||
"pouting_cat": "😾",
|
||||
"heart": "❤️",
|
||||
"hand": "✋",
|
||||
"raised_hand": "✋",
|
||||
"v": "✌️",
|
||||
"point_up": "☝️",
|
||||
"fist_raised": "✊",
|
||||
"fist": "✊",
|
||||
"monkey_face": "🐵",
|
||||
"cat": "🐱",
|
||||
"cow": "🐮",
|
||||
"mouse": "🐭",
|
||||
"coffee": "☕",
|
||||
"hotsprings": "♨️",
|
||||
"anchor": "⚓",
|
||||
"airplane": "✈️",
|
||||
"hourglass": "⌛",
|
||||
"watch": "⌚",
|
||||
"sunny": "☀️",
|
||||
"star": "⭐",
|
||||
"cloud": "☁️",
|
||||
"umbrella": "☔",
|
||||
"zap": "⚡",
|
||||
"snowflake": "❄️",
|
||||
"sparkles": "✨",
|
||||
"black_joker": "🃏",
|
||||
"mahjong": "🀄",
|
||||
"phone": "☎️",
|
||||
"telephone": "☎️",
|
||||
"envelope": "✉️",
|
||||
"pencil2": "✏️",
|
||||
"black_nib": "✒️",
|
||||
"scissors": "✂️",
|
||||
"wheelchair": "♿",
|
||||
"warning": "⚠️",
|
||||
"aries": "♈",
|
||||
"taurus": "♉",
|
||||
"gemini": "♊",
|
||||
"cancer": "♋",
|
||||
"leo": "♌",
|
||||
"virgo": "♍",
|
||||
"libra": "♎",
|
||||
"scorpius": "♏",
|
||||
"sagittarius": "♐",
|
||||
"capricorn": "♑",
|
||||
"aquarius": "♒",
|
||||
"pisces": "♓",
|
||||
"heavy_multiplication_x": "✖️",
|
||||
"heavy_plus_sign": "➕",
|
||||
"heavy_minus_sign": "➖",
|
||||
"heavy_division_sign": "➗",
|
||||
"bangbang": "‼️",
|
||||
"interrobang": "⁉️",
|
||||
"question": "❓",
|
||||
"grey_question": "❔",
|
||||
"grey_exclamation": "❕",
|
||||
"exclamation": "❗",
|
||||
"heavy_exclamation_mark": "❗",
|
||||
"wavy_dash": "〰️",
|
||||
"recycle": "♻️",
|
||||
"white_check_mark": "✅",
|
||||
"ballot_box_with_check": "☑️",
|
||||
"heavy_check_mark": "✔️",
|
||||
"x": "❌",
|
||||
"negative_squared_cross_mark": "❎",
|
||||
"curly_loop": "➰",
|
||||
"loop": "➿",
|
||||
"part_alternation_mark": "〽️",
|
||||
"eight_spoked_asterisk": "✳️",
|
||||
"eight_pointed_black_star": "✴️",
|
||||
"sparkle": "❇️",
|
||||
"copyright": "©️",
|
||||
"registered": "®️",
|
||||
"tm": "™️",
|
||||
"information_source": "ℹ️",
|
||||
"m": "Ⓜ️",
|
||||
"black_circle": "⚫",
|
||||
"white_circle": "⚪",
|
||||
"black_large_square": "⬛",
|
||||
"white_large_square": "⬜",
|
||||
"black_medium_square": "◼️",
|
||||
"white_medium_square": "◻️",
|
||||
"black_medium_small_square": "◾",
|
||||
"white_medium_small_square": "◽",
|
||||
"black_small_square": "▪️",
|
||||
"white_small_square": "▫️"
|
||||
};
|
||||
|
||||
export default emojies;
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Emoticons -> Emoji mapping.
|
||||
//
|
||||
// (!) Some patterns skipped, to avoid collisions
|
||||
// without increase matcher complicity. Than can change in future.
|
||||
//
|
||||
// Places to look for more emoticons info:
|
||||
//
|
||||
// - http://en.wikipedia.org/wiki/List_of_emoticons#Western
|
||||
// - https://github.com/wooorm/emoticon/blob/master/Support.md
|
||||
// - http://factoryjoe.com/projects/emoticons/
|
||||
//
|
||||
|
||||
import { EmojiShortcuts } from '../normalize_opts';
|
||||
|
||||
const shortcuts: EmojiShortcuts = {
|
||||
angry: ['>:(', '>:-('],
|
||||
blush: [':")', ':-")'],
|
||||
broken_heart: ['</3', '<\\3'],
|
||||
// :\ and :-\ not used because of conflict with markdown escaping
|
||||
confused: [':/', ':-/'], // twemoji shows question
|
||||
cry: [":'(", ":'-(", ':,(', ':,-('],
|
||||
frowning: [':(', ':-('],
|
||||
heart: ['<3'],
|
||||
imp: [']:(', ']:-('],
|
||||
innocent: ['o:)', 'O:)', 'o:-)', 'O:-)', '0:)', '0:-)'],
|
||||
joy: [":')", ":'-)", ':,)', ':,-)', ":'D", ":'-D", ':,D', ':,-D'],
|
||||
kissing: [':*', ':-*'],
|
||||
laughing: ['x-)', 'X-)'],
|
||||
neutral_face: [':|', ':-|'],
|
||||
open_mouth: [':o', ':-o', ':O', ':-O'],
|
||||
rage: [':@', ':-@'],
|
||||
smile: [':D', ':-D'],
|
||||
smiley: [':)', ':-)'],
|
||||
smiling_imp: [']:)', ']:-)'],
|
||||
sob: [":,'(", ":,'-(", ';(', ';-('],
|
||||
stuck_out_tongue: [':P', ':-P'],
|
||||
sunglasses: ['8-)', 'B-)'],
|
||||
sweat: [',:(', ',:-('],
|
||||
sweat_smile: [',:)', ',:-)'],
|
||||
unamused: [':s', ':-S', ':z', ':-Z', ':$', ':-$'],
|
||||
wink: [';)', ';-)']
|
||||
};
|
||||
|
||||
export default shortcuts;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import emojies_defs from './data/full';
|
||||
import emojies_shortcuts from './data/shortcuts';
|
||||
import bare_emoji_plugin from './bare';
|
||||
import { EmojiOptions } from './normalize_opts';
|
||||
|
||||
/**
|
||||
* Full emoji 插件(包含完整的 emoji 数据)
|
||||
*/
|
||||
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
|
||||
const defaults: EmojiOptions = {
|
||||
defs: emojies_defs,
|
||||
shortcuts: emojies_shortcuts,
|
||||
enabled: []
|
||||
};
|
||||
|
||||
const opts = md.utils.assign({}, defaults, options || {}) as EmojiOptions;
|
||||
|
||||
bare_emoji_plugin(md, opts);
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import emojies_defs from './data/light';
|
||||
import emojies_shortcuts from './data/shortcuts';
|
||||
import bare_emoji_plugin from './bare';
|
||||
import { EmojiOptions } from './normalize_opts';
|
||||
|
||||
/**
|
||||
* Light emoji 插件(包含常用的 emoji 数据)
|
||||
*/
|
||||
export default function emoji_plugin(md: MarkdownIt, options?: Partial<EmojiOptions>): void {
|
||||
const defaults: EmojiOptions = {
|
||||
defs: emojies_defs,
|
||||
shortcuts: emojies_shortcuts,
|
||||
enabled: []
|
||||
};
|
||||
|
||||
const opts = md.utils.assign({}, defaults, options || {}) as EmojiOptions;
|
||||
|
||||
bare_emoji_plugin(md, opts);
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* Emoji 定义类型
|
||||
*/
|
||||
export interface EmojiDefs {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji 快捷方式类型
|
||||
*/
|
||||
export interface EmojiShortcuts {
|
||||
[key: string]: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入选项接口
|
||||
*/
|
||||
export interface EmojiOptions {
|
||||
defs: EmojiDefs;
|
||||
shortcuts: EmojiShortcuts;
|
||||
enabled: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化后的选项接口
|
||||
*/
|
||||
export interface NormalizedEmojiOptions {
|
||||
defs: EmojiDefs;
|
||||
shortcuts: { [key: string]: string };
|
||||
scanRE: RegExp;
|
||||
replaceRE: RegExp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义正则表达式特殊字符
|
||||
*/
|
||||
function quoteRE(str: string): string {
|
||||
return str.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将输入选项转换为更可用的格式并编译搜索正则表达式
|
||||
*/
|
||||
export default function normalize_opts(options: EmojiOptions): NormalizedEmojiOptions {
|
||||
let emojies = options.defs;
|
||||
|
||||
// Filter emojies by whitelist, if needed
|
||||
if (options.enabled.length) {
|
||||
emojies = Object.keys(emojies).reduce((acc: EmojiDefs, key: string) => {
|
||||
if (options.enabled.indexOf(key) >= 0) acc[key] = emojies[key];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Flatten shortcuts to simple object: { alias: emoji_name }
|
||||
const shortcuts = Object.keys(options.shortcuts).reduce((acc: { [key: string]: string }, key: string) => {
|
||||
// Skip aliases for filtered emojies, to reduce regexp
|
||||
if (!emojies[key]) return acc;
|
||||
|
||||
if (Array.isArray(options.shortcuts[key])) {
|
||||
(options.shortcuts[key] as string[]).forEach((alias: string) => { acc[alias] = key; });
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[options.shortcuts[key] as string] = key;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const keys = Object.keys(emojies);
|
||||
let names: string;
|
||||
|
||||
// If no definitions are given, return empty regex to avoid replacements with 'undefined'.
|
||||
if (keys.length === 0) {
|
||||
names = '^$';
|
||||
} else {
|
||||
// Compile regexp
|
||||
names = keys
|
||||
.map((name: string) => { return `:${name}:`; })
|
||||
.concat(Object.keys(shortcuts))
|
||||
.sort()
|
||||
.reverse()
|
||||
.map((name: string) => { return quoteRE(name); })
|
||||
.join('|');
|
||||
}
|
||||
const scanRE = RegExp(names);
|
||||
const replaceRE = RegExp(names, 'g');
|
||||
|
||||
return {
|
||||
defs: emojies,
|
||||
shortcuts,
|
||||
scanRE,
|
||||
replaceRE
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Token } from 'markdown-it';
|
||||
|
||||
/**
|
||||
* Emoji 渲染函数
|
||||
*/
|
||||
export default function emoji_html(tokens: Token[], idx: number): string {
|
||||
return tokens[idx].content;
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import MarkdownIt, { StateCore, Token } from 'markdown-it';
|
||||
import { EmojiDefs } from './normalize_opts';
|
||||
|
||||
/**
|
||||
* Emoji 和快捷方式替换逻辑
|
||||
*
|
||||
* 注意:理论上,在内联链中解析 :smile: 并只留下快捷方式会更快。
|
||||
* 但是,谁在乎呢...
|
||||
*/
|
||||
export default function create_rule(
|
||||
md: MarkdownIt,
|
||||
emojies: EmojiDefs,
|
||||
shortcuts: { [key: string]: string },
|
||||
scanRE: RegExp,
|
||||
replaceRE: RegExp
|
||||
) {
|
||||
const arrayReplaceAt = md.utils.arrayReplaceAt;
|
||||
const ucm = md.utils.lib.ucmicro;
|
||||
const has = md.utils.has;
|
||||
const ZPCc = new RegExp([ucm.Z.source, ucm.P.source, ucm.Cc.source].join('|'));
|
||||
|
||||
function splitTextToken(text: string, level: number, TokenConstructor: any): Token[] {
|
||||
let last_pos = 0;
|
||||
const nodes: Token[] = [];
|
||||
|
||||
text.replace(replaceRE, function (match: string, offset: number, src: string): string {
|
||||
let emoji_name: string;
|
||||
// Validate emoji name
|
||||
if (has(shortcuts, match)) {
|
||||
// replace shortcut with full name
|
||||
emoji_name = shortcuts[match];
|
||||
|
||||
// Don't allow letters before any shortcut (as in no ":/" in http://)
|
||||
if (offset > 0 && !ZPCc.test(src[offset - 1])) return '';
|
||||
|
||||
// Don't allow letters after any shortcut
|
||||
if (offset + match.length < src.length && !ZPCc.test(src[offset + match.length])) {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
emoji_name = match.slice(1, -1);
|
||||
}
|
||||
|
||||
// Add new tokens to pending list
|
||||
if (offset > last_pos) {
|
||||
const token = new TokenConstructor('text', '', 0);
|
||||
token.content = text.slice(last_pos, offset);
|
||||
nodes.push(token);
|
||||
}
|
||||
|
||||
const token = new TokenConstructor('emoji', '', 0);
|
||||
token.markup = emoji_name;
|
||||
token.content = emojies[emoji_name];
|
||||
nodes.push(token);
|
||||
|
||||
last_pos = offset + match.length;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (last_pos < text.length) {
|
||||
const token = new TokenConstructor('text', '', 0);
|
||||
token.content = text.slice(last_pos);
|
||||
nodes.push(token);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
return function emoji_replace(state: StateCore): void {
|
||||
let token: Token;
|
||||
const blockTokens = state.tokens;
|
||||
let autolinkLevel = 0;
|
||||
|
||||
for (let j = 0, l = blockTokens.length; j < l; j++) {
|
||||
if (blockTokens[j].type !== 'inline') { continue; }
|
||||
let tokens = blockTokens[j].children!;
|
||||
|
||||
// We scan from the end, to keep position when new tags added.
|
||||
// Use reversed logic in links start/end match
|
||||
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||
token = tokens[i];
|
||||
|
||||
if (token.type === 'link_open' || token.type === 'link_close') {
|
||||
if (token.info === 'auto') { autolinkLevel -= token.nesting; }
|
||||
}
|
||||
|
||||
if (token.type === 'text' && autolinkLevel === 0 && scanRE.test(token.content)) {
|
||||
// replace current node
|
||||
blockTokens[j].children = tokens = arrayReplaceAt(
|
||||
tokens, i, splitTextToken(token.content, token.level, state.Token)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
import MarkdownIt, {Renderer, StateBlock, StateCore, StateInline, Token} from 'markdown-it';
|
||||
|
||||
/**
|
||||
* 脚注元数据接口
|
||||
*/
|
||||
interface FootnoteMeta {
|
||||
id: number;
|
||||
subId: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脚注列表项接口
|
||||
*/
|
||||
interface FootnoteItem {
|
||||
label?: string;
|
||||
content?: string;
|
||||
tokens?: Token[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 环境接口
|
||||
*/
|
||||
interface FootnoteEnv {
|
||||
footnotes?: {
|
||||
refs?: { [key: string]: number };
|
||||
list?: FootnoteItem[];
|
||||
};
|
||||
docId?: string;
|
||||
}
|
||||
|
||||
/// /////////////////////////////////////////////////////////////////////////////
|
||||
// Renderer partials
|
||||
|
||||
function render_footnote_anchor_name(tokens: Token[], idx: number, options: any, env: FootnoteEnv): string {
|
||||
const n = Number(tokens[idx].meta.id + 1).toString();
|
||||
let prefix = '';
|
||||
|
||||
if (typeof env.docId === 'string') prefix = `-${env.docId}-`;
|
||||
|
||||
return prefix + n;
|
||||
}
|
||||
|
||||
function render_footnote_caption(tokens: Token[], idx: number): string {
|
||||
let n = Number(tokens[idx].meta.id + 1).toString();
|
||||
|
||||
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
return `[${n}]`;
|
||||
}
|
||||
|
||||
function render_footnote_ref(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
|
||||
const id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
|
||||
const caption = slf.rules.footnote_caption!(tokens, idx, options, env, slf);
|
||||
let refid = id;
|
||||
|
||||
if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`;
|
||||
}
|
||||
|
||||
function render_footnote_block_open(tokens: Token[], idx: number, options: any): string {
|
||||
return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') +
|
||||
'<section class="footnotes">\n' +
|
||||
'<ol class="footnotes-list">\n';
|
||||
}
|
||||
|
||||
function render_footnote_block_close(): string {
|
||||
return '</ol>\n</section>\n';
|
||||
}
|
||||
|
||||
function render_footnote_open(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
|
||||
let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
|
||||
|
||||
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
return `<li id="fn${id}" class="footnote-item">`;
|
||||
}
|
||||
|
||||
function render_footnote_close(): string {
|
||||
return '</li>\n';
|
||||
}
|
||||
|
||||
function render_footnote_anchor(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
|
||||
let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
|
||||
|
||||
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
|
||||
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\uFE0E</a>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-footnote 插件
|
||||
* 用于支持脚注语法
|
||||
*/
|
||||
export default function footnote_plugin(md: MarkdownIt): void {
|
||||
const parseLinkLabel = md.helpers.parseLinkLabel;
|
||||
const isSpace = md.utils.isSpace;
|
||||
|
||||
md.renderer.rules.footnote_ref = render_footnote_ref;
|
||||
md.renderer.rules.footnote_block_open = render_footnote_block_open;
|
||||
md.renderer.rules.footnote_block_close = render_footnote_block_close;
|
||||
md.renderer.rules.footnote_open = render_footnote_open;
|
||||
md.renderer.rules.footnote_close = render_footnote_close;
|
||||
md.renderer.rules.footnote_anchor = render_footnote_anchor;
|
||||
|
||||
// helpers (only used in other rules, no tokens are attached to those)
|
||||
md.renderer.rules.footnote_caption = render_footnote_caption;
|
||||
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
|
||||
|
||||
// Process footnote block definition
|
||||
function footnote_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
|
||||
const start = state.bMarks[startLine] + state.tShift[startLine];
|
||||
const max = state.eMarks[startLine];
|
||||
|
||||
// line should be at least 5 chars - "[^x]:"
|
||||
if (start + 4 > max) return false;
|
||||
|
||||
if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false;
|
||||
if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) return false;
|
||||
|
||||
let pos: number;
|
||||
|
||||
for (pos = start + 2; pos < max; pos++) {
|
||||
if (state.src.charCodeAt(pos) === 0x20) return false;
|
||||
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos === start + 2) return false; // no empty footnote labels
|
||||
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) return false;
|
||||
if (silent) return true;
|
||||
pos++;
|
||||
|
||||
const env = state.env as FootnoteEnv;
|
||||
if (!env.footnotes) env.footnotes = {};
|
||||
if (!env.footnotes.refs) env.footnotes.refs = {};
|
||||
const label = state.src.slice(start + 2, pos - 2);
|
||||
env.footnotes.refs[`:${label}`] = -1;
|
||||
|
||||
const token_fref_o = new state.Token('footnote_reference_open', '', 1);
|
||||
token_fref_o.meta = { label };
|
||||
token_fref_o.level = state.level++;
|
||||
state.tokens.push(token_fref_o);
|
||||
|
||||
const oldBMark = state.bMarks[startLine];
|
||||
const oldTShift = state.tShift[startLine];
|
||||
const oldSCount = state.sCount[startLine];
|
||||
const oldParentType = state.parentType;
|
||||
|
||||
const posAfterColon = pos;
|
||||
const initial = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
|
||||
let offset = initial;
|
||||
|
||||
while (pos < max) {
|
||||
const ch = state.src.charCodeAt(pos);
|
||||
|
||||
if (isSpace(ch)) {
|
||||
if (ch === 0x09) {
|
||||
offset += 4 - offset % 4;
|
||||
} else {
|
||||
offset++;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
state.tShift[startLine] = pos - posAfterColon;
|
||||
state.sCount[startLine] = offset - initial;
|
||||
|
||||
state.bMarks[startLine] = posAfterColon;
|
||||
state.blkIndent += 4;
|
||||
state.parentType = 'footnote' as any;
|
||||
|
||||
if (state.sCount[startLine] < state.blkIndent) {
|
||||
state.sCount[startLine] += state.blkIndent;
|
||||
}
|
||||
|
||||
state.md.block.tokenize(state, startLine, endLine);
|
||||
|
||||
state.parentType = oldParentType;
|
||||
state.blkIndent -= 4;
|
||||
state.tShift[startLine] = oldTShift;
|
||||
state.sCount[startLine] = oldSCount;
|
||||
state.bMarks[startLine] = oldBMark;
|
||||
|
||||
const token_fref_c = new state.Token('footnote_reference_close', '', -1);
|
||||
token_fref_c.level = --state.level;
|
||||
state.tokens.push(token_fref_c);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Process inline footnotes (^[...])
|
||||
function footnote_inline(state: StateInline, silent: boolean): boolean {
|
||||
const max = state.posMax;
|
||||
const start = state.pos;
|
||||
|
||||
if (start + 2 >= max) return false;
|
||||
if (state.src.charCodeAt(start) !== 0x5E/* ^ */) return false;
|
||||
if (state.src.charCodeAt(start + 1) !== 0x5B/* [ */) return false;
|
||||
|
||||
const labelStart = start + 2;
|
||||
const labelEnd = parseLinkLabel(state, start + 1);
|
||||
|
||||
// parser failed to find ']', so it's not a valid note
|
||||
if (labelEnd < 0) return false;
|
||||
|
||||
// We found the end of the link, and know for a fact it's a valid link;
|
||||
// so all that's left to do is to call tokenizer.
|
||||
//
|
||||
if (!silent) {
|
||||
const env = state.env as FootnoteEnv;
|
||||
if (!env.footnotes) env.footnotes = {};
|
||||
if (!env.footnotes.list) env.footnotes.list = [];
|
||||
const footnoteId = env.footnotes.list.length;
|
||||
const tokens: Token[] = [];
|
||||
|
||||
state.md.inline.parse(
|
||||
state.src.slice(labelStart, labelEnd),
|
||||
state.md,
|
||||
state.env,
|
||||
tokens
|
||||
);
|
||||
|
||||
const token = state.push('footnote_ref', '', 0);
|
||||
token.meta = { id: footnoteId };
|
||||
|
||||
env.footnotes.list[footnoteId] = {
|
||||
content: state.src.slice(labelStart, labelEnd),
|
||||
tokens,
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
|
||||
state.pos = labelEnd + 1;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Process footnote references ([^...])
|
||||
function footnote_ref(state: StateInline, silent: boolean): boolean {
|
||||
const max = state.posMax;
|
||||
const start = state.pos;
|
||||
|
||||
// should be at least 4 chars - "[^x]"
|
||||
if (start + 3 > max) return false;
|
||||
|
||||
const env = state.env as FootnoteEnv;
|
||||
if (!env.footnotes || !env.footnotes.refs) return false;
|
||||
if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false;
|
||||
if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) return false;
|
||||
|
||||
let pos: number;
|
||||
|
||||
for (pos = start + 2; pos < max; pos++) {
|
||||
if (state.src.charCodeAt(pos) === 0x20) return false;
|
||||
if (state.src.charCodeAt(pos) === 0x0A) return false;
|
||||
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos === start + 2) return false; // no empty footnote labels
|
||||
if (pos >= max) return false;
|
||||
pos++;
|
||||
|
||||
const label = state.src.slice(start + 2, pos - 1);
|
||||
if (typeof env.footnotes.refs[`:${label}`] === 'undefined') return false;
|
||||
|
||||
if (!silent) {
|
||||
if (!env.footnotes.list) env.footnotes.list = [];
|
||||
|
||||
let footnoteId: number;
|
||||
|
||||
if (env.footnotes.refs[`:${label}`] < 0) {
|
||||
footnoteId = env.footnotes.list.length;
|
||||
env.footnotes.list[footnoteId] = { label, count: 0 };
|
||||
env.footnotes.refs[`:${label}`] = footnoteId;
|
||||
} else {
|
||||
footnoteId = env.footnotes.refs[`:${label}`];
|
||||
}
|
||||
|
||||
const footnoteSubId = env.footnotes.list[footnoteId].count;
|
||||
env.footnotes.list[footnoteId].count++;
|
||||
|
||||
const token = state.push('footnote_ref', '', 0);
|
||||
token.meta = { id: footnoteId, subId: footnoteSubId, label };
|
||||
}
|
||||
|
||||
state.pos = pos;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Glue footnote tokens to end of token stream
|
||||
function footnote_tail(state: StateCore): void {
|
||||
let tokens: Token[] | null = null;
|
||||
let current: Token[];
|
||||
let currentLabel: string;
|
||||
let insideRef = false;
|
||||
const refTokens: { [key: string]: Token[] } = {};
|
||||
|
||||
const env = state.env as FootnoteEnv;
|
||||
if (!env.footnotes) { return; }
|
||||
|
||||
state.tokens = state.tokens.filter(function (tok) {
|
||||
if (tok.type === 'footnote_reference_open') {
|
||||
insideRef = true;
|
||||
current = [];
|
||||
currentLabel = tok.meta.label;
|
||||
return false;
|
||||
}
|
||||
if (tok.type === 'footnote_reference_close') {
|
||||
insideRef = false;
|
||||
// prepend ':' to avoid conflict with Object.prototype members
|
||||
refTokens[':' + currentLabel] = current;
|
||||
return false;
|
||||
}
|
||||
if (insideRef) { current.push(tok); }
|
||||
return !insideRef;
|
||||
});
|
||||
|
||||
if (!env.footnotes.list) { return; }
|
||||
const list = env.footnotes.list;
|
||||
|
||||
state.tokens.push(new state.Token('footnote_block_open', '', 1));
|
||||
|
||||
for (let i = 0, l = list.length; i < l; i++) {
|
||||
const token_fo = new state.Token('footnote_open', '', 1);
|
||||
token_fo.meta = { id: i, label: list[i].label };
|
||||
state.tokens.push(token_fo);
|
||||
|
||||
if (list[i].tokens) {
|
||||
tokens = [];
|
||||
|
||||
const token_po = new state.Token('paragraph_open', 'p', 1);
|
||||
token_po.block = true;
|
||||
tokens.push(token_po);
|
||||
|
||||
const token_i = new state.Token('inline', '', 0);
|
||||
token_i.children = list[i].tokens || null;
|
||||
token_i.content = list[i].content || '';
|
||||
tokens.push(token_i);
|
||||
|
||||
const token_pc = new state.Token('paragraph_close', 'p', -1);
|
||||
token_pc.block = true;
|
||||
tokens.push(token_pc);
|
||||
} else if (list[i].label) {
|
||||
tokens = refTokens[`:${list[i].label}`] || null;
|
||||
}
|
||||
|
||||
if (tokens) state.tokens = state.tokens.concat(tokens);
|
||||
|
||||
let lastParagraph: Token | null;
|
||||
|
||||
if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
|
||||
lastParagraph = state.tokens.pop()!;
|
||||
} else {
|
||||
lastParagraph = null;
|
||||
}
|
||||
|
||||
const t = list[i].count > 0 ? list[i].count : 1;
|
||||
for (let j = 0; j < t; j++) {
|
||||
const token_a = new state.Token('footnote_anchor', '', 0);
|
||||
token_a.meta = { id: i, subId: j, label: list[i].label };
|
||||
state.tokens.push(token_a);
|
||||
}
|
||||
|
||||
if (lastParagraph) {
|
||||
state.tokens.push(lastParagraph);
|
||||
}
|
||||
|
||||
state.tokens.push(new state.Token('footnote_close', '', -1));
|
||||
}
|
||||
|
||||
state.tokens.push(new state.Token('footnote_block_close', '', -1));
|
||||
}
|
||||
|
||||
md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: ['paragraph', 'reference'] });
|
||||
md.inline.ruler.after('image', 'footnote_inline', footnote_inline);
|
||||
md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref);
|
||||
md.core.ruler.after('inline', 'footnote_tail', footnote_tail);
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import MarkdownIt, { StateInline, Token } from 'markdown-it';
|
||||
|
||||
/**
|
||||
* 分隔符接口定义
|
||||
*/
|
||||
interface Delimiter {
|
||||
marker: number;
|
||||
length: number;
|
||||
jump: number;
|
||||
token: number;
|
||||
end: number;
|
||||
open: boolean;
|
||||
close: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描结果接口定义
|
||||
*/
|
||||
interface ScanResult {
|
||||
can_open: boolean;
|
||||
can_close: boolean;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 元数据接口定义
|
||||
*/
|
||||
interface TokenMeta {
|
||||
delimiters?: Delimiter[];
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-ins 插件
|
||||
* 用于支持插入文本语法 ++text++
|
||||
*/
|
||||
export default function ins_plugin(md: MarkdownIt): void {
|
||||
// Insert each marker as a separate text token, and add it to delimiter list
|
||||
//
|
||||
function tokenize(state: StateInline, silent: boolean): boolean {
|
||||
const start = state.pos;
|
||||
const marker = state.src.charCodeAt(start);
|
||||
|
||||
if (silent) { return false; }
|
||||
|
||||
if (marker !== 0x2B/* + */) { return false; }
|
||||
|
||||
const scanned = state.scanDelims(state.pos, true) as ScanResult;
|
||||
let len = scanned.length;
|
||||
const ch = String.fromCharCode(marker);
|
||||
|
||||
if (len < 2) { return false; }
|
||||
|
||||
if (len % 2) {
|
||||
const token: Token = state.push('text', '', 0);
|
||||
token.content = ch;
|
||||
len--;
|
||||
}
|
||||
|
||||
for (let i = 0; i < len; i += 2) {
|
||||
const token: Token = state.push('text', '', 0);
|
||||
token.content = ch + ch;
|
||||
|
||||
if (!scanned.can_open && !scanned.can_close) { continue; }
|
||||
|
||||
state.delimiters.push({
|
||||
marker,
|
||||
length: 0, // disable "rule of 3" length checks meant for emphasis
|
||||
jump: i / 2, // 1 delimiter = 2 characters
|
||||
token: state.tokens.length - 1,
|
||||
end: -1,
|
||||
open: scanned.can_open,
|
||||
close: scanned.can_close
|
||||
} as Delimiter);
|
||||
}
|
||||
|
||||
state.pos += scanned.length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Walk through delimiter list and replace text tokens with tags
|
||||
//
|
||||
function postProcess(state: StateInline, delimiters: Delimiter[]): void {
|
||||
let token: Token;
|
||||
const loneMarkers: number[] = [];
|
||||
const max = delimiters.length;
|
||||
|
||||
for (let i = 0; i < max; i++) {
|
||||
const startDelim = delimiters[i];
|
||||
|
||||
if (startDelim.marker !== 0x2B/* + */) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startDelim.end === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const endDelim = delimiters[startDelim.end];
|
||||
|
||||
token = state.tokens[startDelim.token];
|
||||
token.type = 'ins_open';
|
||||
token.tag = 'ins';
|
||||
token.nesting = 1;
|
||||
token.markup = '++';
|
||||
token.content = '';
|
||||
|
||||
token = state.tokens[endDelim.token];
|
||||
token.type = 'ins_close';
|
||||
token.tag = 'ins';
|
||||
token.nesting = -1;
|
||||
token.markup = '++';
|
||||
token.content = '';
|
||||
|
||||
if (state.tokens[endDelim.token - 1].type === 'text' &&
|
||||
state.tokens[endDelim.token - 1].content === '+') {
|
||||
loneMarkers.push(endDelim.token - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If a marker sequence has an odd number of characters, it's splitted
|
||||
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
|
||||
// start of the sequence.
|
||||
//
|
||||
// So, we have to move all those markers after subsequent s_close tags.
|
||||
//
|
||||
while (loneMarkers.length) {
|
||||
const i = loneMarkers.pop()!;
|
||||
let j = i + 1;
|
||||
|
||||
while (j < state.tokens.length && state.tokens[j].type === 'ins_close') {
|
||||
j++;
|
||||
}
|
||||
|
||||
j--;
|
||||
|
||||
if (i !== j) {
|
||||
token = state.tokens[j];
|
||||
state.tokens[j] = state.tokens[i];
|
||||
state.tokens[i] = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
md.inline.ruler.before('emphasis', 'ins', tokenize);
|
||||
md.inline.ruler2.before('emphasis', 'ins', function (state: StateInline): boolean {
|
||||
const tokens_meta = state.tokens_meta as TokenMeta[];
|
||||
const max = (state.tokens_meta || []).length;
|
||||
|
||||
postProcess(state, state.delimiters as Delimiter[]);
|
||||
|
||||
for (let curr = 0; curr < max; curr++) {
|
||||
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
|
||||
postProcess(state, tokens_meta[curr].delimiters!);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import MarkdownIt, {StateInline, Token} from 'markdown-it';
|
||||
|
||||
/**
|
||||
* 分隔符接口定义
|
||||
*/
|
||||
interface Delimiter {
|
||||
marker: number;
|
||||
length: number;
|
||||
jump: number;
|
||||
token: number;
|
||||
end: number;
|
||||
open: boolean;
|
||||
close: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描结果接口定义
|
||||
*/
|
||||
interface ScanResult {
|
||||
can_open: boolean;
|
||||
can_close: boolean;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 元数据接口定义
|
||||
*/
|
||||
interface TokenMeta {
|
||||
delimiters?: Delimiter[];
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-mark 插件
|
||||
* 用于支持 ==标记文本== 语法
|
||||
*/
|
||||
export default function markPlugin(md: MarkdownIt): void {
|
||||
// Insert each marker as a separate text token, and add it to delimiter list
|
||||
//
|
||||
function tokenize(state: StateInline, silent: boolean): boolean {
|
||||
const start = state.pos;
|
||||
const marker = state.src.charCodeAt(start);
|
||||
|
||||
if (silent) { return false; }
|
||||
|
||||
if (marker !== 0x3D/* = */) { return false; }
|
||||
|
||||
const scanned = state.scanDelims(state.pos, true) as ScanResult;
|
||||
let len = scanned.length;
|
||||
const ch = String.fromCharCode(marker);
|
||||
|
||||
if (len < 2) { return false; }
|
||||
|
||||
if (len % 2) {
|
||||
const token: Token = state.push('text', '', 0);
|
||||
token.content = ch;
|
||||
len--;
|
||||
}
|
||||
|
||||
for (let i = 0; i < len; i += 2) {
|
||||
const token: Token = state.push('text', '', 0);
|
||||
token.content = ch + ch;
|
||||
|
||||
if (!scanned.can_open && !scanned.can_close) { continue; }
|
||||
|
||||
state.delimiters.push({
|
||||
marker,
|
||||
length: 0, // disable "rule of 3" length checks meant for emphasis
|
||||
jump: i / 2, // 1 delimiter = 2 characters
|
||||
token: state.tokens.length - 1,
|
||||
end: -1,
|
||||
open: scanned.can_open,
|
||||
close: scanned.can_close
|
||||
} as Delimiter);
|
||||
}
|
||||
|
||||
state.pos += scanned.length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Walk through delimiter list and replace text tokens with tags
|
||||
//
|
||||
function postProcess(state: StateInline, delimiters: Delimiter[]): void {
|
||||
const loneMarkers: number[] = [];
|
||||
const max = delimiters.length;
|
||||
|
||||
for (let i = 0; i < max; i++) {
|
||||
const startDelim = delimiters[i];
|
||||
|
||||
if (startDelim.marker !== 0x3D/* = */) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startDelim.end === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const endDelim = delimiters[startDelim.end];
|
||||
|
||||
const token_o = state.tokens[startDelim.token];
|
||||
token_o.type = 'mark_open';
|
||||
token_o.tag = 'mark';
|
||||
token_o.nesting = 1;
|
||||
token_o.markup = '==';
|
||||
token_o.content = '';
|
||||
|
||||
const token_c = state.tokens[endDelim.token];
|
||||
token_c.type = 'mark_close';
|
||||
token_c.tag = 'mark';
|
||||
token_c.nesting = -1;
|
||||
token_c.markup = '==';
|
||||
token_c.content = '';
|
||||
|
||||
if (state.tokens[endDelim.token - 1].type === 'text' &&
|
||||
state.tokens[endDelim.token - 1].content === '=') {
|
||||
loneMarkers.push(endDelim.token - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If a marker sequence has an odd number of characters, it's splitted
|
||||
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
|
||||
// start of the sequence.
|
||||
//
|
||||
// So, we have to move all those markers after subsequent s_close tags.
|
||||
//
|
||||
while (loneMarkers.length) {
|
||||
const i = loneMarkers.pop()!;
|
||||
let j = i + 1;
|
||||
|
||||
while (j < state.tokens.length && state.tokens[j].type === 'mark_close') {
|
||||
j++;
|
||||
}
|
||||
|
||||
j--;
|
||||
|
||||
if (i !== j) {
|
||||
const token = state.tokens[j];
|
||||
state.tokens[j] = state.tokens[i];
|
||||
state.tokens[i] = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
md.inline.ruler.before('emphasis', 'mark', tokenize);
|
||||
md.inline.ruler2.before('emphasis', 'mark', function (state: StateInline): boolean {
|
||||
let curr: number;
|
||||
const tokens_meta = state.tokens_meta as TokenMeta[];
|
||||
const max = (state.tokens_meta || []).length;
|
||||
|
||||
postProcess(state, state.delimiters as Delimiter[]);
|
||||
|
||||
for (curr = 0; curr < max; curr++) {
|
||||
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
|
||||
postProcess(state, tokens_meta[curr].delimiters!);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import mermaid from "mermaid";
|
||||
import {genUid, hashCode, sleep} from "./utils";
|
||||
|
||||
const mermaidCache = new Map<string, HTMLElement>();
|
||||
|
||||
// 缓存计数器,用于清除缓存
|
||||
const mermaidCacheCount = new Map<string, number>();
|
||||
let count = 0;
|
||||
|
||||
|
||||
let countTmo = setTimeout(() => undefined, 0);
|
||||
const addCount = () => {
|
||||
clearTimeout(countTmo);
|
||||
countTmo = setTimeout(() => {
|
||||
count++;
|
||||
clearCache();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const clearCache = () => {
|
||||
for (const key of mermaidCacheCount.keys()) {
|
||||
const value = mermaidCacheCount.get(key)!;
|
||||
if (value + 3 < count) {
|
||||
mermaidCache.delete(key);
|
||||
mermaidCacheCount.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染 mermaid
|
||||
* @param code mermaid 代码
|
||||
* @param targetId 目标 id
|
||||
* @param count 计数器
|
||||
*/
|
||||
const renderMermaid = async (code: string, targetId: string, count: number) => {
|
||||
let limit = 100;
|
||||
while (limit-- > 0) {
|
||||
const container = document.getElementById(targetId);
|
||||
if (!container) {
|
||||
await sleep(100);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const {svg} = await mermaid.render("mermaid-svg-" + genUid(), code, container);
|
||||
container.innerHTML = svg;
|
||||
mermaidCache.set(targetId, container);
|
||||
mermaidCacheCount.set(targetId, count);
|
||||
} catch (e) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MermaidItOptions {
|
||||
theme?: "default" | "dark" | "forest" | "neutral" | "base";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 mermaid 主题
|
||||
*/
|
||||
export const updateMermaidTheme = (theme: "default" | "dark" | "forest" | "neutral" | "base") => {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: theme
|
||||
});
|
||||
// 清空缓存,强制重新渲染
|
||||
mermaidCache.clear();
|
||||
mermaidCacheCount.clear();
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* mermaid 插件
|
||||
* @param md markdown-it
|
||||
* @param options 配置选项
|
||||
* @constructor MermaidIt
|
||||
*/
|
||||
export const MermaidIt = function (md: any, options?: MermaidItOptions): void {
|
||||
const theme = options?.theme || "default";
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: theme
|
||||
});
|
||||
const defaultRenderer = md.renderer.rules.fence.bind(md.renderer.rules);
|
||||
md.renderer.rules.fence = (tokens: any, idx: any, options: any, env: any, self: any) => {
|
||||
addCount();
|
||||
const token = tokens[idx];
|
||||
const info = token.info.trim();
|
||||
if (info === "mermaid") {
|
||||
const containerId = "mermaid-container-" + hashCode(token.content);
|
||||
const container = document.createElement("div");
|
||||
container.id = containerId;
|
||||
if (mermaidCache.has(containerId)) {
|
||||
container.innerHTML = mermaidCache.get(containerId)!.innerHTML;
|
||||
mermaidCacheCount.set(containerId, count);
|
||||
} else {
|
||||
renderMermaid(token.content, containerId, count).then();
|
||||
}
|
||||
return container.outerHTML;
|
||||
}
|
||||
// 使用默认的渲染规则
|
||||
return defaultRenderer(tokens, idx, options, env, self);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* uuid 生成函数
|
||||
* @param split 分隔符
|
||||
*/
|
||||
export const genUid = (split = "") => {
|
||||
return uuidv4().split("-").join(split);
|
||||
};
|
||||
|
||||
/**
|
||||
* 一个简易的sleep函数
|
||||
*/
|
||||
export const sleep = async (ms: number) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算字符串的hash值
|
||||
* 返回一个数字
|
||||
* @param str
|
||||
*/
|
||||
export const hashCode = (str: string) => {
|
||||
let hash = 0;
|
||||
if (str.length === 0) return hash;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
/**
|
||||
* 一个简易的阻塞函数
|
||||
*/
|
||||
export const awaitFor = async (cb: () => boolean, timeout = 0, errText = "超时暂停阻塞") => {
|
||||
const start = Date.now();
|
||||
while (true) {
|
||||
if (cb()) return true;
|
||||
if (timeout && Date.now() - start > timeout) {
|
||||
console.error("阻塞超时: " + errText);
|
||||
return false;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
// Process ~subscript~
|
||||
|
||||
import MarkdownIt, { StateInline, Token } from 'markdown-it';
|
||||
|
||||
// same as UNESCAPE_MD_RE plus a space
|
||||
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
|
||||
|
||||
function subscript(state: StateInline, silent: boolean): boolean {
|
||||
const max = state.posMax;
|
||||
const start = state.pos;
|
||||
|
||||
if (state.src.charCodeAt(start) !== 0x7E/* ~ */) { return false; }
|
||||
if (silent) { return false; } // don't run any pairs in validation mode
|
||||
if (start + 2 >= max) { return false; }
|
||||
|
||||
state.pos = start + 1;
|
||||
let found = false;
|
||||
|
||||
while (state.pos < max) {
|
||||
if (state.src.charCodeAt(state.pos) === 0x7E/* ~ */) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
state.md.inline.skipToken(state);
|
||||
}
|
||||
|
||||
if (!found || start + 1 === state.pos) {
|
||||
state.pos = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = state.src.slice(start + 1, state.pos);
|
||||
|
||||
// don't allow unescaped spaces/newlines inside
|
||||
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
|
||||
state.pos = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
// found!
|
||||
state.posMax = state.pos;
|
||||
state.pos = start + 1;
|
||||
|
||||
// Earlier we checked !silent, but this implementation does not need it
|
||||
const token_so: Token = state.push('sub_open', 'sub', 1);
|
||||
token_so.markup = '~';
|
||||
|
||||
const token_t: Token = state.push('text', '', 0);
|
||||
token_t.content = content.replace(UNESCAPE_RE, '$1');
|
||||
|
||||
const token_sc: Token = state.push('sub_close', 'sub', -1);
|
||||
token_sc.markup = '~';
|
||||
|
||||
state.pos = state.posMax + 1;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-sub 插件
|
||||
* 用于支持下标语法 ~text~
|
||||
*/
|
||||
export default function sub_plugin(md: MarkdownIt): void {
|
||||
md.inline.ruler.after('emphasis', 'sub', subscript);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Process ^superscript^
|
||||
|
||||
import MarkdownIt, { StateInline, Token } from 'markdown-it';
|
||||
|
||||
// same as UNESCAPE_MD_RE plus a space
|
||||
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
|
||||
|
||||
function superscript(state: StateInline, silent: boolean): boolean {
|
||||
const max = state.posMax;
|
||||
const start = state.pos;
|
||||
|
||||
if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; }
|
||||
if (silent) { return false; } // don't run any pairs in validation mode
|
||||
if (start + 2 >= max) { return false; }
|
||||
|
||||
state.pos = start + 1;
|
||||
let found = false;
|
||||
|
||||
while (state.pos < max) {
|
||||
if (state.src.charCodeAt(state.pos) === 0x5E/* ^ */) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
state.md.inline.skipToken(state);
|
||||
}
|
||||
|
||||
if (!found || start + 1 === state.pos) {
|
||||
state.pos = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = state.src.slice(start + 1, state.pos);
|
||||
|
||||
// don't allow unescaped spaces/newlines inside
|
||||
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
|
||||
state.pos = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
// found!
|
||||
state.posMax = state.pos;
|
||||
state.pos = start + 1;
|
||||
|
||||
// Earlier we checked !silent, but this implementation does not need it
|
||||
const token_so: Token = state.push('sup_open', 'sup', 1);
|
||||
token_so.markup = '^';
|
||||
|
||||
const token_t: Token = state.push('text', '', 0);
|
||||
token_t.content = content.replace(UNESCAPE_RE, '$1');
|
||||
|
||||
const token_sc: Token = state.push('sup_close', 'sup', -1);
|
||||
token_sc.markup = '^';
|
||||
|
||||
state.pos = state.posMax + 1;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-sup 插件
|
||||
* 用于支持上标语法 ^text^
|
||||
*/
|
||||
export default function sup_plugin(md: MarkdownIt): void {
|
||||
md.inline.ruler.after('emphasis', 'sup', superscript);
|
||||
}
|
||||
@@ -35,7 +35,6 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
// Helper methods
|
||||
public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[];
|
||||
public visitSingle: (ctx: TomlContext) => Doc | string;
|
||||
public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -57,26 +56,38 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
const singleElement = getSingle(ctx);
|
||||
return this.visit(singleElement);
|
||||
};
|
||||
}
|
||||
|
||||
// Store reference to inherited visit method and override it
|
||||
const originalVisit = Object.getPrototypeOf(this).visit?.bind(this);
|
||||
this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => {
|
||||
/**
|
||||
* Override visit method to handle TOML CST nodes
|
||||
* Accepts both single node and array of nodes as per base class signature
|
||||
*/
|
||||
visit(cstNode: any, param?: any): any {
|
||||
// Handle array of nodes
|
||||
if (Array.isArray(cstNode)) {
|
||||
return cstNode.map(node => this.visit(node, param));
|
||||
}
|
||||
|
||||
const ctx = cstNode;
|
||||
if (!ctx) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 确保节点有name属性才调用基类方法
|
||||
if (ctx.name) {
|
||||
// Try to use the inherited visit method first
|
||||
const originalVisit = super.visit;
|
||||
if (originalVisit) {
|
||||
try {
|
||||
return originalVisit(ctx, inParam);
|
||||
return originalVisit.call(this, ctx, param);
|
||||
} catch (error) {
|
||||
console.warn('Original visit method failed:', error);
|
||||
// Fallback to manual dispatch
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: manually dispatch based on node name/type
|
||||
const methodName = ctx.name;
|
||||
if (methodName && typeof (this as any)[methodName] === 'function') {
|
||||
if (typeof (this as any)[methodName] === 'function') {
|
||||
const visitMethod = (this as any)[methodName];
|
||||
try {
|
||||
if (ctx.children) {
|
||||
@@ -88,16 +99,16 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
console.warn(`Visit method ${methodName} failed:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: return image if available
|
||||
return ctx.image || '';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit the root TOML document
|
||||
*/
|
||||
toml(ctx: TomlDocument): Doc {
|
||||
toml(ctx: any): Doc {
|
||||
// Handle empty toml document
|
||||
if (!ctx.expression) {
|
||||
return [line];
|
||||
@@ -164,7 +175,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
/**
|
||||
* Visit an expression (keyval, table, or comment)
|
||||
*/
|
||||
expression(ctx: TomlExpression): Doc | string {
|
||||
expression(ctx: any): Doc | string {
|
||||
if (ctx.keyval) {
|
||||
let keyValDoc = this.visit(ctx.keyval[0]);
|
||||
if (ctx.Comment) {
|
||||
@@ -189,7 +200,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||
/**
|
||||
* Visit a key-value pair
|
||||
*/
|
||||
keyval(ctx: TomlKeyVal): Doc {
|
||||
keyval(ctx: any): Doc {
|
||||
const keyDoc = this.visit(ctx.key[0]);
|
||||
const valueDoc = this.visit(ctx.val[0]);
|
||||
return [keyDoc, ' = ', valueDoc];
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
/**
|
||||
* 操作信息接口
|
||||
*/
|
||||
interface OperationInfo {
|
||||
controller: AbortController;
|
||||
createdAt: number;
|
||||
timeout?: number;
|
||||
timeoutId?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步操作管理器
|
||||
* 用于管理异步操作的竞态条件,确保只有最新的操作有效
|
||||
* 支持操作超时和自动清理机制
|
||||
*
|
||||
* @template T 操作上下文的类型
|
||||
*/
|
||||
export class AsyncManager<T = any> {
|
||||
private operationSequence = 0;
|
||||
private pendingOperations = new Map<number, OperationInfo>();
|
||||
private currentContext: T | null = null;
|
||||
private defaultTimeout: number;
|
||||
|
||||
/**
|
||||
* 创建异步操作管理器
|
||||
*
|
||||
* @param defaultTimeout 默认超时时间(毫秒),0表示不设置超时
|
||||
*/
|
||||
constructor(defaultTimeout: number = 0) {
|
||||
this.defaultTimeout = defaultTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新的操作ID
|
||||
*
|
||||
* @returns 新的操作ID
|
||||
*/
|
||||
getNextOperationId(): number {
|
||||
return ++this.operationSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始新的操作
|
||||
*
|
||||
* @param context 操作上下文
|
||||
* @param options 操作选项
|
||||
* @returns 操作ID和AbortController
|
||||
*/
|
||||
startOperation(
|
||||
context: T,
|
||||
options?: {
|
||||
excludeId?: number;
|
||||
timeout?: number;
|
||||
}
|
||||
): { operationId: number; abortController: AbortController } {
|
||||
const operationId = this.getNextOperationId();
|
||||
const abortController = new AbortController();
|
||||
const timeout = options?.timeout ?? this.defaultTimeout;
|
||||
|
||||
// 取消之前的操作
|
||||
this.cancelPreviousOperations(options?.excludeId);
|
||||
|
||||
// 创建操作信息
|
||||
const operationInfo: OperationInfo = {
|
||||
controller: abortController,
|
||||
createdAt: Date.now(),
|
||||
timeout: timeout > 0 ? timeout : undefined
|
||||
};
|
||||
|
||||
// 设置超时处理
|
||||
if (timeout > 0) {
|
||||
operationInfo.timeoutId = setTimeout(() => {
|
||||
this.cancelOperation(operationId, 'timeout');
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
// 设置当前上下文和操作
|
||||
this.currentContext = context;
|
||||
this.pendingOperations.set(operationId, operationInfo);
|
||||
|
||||
return { operationId, abortController };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查操作是否仍然有效
|
||||
*
|
||||
* @param operationId 操作ID
|
||||
* @param context 操作上下文
|
||||
* @returns 操作是否有效
|
||||
*/
|
||||
isOperationValid(operationId: number, context?: T): boolean {
|
||||
const operationInfo = this.pendingOperations.get(operationId);
|
||||
const contextValid = context === undefined || this.currentContext === context;
|
||||
|
||||
return (
|
||||
operationInfo !== undefined &&
|
||||
!operationInfo.controller.signal.aborted &&
|
||||
contextValid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成操作
|
||||
*
|
||||
* @param operationId 操作ID
|
||||
*/
|
||||
completeOperation(operationId: number): void {
|
||||
const operationInfo = this.pendingOperations.get(operationId);
|
||||
if (operationInfo) {
|
||||
// 清理超时定时器
|
||||
if (operationInfo.timeoutId) {
|
||||
clearTimeout(operationInfo.timeoutId);
|
||||
}
|
||||
this.pendingOperations.delete(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消指定操作
|
||||
*
|
||||
* @param operationId 操作ID
|
||||
* @param reason 取消原因
|
||||
*/
|
||||
cancelOperation(operationId: number, reason?: string): void {
|
||||
const operationInfo = this.pendingOperations.get(operationId);
|
||||
if (operationInfo) {
|
||||
// 清理超时定时器
|
||||
if (operationInfo.timeoutId) {
|
||||
clearTimeout(operationInfo.timeoutId);
|
||||
}
|
||||
// 取消操作
|
||||
operationInfo.controller.abort(reason);
|
||||
this.pendingOperations.delete(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消之前的操作(修复并发bug)
|
||||
*
|
||||
* @param excludeId 要排除的操作ID(不取消该操作)
|
||||
*/
|
||||
cancelPreviousOperations(excludeId?: number): void {
|
||||
// 创建要取消的操作ID数组,避免在遍历时修改Map
|
||||
const operationIdsToCancel: number[] = [];
|
||||
|
||||
for (const [operationId] of this.pendingOperations) {
|
||||
if (excludeId === undefined || operationId !== excludeId) {
|
||||
operationIdsToCancel.push(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量取消操作
|
||||
for (const operationId of operationIdsToCancel) {
|
||||
this.cancelOperation(operationId, 'superseded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有操作
|
||||
*/
|
||||
cancelAllOperations(): void {
|
||||
// 创建要取消的操作ID数组,避免在遍历时修改Map
|
||||
const operationIdsToCancel = Array.from(this.pendingOperations.keys());
|
||||
|
||||
// 批量取消操作
|
||||
for (const operationId of operationIdsToCancel) {
|
||||
this.cancelOperation(operationId, 'cancelled');
|
||||
}
|
||||
this.currentContext = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期操作(手动清理超时操作)
|
||||
*
|
||||
* @param maxAge 最大存活时间(毫秒)
|
||||
* @returns 清理的操作数量
|
||||
*/
|
||||
cleanupExpiredOperations(maxAge: number): number {
|
||||
const now = Date.now();
|
||||
const expiredOperationIds: number[] = [];
|
||||
|
||||
for (const [operationId, operationInfo] of this.pendingOperations) {
|
||||
if (now - operationInfo.createdAt > maxAge) {
|
||||
expiredOperationIds.push(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量取消过期操作
|
||||
for (const operationId of expiredOperationIds) {
|
||||
this.cancelOperation(operationId, 'expired');
|
||||
}
|
||||
|
||||
return expiredOperationIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作统计信息
|
||||
*
|
||||
* @returns 操作统计信息
|
||||
*/
|
||||
getOperationStats(): {
|
||||
total: number;
|
||||
withTimeout: number;
|
||||
averageAge: number;
|
||||
oldestAge: number;
|
||||
} {
|
||||
const now = Date.now();
|
||||
let withTimeout = 0;
|
||||
let totalAge = 0;
|
||||
let oldestAge = 0;
|
||||
|
||||
for (const operationInfo of this.pendingOperations.values()) {
|
||||
const age = now - operationInfo.createdAt;
|
||||
totalAge += age;
|
||||
oldestAge = Math.max(oldestAge, age);
|
||||
|
||||
if (operationInfo.timeout) {
|
||||
withTimeout++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total: this.pendingOperations.size,
|
||||
withTimeout,
|
||||
averageAge: this.pendingOperations.size > 0 ? totalAge / this.pendingOperations.size : 0,
|
||||
oldestAge
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前上下文
|
||||
*
|
||||
* @returns 当前上下文
|
||||
*/
|
||||
getCurrentContext(): T | null {
|
||||
return this.currentContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前上下文
|
||||
*
|
||||
* @param context 新的上下文
|
||||
*/
|
||||
setCurrentContext(context: T | null): void {
|
||||
this.currentContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待处理操作数量
|
||||
*
|
||||
* @returns 待处理操作数量
|
||||
*/
|
||||
get pendingCount(): number {
|
||||
return this.pendingOperations.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的操作
|
||||
*
|
||||
* @returns 是否有待处理的操作
|
||||
*/
|
||||
hasPendingOperations(): boolean {
|
||||
return this.pendingOperations.size > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,7 @@
|
||||
import { LanguageType } from '@/../bindings/voidraft/internal/models/models';
|
||||
import type { SupportedLocaleType } from '@/common/constant/locales';
|
||||
|
||||
/**
|
||||
* 配置工具类
|
||||
*/
|
||||
export class ConfigUtils {
|
||||
/**
|
||||
* 将后端语言类型转换为前端语言代码
|
||||
*/
|
||||
static backendLanguageToFrontend(language: LanguageType): SupportedLocaleType {
|
||||
return language === LanguageType.LangZhCN ? 'zh-CN' : 'en-US';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端语言代码转换为后端语言类型
|
||||
*/
|
||||
static frontendLanguageToBackend(locale: SupportedLocaleType): LanguageType {
|
||||
return locale === 'zh-CN' ? LanguageType.LangZhCN : LanguageType.LangEnUS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数值是否在指定范围内
|
||||
@@ -26,17 +10,4 @@ export class ConfigUtils {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置值是否有效
|
||||
*/
|
||||
static isValidConfigValue<T>(value: T, validValues: readonly T[]): boolean {
|
||||
return validValues.includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置的默认值
|
||||
*/
|
||||
static getDefaultValue<T>(key: string, defaults: Record<string, { default: T }>): T {
|
||||
return defaults[key]?.default;
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
/**
|
||||
* DOM Diff 算法单元测试
|
||||
*/
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { morphNode, morphHTML, morphWithKeys } from './domDiff';
|
||||
|
||||
describe('DOM Diff Algorithm', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
describe('morphNode - 基础功能', () => {
|
||||
test('应该更新文本节点内容', () => {
|
||||
const fromNode = document.createTextNode('Hello');
|
||||
const toNode = document.createTextNode('World');
|
||||
container.appendChild(fromNode);
|
||||
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(fromNode.nodeValue).toBe('World');
|
||||
});
|
||||
|
||||
test('应该保持相同的文本节点不变', () => {
|
||||
const fromNode = document.createTextNode('Hello');
|
||||
const toNode = document.createTextNode('Hello');
|
||||
container.appendChild(fromNode);
|
||||
|
||||
const originalNode = fromNode;
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(fromNode).toBe(originalNode);
|
||||
expect(fromNode.nodeValue).toBe('Hello');
|
||||
});
|
||||
|
||||
test('应该替换不同类型的节点', () => {
|
||||
const fromNode = document.createElement('span');
|
||||
fromNode.textContent = 'Hello';
|
||||
const toNode = document.createElement('div');
|
||||
toNode.textContent = 'World';
|
||||
container.appendChild(fromNode);
|
||||
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(container.firstChild?.nodeName).toBe('DIV');
|
||||
expect(container.firstChild?.textContent).toBe('World');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphNode - 属性更新', () => {
|
||||
test('应该添加新属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'test');
|
||||
toEl.setAttribute('id', 'myid');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('test');
|
||||
expect(fromEl.getAttribute('id')).toBe('myid');
|
||||
});
|
||||
|
||||
test('应该更新已存在的属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.setAttribute('class', 'old');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'new');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('new');
|
||||
});
|
||||
|
||||
test('应该删除不存在的属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.setAttribute('class', 'test');
|
||||
fromEl.setAttribute('id', 'myid');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'test');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('test');
|
||||
expect(fromEl.hasAttribute('id')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphNode - 子节点更新', () => {
|
||||
test('应该添加新子节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = '<li>1</li><li>2</li>';
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(3);
|
||||
expect(fromEl.children[2].textContent).toBe('3');
|
||||
});
|
||||
|
||||
test('应该删除多余的子节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = '<li>1</li><li>2</li>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(2);
|
||||
expect(fromEl.textContent).toBe('12');
|
||||
});
|
||||
|
||||
test('应该更新子节点内容', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.innerHTML = '<p>Old</p>';
|
||||
const toEl = document.createElement('div');
|
||||
toEl.innerHTML = '<p>New</p>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const originalP = fromEl.querySelector('p');
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
// 应该保持同一个 p 元素,只更新内容
|
||||
expect(fromEl.querySelector('p')).toBe(originalP);
|
||||
expect(fromEl.querySelector('p')?.textContent).toBe('New');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphHTML - HTML 字符串更新', () => {
|
||||
test('应该从 HTML 字符串更新元素', () => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = '<p>Old</p>';
|
||||
container.appendChild(element);
|
||||
|
||||
morphHTML(element, '<p>New</p>');
|
||||
|
||||
expect(element.innerHTML).toBe('<p>New</p>');
|
||||
});
|
||||
|
||||
test('应该处理复杂的 HTML 结构', () => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = '<h1>Title</h1><p>Paragraph</p>';
|
||||
container.appendChild(element);
|
||||
|
||||
morphHTML(element, '<h1>New Title</h1><p>New Paragraph</p><span>Extra</span>');
|
||||
|
||||
expect(element.children.length).toBe(3);
|
||||
expect(element.querySelector('h1')?.textContent).toBe('New Title');
|
||||
expect(element.querySelector('p')?.textContent).toBe('New Paragraph');
|
||||
expect(element.querySelector('span')?.textContent).toBe('Extra');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphWithKeys - 基于 key 的智能 diff', () => {
|
||||
test('应该保持相同 key 的节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A Updated</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const originalA = fromEl.querySelector('[data-key="a"]');
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.querySelector('[data-key="a"]')).toBe(originalA);
|
||||
expect(originalA?.textContent).toBe('A Updated');
|
||||
});
|
||||
|
||||
test('应该重新排序节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="c">C</li>
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
const keys = Array.from(fromEl.children).map(child => child.getAttribute('data-key'));
|
||||
expect(keys).toEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
test('应该添加新的 key 节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(3);
|
||||
expect(fromEl.querySelector('[data-key="c"]')?.textContent).toBe('C');
|
||||
});
|
||||
|
||||
test('应该删除不存在的 key 节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(2);
|
||||
expect(fromEl.querySelector('[data-key="b"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能测试', () => {
|
||||
test('应该高效处理大量节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `Item ${i}`;
|
||||
fromEl.appendChild(li);
|
||||
}
|
||||
|
||||
const toEl = document.createElement('ul');
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `Updated Item ${i}`;
|
||||
toEl.appendChild(li);
|
||||
}
|
||||
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const startTime = performance.now();
|
||||
morphNode(fromEl, toEl);
|
||||
const endTime = performance.now();
|
||||
|
||||
expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成
|
||||
expect(fromEl.children.length).toBe(1000);
|
||||
expect(fromEl.children[0].textContent).toBe('Updated Item 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
test('应该处理空节点', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
const toEl = document.createElement('div');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
expect(() => morphNode(fromEl, toEl)).not.toThrow();
|
||||
});
|
||||
|
||||
test('应该处理只有文本的节点', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.textContent = 'Hello';
|
||||
const toEl = document.createElement('div');
|
||||
toEl.textContent = 'World';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.textContent).toBe('World');
|
||||
});
|
||||
|
||||
test('应该处理嵌套的复杂结构', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.innerHTML = `
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<span>Text</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const toEl = document.createElement('div');
|
||||
toEl.innerHTML = `
|
||||
<div class="outer modified">
|
||||
<div class="inner">
|
||||
<span>Updated Text</span>
|
||||
<strong>New</strong>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.querySelector('.outer')?.classList.contains('modified')).toBe(true);
|
||||
expect(fromEl.querySelector('span')?.textContent).toBe('Updated Text');
|
||||
expect(fromEl.querySelector('strong')?.textContent).toBe('New');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/**
|
||||
* 轻量级 DOM Diff 算法实现
|
||||
* 基于 morphdom 思路,只更新变化的节点,保持未变化的节点不动
|
||||
*/
|
||||
|
||||
/**
|
||||
* 比较并更新两个 DOM 节点
|
||||
* @param fromNode 原节点
|
||||
* @param toNode 目标节点
|
||||
*/
|
||||
export function morphNode(fromNode: Node, toNode: Node): void {
|
||||
// 节点类型不同,直接替换
|
||||
if (fromNode.nodeType !== toNode.nodeType || fromNode.nodeName !== toNode.nodeName) {
|
||||
fromNode.parentNode?.replaceChild(toNode.cloneNode(true), fromNode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 文本节点:比较内容
|
||||
if (fromNode.nodeType === Node.TEXT_NODE) {
|
||||
if (fromNode.nodeValue !== toNode.nodeValue) {
|
||||
fromNode.nodeValue = toNode.nodeValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 元素节点:更新属性和子节点
|
||||
if (fromNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const fromEl = fromNode as Element;
|
||||
const toEl = toNode as Element;
|
||||
|
||||
// 更新属性
|
||||
morphAttributes(fromEl, toEl);
|
||||
|
||||
// 更新子节点
|
||||
morphChildren(fromEl, toEl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新元素属性
|
||||
*/
|
||||
function morphAttributes(fromEl: Element, toEl: Element): void {
|
||||
// 移除旧属性
|
||||
const fromAttrs = fromEl.attributes;
|
||||
for (let i = fromAttrs.length - 1; i >= 0; i--) {
|
||||
const attr = fromAttrs[i];
|
||||
if (!toEl.hasAttribute(attr.name)) {
|
||||
fromEl.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加/更新新属性
|
||||
const toAttrs = toEl.attributes;
|
||||
for (let i = 0; i < toAttrs.length; i++) {
|
||||
const attr = toAttrs[i];
|
||||
const fromValue = fromEl.getAttribute(attr.name);
|
||||
if (fromValue !== attr.value) {
|
||||
fromEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新子节点(核心 diff 算法)
|
||||
*/
|
||||
function morphChildren(fromEl: Element, toEl: Element): void {
|
||||
const fromChildren = Array.from(fromEl.childNodes);
|
||||
const toChildren = Array.from(toEl.childNodes);
|
||||
|
||||
const fromLen = fromChildren.length;
|
||||
const toLen = toChildren.length;
|
||||
const minLen = Math.min(fromLen, toLen);
|
||||
|
||||
// 1. 更新公共部分
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
morphNode(fromChildren[i], toChildren[i]);
|
||||
}
|
||||
|
||||
// 2. 移除多余的旧节点
|
||||
if (fromLen > toLen) {
|
||||
for (let i = fromLen - 1; i >= toLen; i--) {
|
||||
fromEl.removeChild(fromChildren[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 添加新节点
|
||||
if (toLen > fromLen) {
|
||||
for (let i = fromLen; i < toLen; i++) {
|
||||
fromEl.appendChild(toChildren[i].cloneNode(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化版:使用 key 进行更智能的 diff(可选)
|
||||
* 适用于有 data-key 属性的元素
|
||||
*/
|
||||
export function morphWithKeys(fromEl: Element, toEl: Element): void {
|
||||
const toChildren = Array.from(toEl.children) as Element[];
|
||||
|
||||
// 构建 from 的 key 映射
|
||||
const fromKeyMap = new Map<string, Element>();
|
||||
Array.from(fromEl.children).forEach((child) => {
|
||||
const key = child.getAttribute('data-key');
|
||||
if (key) {
|
||||
fromKeyMap.set(key, child);
|
||||
}
|
||||
});
|
||||
|
||||
const processedKeys = new Set<string>();
|
||||
|
||||
// 按照 toChildren 的顺序处理
|
||||
toChildren.forEach((toChild, toIndex) => {
|
||||
const key = toChild.getAttribute('data-key');
|
||||
if (!key) return;
|
||||
|
||||
processedKeys.add(key);
|
||||
const fromChild = fromKeyMap.get(key);
|
||||
|
||||
if (fromChild) {
|
||||
// 找到对应节点,更新内容
|
||||
morphNode(fromChild, toChild);
|
||||
|
||||
// 确保节点在正确的位置
|
||||
const currentNode = fromEl.children[toIndex];
|
||||
if (currentNode !== fromChild) {
|
||||
// 将 fromChild 移动到正确位置
|
||||
fromEl.insertBefore(fromChild, currentNode);
|
||||
}
|
||||
} else {
|
||||
// 新节点,插入到正确位置
|
||||
const currentNode = fromEl.children[toIndex];
|
||||
fromEl.insertBefore(toChild.cloneNode(true), currentNode || null);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除不再存在的节点(从后往前删除,避免索引问题)
|
||||
const childrenToRemove: Element[] = [];
|
||||
fromKeyMap.forEach((child, key) => {
|
||||
if (!processedKeys.has(key)) {
|
||||
childrenToRemove.push(child);
|
||||
}
|
||||
});
|
||||
childrenToRemove.forEach(child => {
|
||||
fromEl.removeChild(child);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级 API:直接从 HTML 字符串更新元素
|
||||
*/
|
||||
export function morphHTML(element: Element, htmlString: string): void {
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = htmlString;
|
||||
|
||||
// 更新元素的子节点列表
|
||||
morphChildren(element, tempContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新(使用 DocumentFragment)
|
||||
*/
|
||||
export function batchMorph(element: Element, htmlString: string): void {
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = htmlString;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
Array.from(tempContainer.childNodes).forEach(node => {
|
||||
fragment.appendChild(node);
|
||||
});
|
||||
|
||||
// 清空原内容
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild);
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
element.appendChild(fragment);
|
||||
}
|
||||
|
||||
65
frontend/src/common/utils/formatter.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Formatter utility functions
|
||||
*/
|
||||
|
||||
export interface DateTimeFormatOptions {
|
||||
locale?: string;
|
||||
includeTime?: boolean;
|
||||
hour12?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date time string to localized format
|
||||
* @param dateString - ISO date string or null
|
||||
* @param options - Formatting options
|
||||
* @returns Formatted date string or error message
|
||||
*/
|
||||
export const formatDateTime = (
|
||||
dateString: string | null,
|
||||
options: DateTimeFormatOptions = {}
|
||||
): string => {
|
||||
const {
|
||||
locale = 'en-US',
|
||||
includeTime = true,
|
||||
hour12 = false
|
||||
} = options;
|
||||
|
||||
if (!dateString) {
|
||||
return 'Unknown time';
|
||||
}
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Invalid date';
|
||||
}
|
||||
|
||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
};
|
||||
|
||||
if (includeTime) {
|
||||
formatOptions.hour = '2-digit';
|
||||
formatOptions.minute = '2-digit';
|
||||
formatOptions.hour12 = hour12;
|
||||
}
|
||||
|
||||
return date.toLocaleString(locale, formatOptions);
|
||||
} catch {
|
||||
return 'Time error';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncate string with ellipsis
|
||||
* @param str - String to truncate
|
||||
* @param maxLength - Maximum length before truncation
|
||||
* @returns Truncated string with ellipsis if needed
|
||||
*/
|
||||
export const truncateString = (str: string, maxLength: number): string => {
|
||||
if (!str) return '';
|
||||
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
|
||||
};
|
||||