Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5d90cc59a | |||
| d49ffc20df | |||
| c22e349181 | |||
| 45968cd353 | |||
| 2d02bf7f1f | |||
| 1216b0b67c | |||
| cf8bf688bf | |||
| 4d6a4ff79f | |||
| 3077d5a7c5 | |||
| bc0569af93 | |||
| 0188b618f2 | |||
| 08860e9a5c | |||
| a56d4ef379 | |||
| f5bfff80b7 | |||
| 1462d8a753 | |||
| 39ee2d14f3 | |||
| e536cdd9ba |
@@ -1,4 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
@@ -1,51 +0,0 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* A Time represents an instant in time with nanosecond precision.
|
||||
*
|
||||
* Programs using times should typically store and pass them as values,
|
||||
* not pointers. That is, time variables and struct fields should be of
|
||||
* type [time.Time], not *time.Time.
|
||||
*
|
||||
* A Time value can be used by multiple goroutines simultaneously except
|
||||
* that the methods [Time.GobDecode], [Time.UnmarshalBinary], [Time.UnmarshalJSON] and
|
||||
* [Time.UnmarshalText] are not concurrency-safe.
|
||||
*
|
||||
* Time instants can be compared using the [Time.Before], [Time.After], and [Time.Equal] methods.
|
||||
* The [Time.Sub] method subtracts two instants, producing a [Duration].
|
||||
* The [Time.Add] method adds a Time and a Duration, producing a Time.
|
||||
*
|
||||
* The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
|
||||
* As this time is unlikely to come up in practice, the [Time.IsZero] method gives
|
||||
* a simple way of detecting a time that has not been initialized explicitly.
|
||||
*
|
||||
* Each time has an associated [Location]. The methods [Time.Local], [Time.UTC], and Time.In return a
|
||||
* Time with a specific Location. Changing the Location of a Time value with
|
||||
* these methods does not change the actual instant it represents, only the time
|
||||
* zone in which to interpret it.
|
||||
*
|
||||
* Representations of a Time value saved by the [Time.GobEncode], [Time.MarshalBinary], [Time.AppendBinary],
|
||||
* [Time.MarshalJSON], [Time.MarshalText] and [Time.AppendText] methods store the [Time.Location]'s offset,
|
||||
* but not the location name. They therefore lose information about Daylight Saving Time.
|
||||
*
|
||||
* In addition to the required “wall clock” reading, a Time may contain an optional
|
||||
* reading of the current process's monotonic clock, to provide additional precision
|
||||
* for comparison or subtraction.
|
||||
* See the “Monotonic Clocks” section in the package documentation for details.
|
||||
*
|
||||
* Note that the Go == operator compares not just the time instant but also the
|
||||
* Location and the monotonic clock reading. Therefore, Time values should not
|
||||
* be used as map or database keys without first guaranteeing that the
|
||||
* identical Location has been set for all values, which can be achieved
|
||||
* through use of the UTC or Local method, and that the monotonic clock reading
|
||||
* has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u)
|
||||
* to t == u, since t.Equal uses the most accurate comparison available and
|
||||
* correctly handles the case when only one of its arguments has a monotonic
|
||||
* clock reading.
|
||||
*/
|
||||
export type Time = any;
|
||||
@@ -68,4 +68,9 @@ export enum TranslatorType {
|
||||
* DeeplTranslatorType DeepL翻译器
|
||||
*/
|
||||
DeeplTranslatorType = "deepl",
|
||||
|
||||
/**
|
||||
* TartuNLPTranslatorType TartuNLP翻译器
|
||||
*/
|
||||
TartuNLPTranslatorType = "tartunlp",
|
||||
};
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
// @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 time$0 from "../../../time/models.js";
|
||||
|
||||
/**
|
||||
* AppConfig 应用配置 - 按照前端设置页面分类组织
|
||||
*/
|
||||
@@ -196,8 +192,8 @@ export class Document {
|
||||
"id": number;
|
||||
"title": string;
|
||||
"content": string;
|
||||
"createdAt": time$0.Time;
|
||||
"updatedAt": time$0.Time;
|
||||
"createdAt": string;
|
||||
"updatedAt": string;
|
||||
"is_deleted": boolean;
|
||||
|
||||
/**
|
||||
@@ -217,10 +213,10 @@ export class Document {
|
||||
this["content"] = "";
|
||||
}
|
||||
if (!("createdAt" in $$source)) {
|
||||
this["createdAt"] = null;
|
||||
this["createdAt"] = "";
|
||||
}
|
||||
if (!("updatedAt" in $$source)) {
|
||||
this["updatedAt"] = null;
|
||||
this["updatedAt"] = "";
|
||||
}
|
||||
if (!("is_deleted" in $$source)) {
|
||||
this["is_deleted"] = false;
|
||||
@@ -490,6 +486,11 @@ export class GeneralConfig {
|
||||
*/
|
||||
"enableLoadingAnimation": boolean;
|
||||
|
||||
/**
|
||||
* 是否启用标签页模式
|
||||
*/
|
||||
"enableTabs": boolean;
|
||||
|
||||
/** Creates a new GeneralConfig instance. */
|
||||
constructor($$source: Partial<GeneralConfig> = {}) {
|
||||
if (!("alwaysOnTop" in $$source)) {
|
||||
@@ -516,6 +517,9 @@ export class GeneralConfig {
|
||||
if (!("enableLoadingAnimation" in $$source)) {
|
||||
this["enableLoadingAnimation"] = false;
|
||||
}
|
||||
if (!("enableTabs" in $$source)) {
|
||||
this["enableTabs"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -1143,8 +1147,8 @@ export class Theme {
|
||||
"type": ThemeType;
|
||||
"colors": ThemeColorConfig;
|
||||
"isDefault": boolean;
|
||||
"createdAt": time$0.Time;
|
||||
"updatedAt": time$0.Time;
|
||||
"createdAt": string;
|
||||
"updatedAt": string;
|
||||
|
||||
/** Creates a new Theme instance. */
|
||||
constructor($$source: Partial<Theme> = {}) {
|
||||
@@ -1164,10 +1168,10 @@ export class Theme {
|
||||
this["isDefault"] = false;
|
||||
}
|
||||
if (!("createdAt" in $$source)) {
|
||||
this["createdAt"] = null;
|
||||
this["createdAt"] = "";
|
||||
}
|
||||
if (!("updatedAt" in $$source)) {
|
||||
this["updatedAt"] = null;
|
||||
this["updatedAt"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
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";
|
||||
@@ -54,6 +57,11 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2900331732, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* StartAutoBackup 启动自动备份定时器
|
||||
*/
|
||||
|
||||
@@ -49,14 +49,6 @@ export function GetDocumentByID(id: number): Promise<models$0.Document | null> &
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetFirstDocumentID gets the first active document's ID for frontend initialization
|
||||
*/
|
||||
export function GetFirstDocumentID(): Promise<number> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2970773833) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
|
||||
*/
|
||||
|
||||
@@ -119,6 +119,42 @@ export enum MigrationStatus {
|
||||
MigrationStatusFailed = "failed",
|
||||
};
|
||||
|
||||
/**
|
||||
* OSInfo 操作系统信息
|
||||
*/
|
||||
export class OSInfo {
|
||||
"id": string;
|
||||
"name": string;
|
||||
"version": string;
|
||||
"branding": string;
|
||||
|
||||
/** Creates a new OSInfo instance. */
|
||||
constructor($$source: Partial<OSInfo> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("version" in $$source)) {
|
||||
this["version"] = "";
|
||||
}
|
||||
if (!("branding" in $$source)) {
|
||||
this["branding"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new OSInfo instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): OSInfo {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new OSInfo($$parsedSource as Partial<OSInfo>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SelfUpdateResult 自我更新结果
|
||||
*/
|
||||
@@ -202,6 +238,54 @@ export class SelfUpdateResult {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemInfo 系统信息
|
||||
*/
|
||||
export class SystemInfo {
|
||||
"os": string;
|
||||
"arch": string;
|
||||
"debug": boolean;
|
||||
"osInfo": OSInfo | null;
|
||||
"platformInfo": { [_: string]: any };
|
||||
|
||||
/** Creates a new SystemInfo instance. */
|
||||
constructor($$source: Partial<SystemInfo> = {}) {
|
||||
if (!("os" in $$source)) {
|
||||
this["os"] = "";
|
||||
}
|
||||
if (!("arch" in $$source)) {
|
||||
this["arch"] = "";
|
||||
}
|
||||
if (!("debug" in $$source)) {
|
||||
this["debug"] = false;
|
||||
}
|
||||
if (!("osInfo" in $$source)) {
|
||||
this["osInfo"] = null;
|
||||
}
|
||||
if (!("platformInfo" in $$source)) {
|
||||
this["platformInfo"] = {};
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SystemInfo instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): SystemInfo {
|
||||
const $$createField3_0 = $$createType1;
|
||||
const $$createField4_0 = $$createType2;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("osInfo" in $$parsedSource) {
|
||||
$$parsedSource["osInfo"] = $$createField3_0($$parsedSource["osInfo"]);
|
||||
}
|
||||
if ("platformInfo" in $$parsedSource) {
|
||||
$$parsedSource["platformInfo"] = $$createField4_0($$parsedSource["platformInfo"]);
|
||||
}
|
||||
return new SystemInfo($$parsedSource as Partial<SystemInfo>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WindowInfo 窗口信息(简化版)
|
||||
*/
|
||||
@@ -229,7 +313,7 @@ export class WindowInfo {
|
||||
* Creates a new WindowInfo instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WindowInfo {
|
||||
const $$createField0_0 = $$createType1;
|
||||
const $$createField0_0 = $$createType4;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("Window" in $$parsedSource) {
|
||||
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
|
||||
@@ -259,5 +343,8 @@ export class WindowSnapService {
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = application$0.WebviewWindow.createFrom;
|
||||
const $$createType0 = OSInfo.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType3 = application$0.WebviewWindow.createFrom;
|
||||
const $$createType4 = $Create.Nullable($$createType3);
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
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 from "./models.js";
|
||||
@@ -34,6 +38,26 @@ export function GetMemoryStats(): Promise<$models.MemoryStats> & { cancel(): voi
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetSystemInfo 获取系统环境信息
|
||||
*/
|
||||
export function GetSystemInfo(): Promise<$models.SystemInfo | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2629436820) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetAppReferences 设置应用引用
|
||||
*/
|
||||
export function SetAppReferences(app: application$0.App | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3873053414, app) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* TriggerGC 手动触发垃圾回收
|
||||
*/
|
||||
@@ -44,3 +68,5 @@ export function TriggerGC(): Promise<void> & { cancel(): void } {
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.MemoryStats.createFrom;
|
||||
const $$createType1 = $models.SystemInfo.createFrom;
|
||||
const $$createType2 = $Create.Nullable($$createType1);
|
||||
|
||||
@@ -14,27 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as translator$0 from "../common/translator/models.js";
|
||||
|
||||
/**
|
||||
* GetAvailableTranslators 获取所有可用翻译器类型
|
||||
* @returns {[]string} 翻译器类型列表
|
||||
*/
|
||||
export function GetAvailableTranslators(): Promise<string[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1186597995) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetStandardLanguageCode 获取标准化的语言代码
|
||||
*/
|
||||
export function GetStandardLanguageCode(translatorType: translator$0.TranslatorType, languageCode: string): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1158131995, translatorType, languageCode) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTranslatorLanguages 获取翻译器的语言列表
|
||||
* @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl")
|
||||
@@ -43,6 +22,19 @@ export function GetStandardLanguageCode(translatorType: translator$0.TranslatorT
|
||||
*/
|
||||
export function GetTranslatorLanguages(translatorType: translator$0.TranslatorType): Promise<{ [_: string]: translator$0.LanguageInfo }> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3976114458, translatorType) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTranslators 获取所有可用翻译器类型
|
||||
* @returns {[]string} 翻译器类型列表
|
||||
*/
|
||||
export function GetTranslators(): Promise<string[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3720069432) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
@@ -73,6 +65,6 @@ export function TranslateWith(text: string, $from: string, to: string, translato
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
const $$createType1 = translator$0.LanguageInfo.createFrom;
|
||||
const $$createType2 = $Create.Map($Create.Any, $$createType1);
|
||||
const $$createType0 = translator$0.LanguageInfo.createFrom;
|
||||
const $$createType1 = $Create.Map($Create.Any, $$createType0);
|
||||
const $$createType2 = $Create.Array($Create.Any);
|
||||
|
||||
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@@ -16,6 +16,9 @@ declare module 'vue' {
|
||||
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
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']
|
||||
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']
|
||||
|
||||
@@ -50,7 +50,11 @@ export default defineConfig([
|
||||
'.local',
|
||||
'/bin',
|
||||
'Dockerfile',
|
||||
'**/bindings/'
|
||||
'**/bindings/',
|
||||
'*.js',
|
||||
'**/*.js',
|
||||
'**/*.cjs',
|
||||
'**/*.mjs',
|
||||
],
|
||||
}
|
||||
]);
|
||||
691
frontend/package-lock.json
generated
691
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,11 @@
|
||||
"build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" vue-tsc && cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
"lint:fix": "eslint --fix",
|
||||
"build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.7",
|
||||
"@codemirror/autocomplete": "^6.19.0",
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
@@ -30,29 +31,28 @@
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
"@codemirror/lang-sass": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.9.1",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@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-data": "^6.5.1",
|
||||
"@codemirror/legacy-modes": "^6.5.1",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.8.5",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.2",
|
||||
"@codemirror/view": "^6.38.4",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@prettier/plugin-xml": "^3.4.2",
|
||||
"@reteps/dockerfmt": "^0.3.6",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
"@toml-tools/lexer": "^1.0.0",
|
||||
"@toml-tools/parser": "^1.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"franc-min": "^6.2.0",
|
||||
"groovy-beautify": "^0.0.17",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"java-parser": "^3.0.1",
|
||||
@@ -63,30 +63,29 @@
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.92.1",
|
||||
"sh-syntax": "^0.5.8",
|
||||
"vue": "^3.5.21",
|
||||
"sass": "^1.93.2",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@wailsio/runtime": "latest",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"globals": "^16.4.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"unplugin-vue-components": "^29.0.0",
|
||||
"vite": "^7.1.5",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.6"
|
||||
"vue-tsc": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from 'vue';
|
||||
import {onBeforeMount} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useSystemStore} from '@/stores/systemStore';
|
||||
import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||
import {useThemeStore} from '@/stores/themeStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import {useBackupStore} from '@/stores/backupStore';
|
||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||
import {useTranslationStore} from "@/stores/translationStore";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const systemStore = useSystemStore();
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const themeStore = useThemeStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const backupStore = useBackupStore();
|
||||
const translationStore = useTranslationStore();
|
||||
|
||||
// 应用启动时加载配置和初始化系统信息
|
||||
onMounted(async () => {
|
||||
onBeforeMount(async () => {
|
||||
// 并行初始化配置、系统信息和快捷键配置
|
||||
await Promise.all([
|
||||
configStore.initConfig(),
|
||||
@@ -26,10 +25,8 @@ onMounted(async () => {
|
||||
|
||||
// 初始化语言和主题
|
||||
await configStore.initializeLanguage();
|
||||
themeStore.initializeTheme();
|
||||
|
||||
// 初始化备份服务
|
||||
await backupStore.initialize();
|
||||
await themeStore.initializeTheme();
|
||||
await translationStore.loadTranslators();
|
||||
|
||||
// 启动时检查更新
|
||||
await updateStore.checkOnStartup();
|
||||
|
||||
BIN
frontend/src/assets/fonts/Hack/hack-bold.woff
Normal file
BIN
frontend/src/assets/fonts/Hack/hack-bold.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Hack/hack-bold.woff2
Normal file
BIN
frontend/src/assets/fonts/Hack/hack-bold.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Hack/hack-bolditalic.woff
Normal file
BIN
frontend/src/assets/fonts/Hack/hack-bolditalic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Hack/hack-bolditalic.woff2
Normal file
BIN
frontend/src/assets/fonts/Hack/hack-bolditalic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Hack/hack-italic.woff
Normal file
BIN
frontend/src/assets/fonts/Hack/hack-italic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Hack/hack-italic.woff2
Normal file
BIN
frontend/src/assets/fonts/Hack/hack-italic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Hack/hack-regular.woff
Normal file
BIN
frontend/src/assets/fonts/Hack/hack-regular.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Hack/hack-regular.woff2
Normal file
BIN
frontend/src/assets/fonts/Hack/hack-regular.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
254
frontend/src/assets/fonts/HarmonyOS/font_compressor.py
Normal file
254
frontend/src/assets/fonts/HarmonyOS/font_compressor.py
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
鸿蒙字体压缩工具
|
||||
使用 fonttools 库压缩 TTF 字体文件,减小文件大小
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
def check_dependencies():
|
||||
"""检查必要的依赖是否已安装"""
|
||||
missing_packages = []
|
||||
|
||||
# 检查 fonttools
|
||||
try:
|
||||
import fontTools
|
||||
except ImportError:
|
||||
missing_packages.append('fonttools')
|
||||
|
||||
# 检查 brotli
|
||||
try:
|
||||
import brotli
|
||||
except ImportError:
|
||||
missing_packages.append('brotli')
|
||||
|
||||
# 检查 pyftsubset 命令是否可用
|
||||
try:
|
||||
result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
missing_packages.append('fonttools[subset]')
|
||||
except FileNotFoundError:
|
||||
if 'fonttools' not in missing_packages:
|
||||
missing_packages.append('fonttools[subset]')
|
||||
|
||||
if missing_packages:
|
||||
print(f"缺少必要的依赖包: {', '.join(missing_packages)}")
|
||||
print("请运行以下命令安装:")
|
||||
print(f"pip install {' '.join(missing_packages)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_file_size(file_path: str) -> int:
|
||||
"""获取文件大小(字节)"""
|
||||
return os.path.getsize(file_path)
|
||||
|
||||
def format_file_size(size_bytes: int) -> str:
|
||||
"""格式化文件大小显示"""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.2f} KB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
||||
|
||||
def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool:
|
||||
"""
|
||||
压缩单个字体文件
|
||||
|
||||
Args:
|
||||
input_path: 输入字体文件路径
|
||||
output_path: 输出字体文件路径
|
||||
compression_level: 压缩级别 ("basic", "medium", "aggressive")
|
||||
|
||||
Returns:
|
||||
bool: 压缩是否成功
|
||||
"""
|
||||
try:
|
||||
# 基础压缩参数
|
||||
base_args = [
|
||||
"pyftsubset", input_path,
|
||||
"--output-file=" + output_path,
|
||||
"--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高
|
||||
"--with-zopfli", # 使用 Zopfli 算法进一步压缩
|
||||
]
|
||||
|
||||
# 根据压缩级别设置不同的参数
|
||||
if compression_level == "basic":
|
||||
# 基础压缩:保留常用字符和功能
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等
|
||||
"--layout-features=*", # 保留所有布局特性
|
||||
"--glyph-names", # 保留字形名称
|
||||
"--symbol-cmap", # 保留符号映射
|
||||
"--legacy-cmap", # 保留传统字符映射
|
||||
"--notdef-glyph", # 保留 .notdef 字形
|
||||
"--recommended-glyphs", # 保留推荐字形
|
||||
"--name-IDs=*", # 保留所有名称ID
|
||||
"--name-legacy", # 保留传统名称
|
||||
]
|
||||
elif compression_level == "medium":
|
||||
# 中等压缩:移除一些不常用的功能
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围
|
||||
"--layout-features=kern,liga,clig", # 只保留关键布局特性
|
||||
"--no-glyph-names", # 移除字形名称
|
||||
"--notdef-glyph",
|
||||
"--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID
|
||||
]
|
||||
else: # aggressive
|
||||
# 激进压缩:最大程度减小文件大小
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F", # 只保留基本ASCII字符
|
||||
"--no-layout-features", # 移除所有布局特性
|
||||
"--no-glyph-names", # 移除字形名称
|
||||
"--no-symbol-cmap", # 移除符号映射
|
||||
"--no-legacy-cmap", # 移除传统映射
|
||||
"--notdef-glyph",
|
||||
"--name-IDs=1,2", # 只保留最基本的名称
|
||||
"--desubroutinize", # 去子程序化(可能减小CFF字体大小)
|
||||
]
|
||||
|
||||
# 执行压缩命令
|
||||
result = subprocess.run(args, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"压缩失败: {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"压缩过程中出现错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def find_font_files(directory: str) -> List[str]:
|
||||
"""查找目录中的所有字体文件"""
|
||||
font_extensions = ['.ttf', '.otf', '.woff', '.woff2']
|
||||
font_files = []
|
||||
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
if any(file.lower().endswith(ext) for ext in font_extensions):
|
||||
font_files.append(os.path.join(root, file))
|
||||
|
||||
return font_files
|
||||
|
||||
def compress_fonts_batch(font_directory: str, compression_level: str = "basic"):
|
||||
"""
|
||||
批量压缩字体文件
|
||||
|
||||
Args:
|
||||
font_directory: 字体文件目录
|
||||
compression_level: 压缩级别
|
||||
"""
|
||||
if not os.path.exists(font_directory):
|
||||
print(f"错误: 目录 {font_directory} 不存在")
|
||||
return
|
||||
|
||||
# 查找所有字体文件
|
||||
font_files = find_font_files(font_directory)
|
||||
|
||||
if not font_files:
|
||||
print("未找到字体文件")
|
||||
return
|
||||
|
||||
print(f"找到 {len(font_files)} 个字体文件")
|
||||
print(f"压缩级别: {compression_level}")
|
||||
print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2")
|
||||
print("-" * 60)
|
||||
|
||||
total_original_size = 0
|
||||
total_compressed_size = 0
|
||||
successful_compressions = 0
|
||||
|
||||
for i, font_file in enumerate(font_files, 1):
|
||||
print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}")
|
||||
|
||||
# 获取原始文件大小
|
||||
original_size = get_file_size(font_file)
|
||||
total_original_size += original_size
|
||||
|
||||
# 生成输出文件名(保持原文件名,只改变扩展名)
|
||||
file_dir = os.path.dirname(font_file)
|
||||
base_name = os.path.splitext(os.path.basename(font_file))[0]
|
||||
output_file = os.path.join(file_dir, f"{base_name}.woff2")
|
||||
|
||||
# 压缩字体
|
||||
if compress_font(font_file, output_file, compression_level):
|
||||
if os.path.exists(output_file):
|
||||
compressed_size = get_file_size(output_file)
|
||||
total_compressed_size += compressed_size
|
||||
successful_compressions += 1
|
||||
|
||||
# 计算压缩率
|
||||
compression_ratio = (1 - compressed_size / original_size) * 100
|
||||
|
||||
print(f" ✓ 成功: {format_file_size(original_size)} → {format_file_size(compressed_size)} "
|
||||
f"(压缩 {compression_ratio:.1f}%)")
|
||||
else:
|
||||
print(f" ✗ 失败: 输出文件未生成")
|
||||
else:
|
||||
print(f" ✗ 失败: 压缩过程出错")
|
||||
|
||||
print()
|
||||
|
||||
# 显示总结
|
||||
print("=" * 60)
|
||||
print("压缩完成!")
|
||||
print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件")
|
||||
|
||||
if successful_compressions > 0:
|
||||
total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
|
||||
print(f"总大小: {format_file_size(total_original_size)} → {format_file_size(total_compressed_size)}")
|
||||
print(f"总压缩率: {total_compression_ratio:.1f}%")
|
||||
print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}")
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("鸿蒙字体压缩工具")
|
||||
print("=" * 60)
|
||||
|
||||
# 检查依赖
|
||||
if not check_dependencies():
|
||||
return
|
||||
|
||||
# 获取当前脚本所在目录
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 设置默认字体目录
|
||||
font_directory = current_dir
|
||||
|
||||
print(f"字体目录: {font_directory}")
|
||||
|
||||
# 让用户选择压缩级别
|
||||
print("\n请选择压缩级别:")
|
||||
print("1. 基础压缩 (保留大部分功能,适合网页使用)")
|
||||
print("2. 中等压缩 (平衡文件大小和功能)")
|
||||
print("3. 激进压缩 (最小文件大小,可能影响显示效果)")
|
||||
|
||||
while True:
|
||||
choice = input("\n请输入选择 (1-3): ").strip()
|
||||
if choice == "1":
|
||||
compression_level = "basic"
|
||||
break
|
||||
elif choice == "2":
|
||||
compression_level = "medium"
|
||||
break
|
||||
elif choice == "3":
|
||||
compression_level = "aggressive"
|
||||
break
|
||||
else:
|
||||
print("无效选择,请输入 1、2 或 3")
|
||||
|
||||
# 开始批量压缩
|
||||
compress_fonts_batch(font_directory, compression_level=compression_level)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
frontend/src/assets/fonts/OpenSans/Bold/OpenSans-Bold.woff
Normal file
BIN
frontend/src/assets/fonts/OpenSans/Bold/OpenSans-Bold.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/OpenSans/Bold/OpenSans-Bold.woff2
Normal file
BIN
frontend/src/assets/fonts/OpenSans/Bold/OpenSans-Bold.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/OpenSans/Italic/OpenSans-Italic.woff
Normal file
BIN
frontend/src/assets/fonts/OpenSans/Italic/OpenSans-Italic.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/OpenSans/Italic/OpenSans-Italic.woff2
Normal file
BIN
frontend/src/assets/fonts/OpenSans/Italic/OpenSans-Italic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/OpenSans/Light/OpenSans-Light.woff
Normal file
BIN
frontend/src/assets/fonts/OpenSans/Light/OpenSans-Light.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/OpenSans/Light/OpenSans-Light.woff2
Normal file
BIN
frontend/src/assets/fonts/OpenSans/Light/OpenSans-Light.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/OpenSans/Regular/OpenSans-Regular.woff
Normal file
BIN
frontend/src/assets/fonts/OpenSans/Regular/OpenSans-Regular.woff
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,146 +0,0 @@
|
||||
/* HarmonyOS Sans 字体定义 */
|
||||
|
||||
/* HarmonyOS Sans Regular */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Light */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Medium */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Semibold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Semibold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Bold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Black */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Black.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Thin */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC 简体中文字体 */
|
||||
|
||||
/* HarmonyOS Sans SC Regular */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Light */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Medium */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Semibold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Semibold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Bold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Black */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Black.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Thin */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* 字体加载优化 */
|
||||
.font-loading {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.font-loaded {
|
||||
font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* CodeMirror 专用字体类 */
|
||||
.cm-harmonyos-font {
|
||||
font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif !important;
|
||||
font-feature-settings: 'liga' 1, 'calt' 1;
|
||||
font-variant-ligatures: contextual;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
31
frontend/src/assets/styles/hack_fonts.css
Normal file
31
frontend/src/assets/styles/hack_fonts.css
Normal file
@@ -0,0 +1,31 @@
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url('../fonts/Hack/hack-regular.woff2') format('woff2'),
|
||||
url('../fonts/Hack/hack-regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url('../fonts/Hack/hack-bold.woff2') format('woff2'),
|
||||
url('../fonts/Hack/hack-bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url('../fonts/Hack/hack-italic.woff2') format('woff2'),
|
||||
url('../fonts/Hack/hack-italic.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url('../fonts/Hack/hack-bolditalic.woff2') format('woff2'),
|
||||
url('../fonts/Hack/hack-bolditalic.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
134
frontend/src/assets/styles/harmony_fonts.css
Normal file
134
frontend/src/assets/styles/harmony_fonts.css
Normal file
@@ -0,0 +1,134 @@
|
||||
/* HarmonyOS 字体定义 */
|
||||
|
||||
/* HarmonyOS Regular */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Regular.woff2') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Light */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Light.woff2') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Medium */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Medium.woff2') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Semibold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Semibold.woff2') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Bold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Bold.woff2') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Black */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Black.woff2') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Thin */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Thin.woff2') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS SC 简体中文字体 */
|
||||
|
||||
/* HarmonyOS SC Regular */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Regular.woff2') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS SC Light */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Light.woff2') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS SC Medium */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Medium.woff2') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS SC Semibold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Semibold.woff2') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS SC Bold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Bold.woff2') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS SC Black */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Black.woff2') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS SC Thin */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Thin.woff2') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* 字体加载优化 */
|
||||
.font-loading {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/* 导入所有CSS文件 */
|
||||
@import 'normalize.css';
|
||||
@import 'variables.css';
|
||||
@import "fonts.css";
|
||||
@import "harmony_fonts.css";
|
||||
@import 'scrollbar.css';
|
||||
@import 'hack_fonts.css';
|
||||
@import 'opensans_fonts.css';
|
||||
80
frontend/src/assets/styles/opensans_fonts.css
Normal file
80
frontend/src/assets/styles/opensans_fonts.css
Normal file
@@ -0,0 +1,80 @@
|
||||
/* BEGIN Light */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/Light/OpenSans-Light.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Light/OpenSans-Light.woff?v=1.1.0") format("woff");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
/* END Light */
|
||||
/* BEGIN Light Italic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/LightItalic/OpenSans-LightItalic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/LightItalic/OpenSans-LightItalic.woff?v=1.1.0") format("woff");
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
/* END Light Italic */
|
||||
/* BEGIN Regular */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/Regular/OpenSans-Regular.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Regular/OpenSans-Regular.woff?v=1.1.0") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* END Regular */
|
||||
/* BEGIN Italic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/Italic/OpenSans-Italic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Italic/OpenSans-Italic.woff?v=1.1.0") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
/* END Italic */
|
||||
/* BEGIN Semibold */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/Semibold/OpenSans-Semibold.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Semibold/OpenSans-Semibold.woff?v=1.1.0") format("woff");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
/* END Semibold */
|
||||
/* BEGIN Semibold Italic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/SemiboldItalic/OpenSans-SemiboldItalic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/SemiboldItalic/OpenSans-SemiboldItalic.woff?v=1.1.0") format("woff");
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
/* END Semibold Italic */
|
||||
/* BEGIN Bold */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/Bold/OpenSans-Bold.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Bold/OpenSans-Bold.woff?v=1.1.0") format("woff");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
/* END Bold */
|
||||
/* BEGIN Bold Italic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/BoldItalic/OpenSans-BoldItalic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/BoldItalic/OpenSans-BoldItalic.woff?v=1.1.0") format("woff");
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
/* END Bold Italic */
|
||||
/* BEGIN Extrabold */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/ExtraBold/OpenSans-ExtraBold.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/ExtraBold/OpenSans-ExtraBold.woff?v=1.1.0") format("woff");
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
}
|
||||
/* END Extrabold */
|
||||
/* BEGIN Extrabold Italic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("../fonts/OpenSans/ExtraBoldItalic/OpenSans-ExtraBoldItalic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/ExtraBoldItalic/OpenSans-ExtraBoldItalic.woff?v=1.1.0") format("woff");
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
}
|
||||
/* END Extrabold Italic */
|
||||
@@ -8,6 +8,7 @@
|
||||
--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;
|
||||
@@ -40,6 +41,7 @@
|
||||
--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;
|
||||
@@ -73,6 +75,7 @@
|
||||
--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);
|
||||
@@ -112,6 +115,7 @@
|
||||
--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);
|
||||
@@ -149,6 +153,7 @@
|
||||
--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);
|
||||
@@ -185,6 +190,7 @@
|
||||
--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);
|
||||
@@ -220,6 +226,7 @@
|
||||
--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);
|
||||
|
||||
166
frontend/src/common/constant/config.ts
Normal file
166
frontend/src/common/constant/config.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
AppConfig,
|
||||
AppearanceConfig,
|
||||
EditingConfig,
|
||||
GeneralConfig,
|
||||
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 const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
||||
alwaysOnTop: 'general.alwaysOnTop',
|
||||
dataPath: 'general.dataPath',
|
||||
enableSystemTray: 'general.enableSystemTray',
|
||||
startAtLogin: 'general.startAtLogin',
|
||||
enableGlobalHotkey: 'general.enableGlobalHotkey',
|
||||
globalHotkey: 'general.globalHotkey',
|
||||
enableWindowSnap: 'general.enableWindowSnap',
|
||||
enableLoadingAnimation: 'general.enableLoadingAnimation',
|
||||
enableTabs: 'general.enableTabs',
|
||||
} as const;
|
||||
|
||||
export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||
fontSize: 'editing.fontSize',
|
||||
fontFamily: 'editing.fontFamily',
|
||||
fontWeight: 'editing.fontWeight',
|
||||
lineHeight: 'editing.lineHeight',
|
||||
enableTabIndent: 'editing.enableTabIndent',
|
||||
tabSize: 'editing.tabSize',
|
||||
tabType: 'editing.tabType',
|
||||
autoSaveDelay: 'editing.autoSaveDelay'
|
||||
} as const;
|
||||
|
||||
export const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||
language: 'appearance.language',
|
||||
systemTheme: 'appearance.systemTheme'
|
||||
} as const;
|
||||
|
||||
export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
version: 'updates.version',
|
||||
autoUpdate: 'updates.autoUpdate',
|
||||
primarySource: 'updates.primarySource',
|
||||
backupSource: 'updates.backupSource',
|
||||
backupBeforeUpdate: 'updates.backupBeforeUpdate',
|
||||
updateTimeout: 'updates.updateTimeout',
|
||||
github: 'updates.github',
|
||||
gitea: 'updates.gitea'
|
||||
} as const;
|
||||
|
||||
export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
|
||||
enabled: 'backup.enabled',
|
||||
repo_url: 'backup.repo_url',
|
||||
auth_method: 'backup.auth_method',
|
||||
username: 'backup.username',
|
||||
password: 'backup.password',
|
||||
token: 'backup.token',
|
||||
ssh_key_path: 'backup.ssh_key_path',
|
||||
ssh_key_passphrase: 'backup.ssh_key_passphrase',
|
||||
backup_interval: 'backup.backup_interval',
|
||||
auto_backup: 'backup.auto_backup',
|
||||
} as const;
|
||||
|
||||
// 配置限制
|
||||
export const CONFIG_LIMITS = {
|
||||
fontSize: {min: 12, max: 28, default: 13},
|
||||
tabSize: {min: 2, max: 8, default: 4},
|
||||
lineHeight: {min: 1.0, max: 3.0, default: 1.5},
|
||||
tabType: {values: [TabType.TabTypeSpaces, TabType.TabTypeTab], default: TabType.TabTypeSpaces}
|
||||
} as const;
|
||||
|
||||
// 默认配置
|
||||
export const DEFAULT_CONFIG: AppConfig = {
|
||||
general: {
|
||||
alwaysOnTop: false,
|
||||
dataPath: '',
|
||||
enableSystemTray: true,
|
||||
startAtLogin: false,
|
||||
enableGlobalHotkey: false,
|
||||
globalHotkey: {
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
alt: true,
|
||||
win: false,
|
||||
key: 'X'
|
||||
},
|
||||
enableWindowSnap: true,
|
||||
enableLoadingAnimation: true,
|
||||
enableTabs: false,
|
||||
},
|
||||
editing: {
|
||||
fontSize: CONFIG_LIMITS.fontSize.default,
|
||||
fontFamily: FONT_OPTIONS[0].value,
|
||||
fontWeight: '400',
|
||||
lineHeight: CONFIG_LIMITS.lineHeight.default,
|
||||
enableTabIndent: true,
|
||||
tabSize: CONFIG_LIMITS.tabSize.default,
|
||||
tabType: CONFIG_LIMITS.tabType.default,
|
||||
autoSaveDelay: 5000
|
||||
},
|
||||
appearance: {
|
||||
language: LanguageType.LangZhCN,
|
||||
systemTheme: SystemThemeType.SystemThemeAuto
|
||||
},
|
||||
updates: {
|
||||
version: "1.0.0",
|
||||
autoUpdate: true,
|
||||
primarySource: UpdateSourceType.UpdateSourceGithub,
|
||||
backupSource: UpdateSourceType.UpdateSourceGitea,
|
||||
backupBeforeUpdate: true,
|
||||
updateTimeout: 30,
|
||||
github: {
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
},
|
||||
gitea: {
|
||||
baseURL: "https://git.landaiqing.cn",
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
}
|
||||
},
|
||||
backup: {
|
||||
enabled: false,
|
||||
repo_url: "",
|
||||
auth_method: AuthMethod.UserPass,
|
||||
username: "",
|
||||
password: "",
|
||||
token: "",
|
||||
ssh_key_path: "",
|
||||
ssh_key_passphrase: "",
|
||||
backup_interval: 60,
|
||||
auto_backup: true,
|
||||
},
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastUpdated: new Date().toString(),
|
||||
}
|
||||
};
|
||||
13
frontend/src/common/constant/editor.ts
Normal file
13
frontend/src/common/constant/editor.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 编辑器相关常量配置
|
||||
*/
|
||||
|
||||
// 编辑器实例管理
|
||||
export const EDITOR_CONFIG = {
|
||||
/** 最多缓存的编辑器实例数量 */
|
||||
MAX_INSTANCES: 5,
|
||||
/** 语法树缓存过期时间(毫秒) */
|
||||
SYNTAX_TREE_CACHE_TIMEOUT: 30000,
|
||||
/** 加载状态延迟时间(毫秒) */
|
||||
LOADING_DELAY: 500,
|
||||
} as const;
|
||||
101
frontend/src/common/constant/fonts.ts
Normal file
101
frontend/src/common/constant/fonts.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// Font options with popular programming and common fonts
|
||||
export const FONT_OPTIONS = [
|
||||
// Custom fonts
|
||||
{
|
||||
label: 'HarmonyOS',
|
||||
value: 'HarmonyOS'
|
||||
},
|
||||
{
|
||||
label: 'Hack',
|
||||
value: 'Hack'
|
||||
},
|
||||
{
|
||||
label: 'Open Sans',
|
||||
value: '"Open Sans"'
|
||||
},
|
||||
// Common system fonts
|
||||
{
|
||||
label: 'Arial',
|
||||
value: 'Arial'
|
||||
},
|
||||
{
|
||||
label: 'Helvetica',
|
||||
value: 'Helvetica'
|
||||
},
|
||||
{
|
||||
label: 'Times New Roman',
|
||||
value: '"Times New Roman"'
|
||||
},
|
||||
{
|
||||
label: 'Georgia',
|
||||
value: 'Georgia'
|
||||
},
|
||||
{
|
||||
label: 'Verdana',
|
||||
value: 'Verdana'
|
||||
},
|
||||
{
|
||||
label: 'Tahoma',
|
||||
value: 'Tahoma'
|
||||
},
|
||||
{
|
||||
label: 'Segoe UI',
|
||||
value: '"Segoe UI"'
|
||||
},
|
||||
{
|
||||
label: 'System UI',
|
||||
value: 'system-ui'
|
||||
},
|
||||
|
||||
// Chinese fonts
|
||||
{
|
||||
label: 'Microsoft YaHei',
|
||||
value: '"Microsoft YaHei"'
|
||||
},
|
||||
{
|
||||
label: 'PingFang SC',
|
||||
value: '"PingFang SC"'
|
||||
},
|
||||
|
||||
// Popular programming fonts
|
||||
{
|
||||
label: 'JetBrains Mono',
|
||||
value: '"JetBrains Mono"'
|
||||
},
|
||||
{
|
||||
label: 'Fira Code',
|
||||
value: '"Fira Code"'
|
||||
},
|
||||
{
|
||||
label: 'Source Code Pro',
|
||||
value: '"Source Code Pro"'
|
||||
},
|
||||
{
|
||||
label: 'Cascadia Code',
|
||||
value: '"Cascadia Code"'
|
||||
},
|
||||
{
|
||||
label: 'Consolas',
|
||||
value: 'Consolas'
|
||||
},
|
||||
{
|
||||
label: 'Monaco',
|
||||
value: 'Monaco'
|
||||
},
|
||||
{
|
||||
label: 'Menlo',
|
||||
value: 'Menlo'
|
||||
},
|
||||
{
|
||||
label: 'Roboto Mono',
|
||||
value: '"Roboto Mono"'
|
||||
},
|
||||
{
|
||||
label: 'Inconsolata',
|
||||
value: 'Inconsolata'
|
||||
},
|
||||
{
|
||||
label: 'Ubuntu Mono',
|
||||
value: '"Ubuntu Mono"'
|
||||
}
|
||||
];
|
||||
13
frontend/src/common/constant/locales.ts
Normal file
13
frontend/src/common/constant/locales.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type SupportedLocaleType = 'zh-CN' | 'en-US';
|
||||
|
||||
// 支持的语言列表
|
||||
export const SUPPORTED_LOCALES = [
|
||||
{
|
||||
code: 'en-US' as SupportedLocaleType,
|
||||
name: 'English'
|
||||
},
|
||||
{
|
||||
code: 'zh-CN' as SupportedLocaleType,
|
||||
name: '简体中文'
|
||||
}
|
||||
] as const;
|
||||
49
frontend/src/common/constant/translation.ts
Normal file
49
frontend/src/common/constant/translation.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 默认翻译配置
|
||||
*/
|
||||
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>`;
|
||||
@@ -78,7 +78,7 @@ _69: () => {
|
||||
_70: () => {
|
||||
return typeof process != "undefined" &&
|
||||
Object.prototype.toString.call(process) == "[object process]" &&
|
||||
process.platform == "win32"
|
||||
process.platform == "win32";
|
||||
},
|
||||
_85: s => JSON.stringify(s),
|
||||
_86: s => printToConsole(s),
|
||||
@@ -126,7 +126,7 @@ _157: Function.prototype.call.bind(DataView.prototype.setFloat32),
|
||||
_158: Function.prototype.call.bind(DataView.prototype.getFloat64),
|
||||
_159: Function.prototype.call.bind(DataView.prototype.setFloat64),
|
||||
_165: x0 => format = x0,
|
||||
_166: f => finalizeWrapper(f, function(x0,x1,x2) { return dartInstance.exports._166(f,arguments.length,x0,x1,x2) }),
|
||||
_166: f => finalizeWrapper(f, function(x0,x1,x2) { return dartInstance.exports._166(f,arguments.length,x0,x1,x2); }),
|
||||
_184: (c) =>
|
||||
queueMicrotask(() => dartInstance.exports.$invokeCallback(c)),
|
||||
_187: (s, m) => {
|
||||
@@ -337,14 +337,14 @@ _272: (x0,x1) => x0.lastIndex = x1
|
||||
});
|
||||
|
||||
return dartInstance;
|
||||
}
|
||||
};
|
||||
|
||||
// Call the main function for the instantiated module
|
||||
// `moduleInstance` is the instantiated dart2wasm module
|
||||
// `args` are any arguments that should be passed into the main function.
|
||||
export const invoke = (moduleInstance, ...args) => {
|
||||
moduleInstance.exports.$invokeMain(args);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export let format;
|
||||
23
frontend/src/common/prettier/plugins/docker/docker_fmt.d.ts
vendored
Normal file
23
frontend/src/common/prettier/plugins/docker/docker_fmt.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// Format options interface for Dockerfile
|
||||
export interface FormatOptions {
|
||||
indent?: number;
|
||||
trailingNewline?: boolean;
|
||||
spaceRedirects?: boolean;
|
||||
}
|
||||
|
||||
// Initialize the WASM module
|
||||
declare function init(wasmUrl?: string): Promise<void>;
|
||||
|
||||
// Format Dockerfile content
|
||||
export declare function format(text: string, options?: FormatOptions): string;
|
||||
|
||||
// Format Dockerfile contents (alias for compatibility)
|
||||
export declare function formatDockerfileContents(
|
||||
fileContents: string,
|
||||
options?: FormatOptions
|
||||
): string;
|
||||
|
||||
// Placeholder for Node.js compatibility (not implemented in browser)
|
||||
export declare function formatDockerfile(): never;
|
||||
|
||||
export default init;
|
||||
86
frontend/src/common/prettier/plugins/docker/docker_fmt.js
Normal file
86
frontend/src/common/prettier/plugins/docker/docker_fmt.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import './wasm_exec.js'
|
||||
// Format options for Dockerfile
|
||||
export const FormatOptions = {
|
||||
indentSize: 4,
|
||||
trailingNewline: true,
|
||||
spaceRedirects: false,
|
||||
}
|
||||
|
||||
let wasmInstance = null;
|
||||
let isInitialized = false;
|
||||
|
||||
// Initialize the WASM module
|
||||
export default async function init(wasmUrl) {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Load WASM file
|
||||
const wasmPath = wasmUrl || new URL('./docker_fmt.wasm', import.meta.url).href;
|
||||
const wasmResponse = await fetch(wasmPath);
|
||||
const wasmBytes = await wasmResponse.arrayBuffer();
|
||||
|
||||
// Initialize Go runtime
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiate(wasmBytes, go.importObject);
|
||||
|
||||
wasmInstance = result.instance;
|
||||
|
||||
// Run the Go program (don't await, as it needs to stay alive)
|
||||
go.run(wasmInstance);
|
||||
|
||||
// Wait a bit for the Go program to initialize and register the function
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
isInitialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Dockerfile WASM module:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Format Dockerfile content
|
||||
export function format(text, options = {}) {
|
||||
if (!isInitialized) {
|
||||
throw new Error('WASM module not initialized. Call init() first.');
|
||||
}
|
||||
|
||||
if (typeof globalThis.dockerFormat !== 'function') {
|
||||
throw new Error('dockerFormat function not available. WASM module may not be properly initialized.');
|
||||
}
|
||||
|
||||
const config = {
|
||||
indentSize: options.indentSize || options.indent || 4,
|
||||
trailingNewline: options.trailingNewline !== undefined ? options.trailingNewline : true,
|
||||
spaceRedirects: options.spaceRedirects !== undefined ? options.spaceRedirects : false
|
||||
};
|
||||
|
||||
try {
|
||||
// Call the dockerFormat function registered by Go
|
||||
const result = globalThis.dockerFormat(text, config);
|
||||
|
||||
// Check if there was an error
|
||||
if (result && Array.isArray(result) && result[0] === true) {
|
||||
throw new Error(result[1] || 'Unknown formatting error');
|
||||
}
|
||||
|
||||
// Return the formatted result
|
||||
return result && Array.isArray(result) ? result[1] : result;
|
||||
} catch (error) {
|
||||
console.warn('Dockerfile formatting error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Format Dockerfile contents (alias for compatibility)
|
||||
export function formatDockerfileContents(fileContents, options) {
|
||||
return format(fileContents, options);
|
||||
}
|
||||
|
||||
// Placeholder for Node.js compatibility (not implemented in browser)
|
||||
export function formatDockerfile() {
|
||||
throw new Error(
|
||||
'`formatDockerfile` is not implemented in the browser. Use `format` or `formatDockerfileContents` instead.',
|
||||
);
|
||||
}
|
||||
BIN
frontend/src/common/prettier/plugins/docker/docker_fmt.wasm
Normal file
BIN
frontend/src/common/prettier/plugins/docker/docker_fmt.wasm
Normal file
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
import initAsync from './docker_fmt.js'
|
||||
import wasm_url from './docker_fmt.wasm?url'
|
||||
|
||||
export default function init() {
|
||||
return initAsync(wasm_url)
|
||||
}
|
||||
|
||||
export * from './docker_fmt.js'
|
||||
58
frontend/src/common/prettier/plugins/docker/format.go
Normal file
58
frontend/src/common/prettier/plugins/docker/format.go
Normal file
@@ -0,0 +1,58 @@
|
||||
//go:build js || wasm
|
||||
|
||||
// tinygo build -o docker_fmt.wasm -target wasm --no-debug
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"syscall/js"
|
||||
|
||||
"docker_fmt/lib"
|
||||
)
|
||||
|
||||
func Format(this js.Value, args []js.Value) any {
|
||||
if len(args) < 1 {
|
||||
return []any{true, "missing input argument"}
|
||||
}
|
||||
|
||||
input := args[0].String()
|
||||
|
||||
// Default configuration
|
||||
indentSize := uint(4)
|
||||
newlineFlag := true
|
||||
spaceRedirects := false
|
||||
|
||||
// Parse optional configuration if provided
|
||||
if len(args) > 1 && !args[1].IsNull() && !args[1].IsUndefined() {
|
||||
config := args[1]
|
||||
if !config.Get("indentSize").IsUndefined() {
|
||||
indentSize = uint(config.Get("indentSize").Int())
|
||||
}
|
||||
if !config.Get("trailingNewline").IsUndefined() {
|
||||
newlineFlag = config.Get("trailingNewline").Bool()
|
||||
}
|
||||
if !config.Get("spaceRedirects").IsUndefined() {
|
||||
spaceRedirects = config.Get("spaceRedirects").Bool()
|
||||
}
|
||||
}
|
||||
|
||||
originalLines := strings.SplitAfter(input, "\n")
|
||||
c := &lib.Config{
|
||||
IndentSize: indentSize,
|
||||
TrailingNewline: newlineFlag,
|
||||
SpaceRedirects: spaceRedirects,
|
||||
}
|
||||
|
||||
result, err := lib.FormatFileLines(originalLines, c)
|
||||
if err != nil {
|
||||
return []any{true, err.Error()}
|
||||
}
|
||||
|
||||
return []any{false, result}
|
||||
}
|
||||
|
||||
func main() {
|
||||
done := make(chan bool)
|
||||
js.Global().Set("dockerFormat", js.FuncOf(Format))
|
||||
<-done
|
||||
}
|
||||
17
frontend/src/common/prettier/plugins/docker/go.mod
Normal file
17
frontend/src/common/prettier/plugins/docker/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module docker_fmt
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/moby/buildkit v0.24.0
|
||||
mvdan.cc/sh/v3 v3.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
71
frontend/src/common/prettier/plugins/docker/go.sum
Normal file
71
frontend/src/common/prettier/plugins/docker/go.sum
Normal file
@@ -0,0 +1,71 @@
|
||||
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
|
||||
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/moby/buildkit v0.20.2 h1:qIeR47eQ1tzI1rwz0on3Xx2enRw/1CKjFhoONVcTlMA=
|
||||
github.com/moby/buildkit v0.20.2/go.mod h1:DhaF82FjwOElTftl0JUAJpH/SUIUx4UvcFncLeOtlDI=
|
||||
github.com/moby/buildkit v0.24.0 h1:qYfTl7W1SIJzWDIDCcPT8FboHIZCYfi++wvySi3eyFE=
|
||||
github.com/moby/buildkit v0.24.0/go.mod h1:4qovICAdR2H4C7+EGMRva5zgHW1gyhT4/flHI7F5F9k=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/reteps/dockerfmt v0.3.7 h1:GChhICBoy6oiTuoLTLFtGnfyBi2qY9dvHBhrcWrN8Zk=
|
||||
github.com/reteps/dockerfmt v0.3.7/go.mod h1:5lpbp1KzLWaRhL7qB6IEutHoQK3ZcT2Lb5MWPmFts74=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
|
||||
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
|
||||
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
|
||||
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
|
||||
102
frontend/src/common/prettier/plugins/docker/index.ts
Normal file
102
frontend/src/common/prettier/plugins/docker/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { Plugin, SupportLanguage, Parser, Printer, SupportOption } from 'prettier';
|
||||
import dockerfileInit, { format } from './docker_fmt_vite.js';
|
||||
|
||||
// Language configuration for Dockerfile
|
||||
const languages: SupportLanguage[] = [
|
||||
{
|
||||
name: 'dockerfile',
|
||||
parsers: ['dockerfile'],
|
||||
extensions: ['.docker', '.Dockerfile'],
|
||||
filenames: ['Dockerfile', 'dockerfile'],
|
||||
linguistLanguageId: 99,
|
||||
vscodeLanguageIds: ['dockerfile'],
|
||||
},
|
||||
];
|
||||
|
||||
// Parser configuration
|
||||
const parsers: Record<string, Parser<any>> = {
|
||||
dockerfile: {
|
||||
parse: (text: string) => {
|
||||
// For Dockerfile, we don't need complex parsing, just return the text
|
||||
// The formatting will be handled by the print function
|
||||
return { type: 'dockerfile', value: text };
|
||||
},
|
||||
astFormat: 'dockerfile',
|
||||
locStart: () => 0,
|
||||
locEnd: () => 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Printer configuration
|
||||
const printers: Record<string, Printer<any>> = {
|
||||
dockerfile: {
|
||||
// @ts-expect-error -- Support async printer like shell plugin
|
||||
async print(path: any, options: any) {
|
||||
await ensureInitialized();
|
||||
const text = path.getValue().value || path.getValue();
|
||||
|
||||
try {
|
||||
const formatted = format(text, {
|
||||
indent: options.tabWidth || 2,
|
||||
trailingNewline: true,
|
||||
spaceRedirects: options.spaceRedirects !== false,
|
||||
});
|
||||
return formatted;
|
||||
} catch (error) {
|
||||
console.warn('Dockerfile formatting error:', error);
|
||||
return text;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// WASM initialization
|
||||
let isInitialized = false;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
|
||||
async function ensureInitialized(): Promise<void> {
|
||||
if (isInitialized) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!initPromise) {
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
await dockerfileInit();
|
||||
isInitialized = true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize Dockerfile WASM module:', error);
|
||||
initPromise = null;
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
// Plugin options
|
||||
const options: Record<string, SupportOption> = {
|
||||
spaceRedirects: {
|
||||
type: 'boolean',
|
||||
category: 'Format',
|
||||
default: true,
|
||||
description: 'Add spaces around redirect operators',
|
||||
},
|
||||
};
|
||||
|
||||
// Plugin definition
|
||||
const plugin: Plugin = {
|
||||
languages,
|
||||
parsers,
|
||||
printers,
|
||||
options,
|
||||
defaultOptions: {
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
spaceRedirects: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
export { languages, parsers, printers, options };
|
||||
807
frontend/src/common/prettier/plugins/docker/lib/format.go
Normal file
807
frontend/src/common/prettier/plugins/docker/lib/format.go
Normal file
@@ -0,0 +1,807 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"github.com/moby/buildkit/frontend/dockerfile/command"
|
||||
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
)
|
||||
|
||||
type ExtendedNode struct {
|
||||
*parser.Node
|
||||
Children []*ExtendedNode
|
||||
Next *ExtendedNode
|
||||
OriginalMultiline string
|
||||
}
|
||||
|
||||
type ParseState struct {
|
||||
CurrentLine int
|
||||
Output string
|
||||
// Needed to pull in comments
|
||||
AllOriginalLines []string
|
||||
Config *Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
IndentSize uint
|
||||
TrailingNewline bool
|
||||
SpaceRedirects bool
|
||||
}
|
||||
|
||||
func FormatNode(ast *ExtendedNode, c *Config) (string, bool) {
|
||||
nodeName := strings.ToLower(ast.Node.Value)
|
||||
dispatch := map[string]func(*ExtendedNode, *Config) string{
|
||||
command.Add: formatSpaceSeparated,
|
||||
command.Arg: formatBasic,
|
||||
command.Cmd: formatCmd,
|
||||
command.Copy: formatSpaceSeparated,
|
||||
command.Entrypoint: formatEntrypoint,
|
||||
command.Env: formatEnv,
|
||||
command.Expose: formatSpaceSeparated,
|
||||
command.From: formatSpaceSeparated,
|
||||
command.Healthcheck: formatBasic,
|
||||
command.Label: formatLabel,
|
||||
command.Maintainer: formatMaintainer,
|
||||
command.Onbuild: FormatOnBuild,
|
||||
command.Run: formatRun,
|
||||
command.Shell: formatCmd,
|
||||
command.StopSignal: formatBasic,
|
||||
command.User: formatBasic,
|
||||
command.Volume: formatBasic,
|
||||
command.Workdir: formatSpaceSeparated,
|
||||
}
|
||||
|
||||
fmtFunc := dispatch[nodeName]
|
||||
if fmtFunc == nil {
|
||||
return "", false
|
||||
// log.Fatalf("Unknown command: %s %s\n", nodeName, ast.OriginalMultiline)
|
||||
}
|
||||
return fmtFunc(ast, c), true
|
||||
}
|
||||
|
||||
func (df *ParseState) processNode(ast *ExtendedNode) {
|
||||
|
||||
// We don't want to process nodes that don't have a start or end line.
|
||||
if ast.Node.StartLine == 0 || ast.Node.EndLine == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// check if we are on the correct line,
|
||||
// otherwise get the comments we are missing
|
||||
if df.CurrentLine != ast.StartLine {
|
||||
df.Output += FormatComments(df.AllOriginalLines[df.CurrentLine : ast.StartLine-1])
|
||||
df.CurrentLine = ast.StartLine
|
||||
}
|
||||
// if df.Output != "" {
|
||||
// // If the previous line isn't a comment or newline, add a newline
|
||||
// lastTwoChars := df.Output[len(df.Output)-2 : len(df.Output)]
|
||||
// lastNonTrailingNewline := strings.LastIndex(strings.TrimRight(df.Output, "\n"), "\n")
|
||||
// if lastTwoChars != "\n\n" && df.Output[lastNonTrailingNewline+1] != '#' {
|
||||
// df.Output += "\n"
|
||||
// }
|
||||
// }
|
||||
|
||||
output, ok := FormatNode(ast, df.Config)
|
||||
if ok {
|
||||
df.Output += output
|
||||
df.CurrentLine = ast.EndLine
|
||||
}
|
||||
// fmt.Printf("CurrentLine: %d, %d\n", df.CurrentLine, ast.EndLine)
|
||||
// fmt.Printf("Unknown command: %s %s\n", nodeName, ast.OriginalMultiline)
|
||||
|
||||
for _, child := range ast.Children {
|
||||
df.processNode(child)
|
||||
}
|
||||
|
||||
// fmt.Printf("CurrentLine2: %d, %d\n", df.CurrentLine, ast.EndLine)
|
||||
|
||||
if ast.Node.Next != nil {
|
||||
df.processNode(ast.Next)
|
||||
}
|
||||
}
|
||||
|
||||
func FormatOnBuild(n *ExtendedNode, c *Config) string {
|
||||
if len(n.Node.Next.Children) == 1 {
|
||||
// fmt.Printf("Onbuild: %s\n", n.Node.Next.Children[0].Value)
|
||||
output, ok := FormatNode(n.Next.Children[0], c)
|
||||
if ok {
|
||||
return strings.ToUpper(n.Node.Value) + " " + output
|
||||
}
|
||||
}
|
||||
|
||||
return n.OriginalMultiline
|
||||
}
|
||||
|
||||
func FormatFileLines(fileLines []string, c *Config) (string, error) {
|
||||
result, err := parser.Parse(strings.NewReader(strings.Join(fileLines, "")))
|
||||
if err != nil {
|
||||
log.Printf("%s\n", strings.Join(fileLines, ""))
|
||||
return "", fmt.Errorf("Error parsing file: %v", err)
|
||||
}
|
||||
|
||||
parseState := &ParseState{
|
||||
CurrentLine: 0,
|
||||
Output: "",
|
||||
AllOriginalLines: fileLines,
|
||||
}
|
||||
rootNode := BuildExtendedNode(result.AST, fileLines)
|
||||
parseState.Config = c
|
||||
parseState.processNode(rootNode)
|
||||
|
||||
// After all directives are processed, we need to check if we have any trailing comments to add.
|
||||
if parseState.CurrentLine < len(parseState.AllOriginalLines) {
|
||||
// Add the rest of the file
|
||||
parseState.Output += FormatComments(parseState.AllOriginalLines[parseState.CurrentLine:])
|
||||
}
|
||||
|
||||
parseState.Output = strings.TrimRight(parseState.Output, "\n")
|
||||
// Ensure the output ends with a newline if requested
|
||||
if c.TrailingNewline {
|
||||
parseState.Output += "\n"
|
||||
}
|
||||
return parseState.Output, nil
|
||||
}
|
||||
|
||||
func BuildExtendedNode(n *parser.Node, fileLines []string) *ExtendedNode {
|
||||
// Build an extended node from the parser node
|
||||
// This is used to add the original multiline string to the node
|
||||
// and to add the original line numbers
|
||||
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create the extended node with the current parser node
|
||||
en := &ExtendedNode{
|
||||
Node: n,
|
||||
Next: nil,
|
||||
Children: nil,
|
||||
OriginalMultiline: "", // Default to empty string
|
||||
}
|
||||
|
||||
// If we have valid start and end lines, construct the multiline representation
|
||||
if n.StartLine > 0 && n.EndLine > 0 {
|
||||
// Subtract 1 from StartLine because fileLines is 0-indexed while StartLine is 1-indexed
|
||||
for i := n.StartLine - 1; i < n.EndLine; i++ {
|
||||
en.OriginalMultiline += fileLines[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Process all children recursively
|
||||
if len(n.Children) > 0 {
|
||||
childrenNodes := make([]*ExtendedNode, 0, len(n.Children))
|
||||
for _, child := range n.Children {
|
||||
extChild := BuildExtendedNode(child, fileLines)
|
||||
if extChild != nil {
|
||||
childrenNodes = append(childrenNodes, extChild)
|
||||
}
|
||||
}
|
||||
// Replace the children with the processed ones
|
||||
en.Children = childrenNodes
|
||||
}
|
||||
|
||||
// Process the next node recursively
|
||||
if n.Next != nil {
|
||||
extNext := BuildExtendedNode(n.Next, fileLines)
|
||||
if extNext != nil {
|
||||
en.Next = extNext
|
||||
}
|
||||
}
|
||||
|
||||
return en
|
||||
}
|
||||
|
||||
func formatEnv(n *ExtendedNode, c *Config) string {
|
||||
// Only the legacy format will have a empty 3rd child
|
||||
if n.Next.Next.Next.Value == "" {
|
||||
return strings.ToUpper(n.Node.Value) + " " + n.Next.Node.Value + "=" + n.Next.Next.Node.Value + "\n"
|
||||
}
|
||||
// Otherwise, we have a valid env command
|
||||
originalTrimmed := strings.TrimLeft(n.OriginalMultiline, " \t")
|
||||
content := StripWhitespace(regexp.MustCompile(" ").Split(originalTrimmed, 2)[1], true)
|
||||
// Indent all lines with indentSize spaces
|
||||
re := regexp.MustCompile("(?m)^ *")
|
||||
content = strings.Trim(re.ReplaceAllString(content, strings.Repeat(" ", int(c.IndentSize))), " ")
|
||||
return strings.ToUpper(n.Value) + " " + content
|
||||
}
|
||||
|
||||
func formatShell(content string, hereDoc bool, c *Config) string {
|
||||
// Semicolons require special handling so we don't break the command
|
||||
// Improved semicolon support: handle escaped semicolons properly
|
||||
|
||||
// Check for unescaped semicolons - if found, try to format them properly
|
||||
if regexp.MustCompile(`[^\\];`).MatchString(content) {
|
||||
// Split by unescaped semicolons and format each part separately
|
||||
parts := regexp.MustCompile(`([^\\]);`).Split(content, -1)
|
||||
if len(parts) > 1 {
|
||||
var formattedParts []string
|
||||
for i, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
// Try to format each part individually
|
||||
formatted := formatSingleCommand(part, hereDoc, c)
|
||||
formattedParts = append(formattedParts, formatted)
|
||||
}
|
||||
// Add semicolon back except for the last part
|
||||
if i < len(parts)-1 {
|
||||
formattedParts[len(formattedParts)-1] += ";"
|
||||
}
|
||||
}
|
||||
return strings.Join(formattedParts, " ")
|
||||
}
|
||||
// If splitting didn't work, fall back to original content
|
||||
return content
|
||||
}
|
||||
// Grouped expressions aren't formatted well
|
||||
// See: https://github.com/mvdan/sh/issues/1148
|
||||
if strings.Contains(content, "{ \\") {
|
||||
return content
|
||||
}
|
||||
|
||||
if !hereDoc {
|
||||
// Here lies some cursed magic. Be careful.
|
||||
|
||||
// Replace comments with a subshell evaluation -- they won't be run so we can do this.
|
||||
content = StripWhitespace(content, true)
|
||||
lineComment := regexp.MustCompile(`(\n\s*)(#.*)`)
|
||||
lines := strings.SplitAfter(content, "\n")
|
||||
for i := range lines {
|
||||
lineTrim := strings.TrimLeft(lines[i], " \t")
|
||||
if len(lineTrim) >= 1 && lineTrim[0] == '#' {
|
||||
lines[i] = strings.ReplaceAll(lines[i], "`", "×")
|
||||
}
|
||||
}
|
||||
content = strings.Join(lines, "")
|
||||
|
||||
content = lineComment.ReplaceAllString(content, "$1`$2#`\\")
|
||||
|
||||
/*
|
||||
```
|
||||
foo \
|
||||
`#comment#`\
|
||||
&& bar
|
||||
```
|
||||
|
||||
```
|
||||
foo && \
|
||||
`#comment#` \
|
||||
bar
|
||||
```
|
||||
*/
|
||||
|
||||
commentContinuation := regexp.MustCompile(`(\\(?:\s*` + "`#.*#`" + `\\){1,}\s*)&&`)
|
||||
content = commentContinuation.ReplaceAllString(content, "&&$1")
|
||||
|
||||
// log.Printf("Content0: %s\n", content)
|
||||
lines = strings.SplitAfter(content, "\n")
|
||||
/**
|
||||
if the next line is not a comment, and we didn't start with a continuation, don't add the `&&`.
|
||||
*/
|
||||
inContinuation := false
|
||||
for i := range lines {
|
||||
lineTrim := strings.Trim(lines[i], " \t\\\n")
|
||||
// fmt.Printf("LineTrim: %s\n", lineTrim)
|
||||
nextLine := ""
|
||||
isComment := false
|
||||
nextLineIsComment := false
|
||||
if i+1 < len(lines) {
|
||||
nextLine = strings.Trim(lines[i+1], " \t\\\n")
|
||||
}
|
||||
if len(nextLine) >= 2 && nextLine[:2] == "`#" {
|
||||
nextLineIsComment = true
|
||||
}
|
||||
if len(lineTrim) >= 2 && lineTrim[:2] == "`#" {
|
||||
isComment = true
|
||||
}
|
||||
|
||||
// fmt.Printf("isComment: %v, nextLineIsComment: %v, inContinuation: %v\n", isComment, nextLineIsComment, inContinuation)
|
||||
if isComment && (inContinuation || nextLineIsComment) {
|
||||
lines[i] = strings.Replace(lines[i], "#`\\", "#`&&\\", 1)
|
||||
}
|
||||
|
||||
if len(lineTrim) >= 2 && !isComment && lineTrim[len(lineTrim)-2:] == "&&" {
|
||||
inContinuation = true
|
||||
} else if !isComment {
|
||||
inContinuation = false
|
||||
}
|
||||
}
|
||||
|
||||
content = strings.Join(lines, "")
|
||||
}
|
||||
|
||||
// Now that we have a valid bash-style command, we can format it with shfmt
|
||||
// log.Printf("Content1: %s\n", content)
|
||||
content = formatBash(content, c)
|
||||
|
||||
// log.Printf("Content2: %s\n", content)
|
||||
|
||||
if !hereDoc {
|
||||
reBacktickComment := regexp.MustCompile(`([ \t]*)(?:&& )?` + "`(#.*)#` " + `\\`)
|
||||
content = reBacktickComment.ReplaceAllString(content, "$1$2")
|
||||
|
||||
// Fixup the comment indentation
|
||||
lines := strings.SplitAfter(content, "\n")
|
||||
prevIsComment := false
|
||||
prevCommentSpacing := ""
|
||||
firstLineIsComment := false
|
||||
for i := range lines {
|
||||
lineTrim := strings.TrimLeft(lines[i], " \t")
|
||||
// fmt.Printf("LineTrim: %s, %v\n", lineTrim, prevIsComment)
|
||||
if len(lineTrim) >= 1 && lineTrim[0] == '#' {
|
||||
if i == 0 {
|
||||
firstLineIsComment = true
|
||||
lines[i] = strings.Repeat(" ", int(c.IndentSize)) + lineTrim
|
||||
}
|
||||
lineParts := strings.SplitN(lines[i], "#", 2)
|
||||
|
||||
if prevIsComment {
|
||||
lines[i] = prevCommentSpacing + "#" + lineParts[1]
|
||||
} else {
|
||||
prevCommentSpacing = lineParts[0]
|
||||
}
|
||||
prevIsComment = true
|
||||
} else {
|
||||
prevIsComment = false
|
||||
}
|
||||
}
|
||||
// TODO: this formatting isn't perfect (see tests/out/run5.dockerfile)
|
||||
if firstLineIsComment {
|
||||
lines = slices.Insert(lines, 0, "\\\n")
|
||||
}
|
||||
content = strings.Join(lines, "")
|
||||
content = strings.ReplaceAll(content, "×", "`")
|
||||
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// formatSingleCommand formats a single shell command (used for semicolon-separated commands)
|
||||
func formatSingleCommand(content string, hereDoc bool, c *Config) string {
|
||||
// Grouped expressions aren't formatted well
|
||||
// See: https://github.com/mvdan/sh/issues/1148
|
||||
if strings.Contains(content, "{ \\") {
|
||||
return content
|
||||
}
|
||||
|
||||
if !hereDoc {
|
||||
// Here lies some cursed magic. Be careful.
|
||||
|
||||
// Replace comments with a subshell evaluation -- they won't be run so we can do this.
|
||||
content = StripWhitespace(content, true)
|
||||
content = regexp.MustCompile(`#.*`).ReplaceAllString(content, "$(: comment)")
|
||||
content = strings.ReplaceAll(content, "\\\n", " ")
|
||||
content = strings.ReplaceAll(content, "\n", " ")
|
||||
content = regexp.MustCompile(`\s+`).ReplaceAllString(content, " ")
|
||||
content = strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
return formatBash(content, c)
|
||||
}
|
||||
|
||||
func formatRun(n *ExtendedNode, c *Config) string {
|
||||
// Get the original RUN command text
|
||||
hereDoc := false
|
||||
flags := n.Node.Flags
|
||||
|
||||
var content string
|
||||
if len(n.Node.Heredocs) >= 1 {
|
||||
content = n.Node.Heredocs[0].Content
|
||||
hereDoc = true
|
||||
// Check if heredoc FileDescriptor is 0 (stdin) - this is the standard for RUN commands
|
||||
if n.Node.Heredocs[0].FileDescriptor != 0 {
|
||||
log.Printf("Warning: heredoc FileDescriptor is %d, expected 0 for RUN command", n.Node.Heredocs[0].FileDescriptor)
|
||||
}
|
||||
} else {
|
||||
// We split the original multiline string by whitespace
|
||||
originalText := n.OriginalMultiline
|
||||
if n.OriginalMultiline == "" {
|
||||
// If the original multiline string is empty, use the original value
|
||||
originalText = n.Node.Original
|
||||
}
|
||||
|
||||
originalTrimmed := strings.TrimLeft(originalText, " \t")
|
||||
parts := regexp.MustCompile("[ \t]").Split(originalTrimmed, 2+len(flags))
|
||||
content = parts[1+len(flags)]
|
||||
}
|
||||
// Try to parse as JSON
|
||||
jsonItems, err := parseJSONStringArray(content)
|
||||
if err == nil {
|
||||
outStr := marshalStringArray(jsonItems)
|
||||
outStr = strings.ReplaceAll(outStr, "\",\"", "\", \"")
|
||||
content = outStr + "\n"
|
||||
} else {
|
||||
content = formatShell(content, hereDoc, c)
|
||||
if hereDoc {
|
||||
n.Node.Heredocs[0].Content = content
|
||||
content, _ = GetHeredoc(n)
|
||||
}
|
||||
}
|
||||
|
||||
if len(flags) > 0 {
|
||||
content = strings.Join(flags, " ") + " " + content
|
||||
}
|
||||
|
||||
return strings.ToUpper(n.Value) + " " + content
|
||||
}
|
||||
|
||||
func GetHeredoc(n *ExtendedNode) (string, bool) {
|
||||
if len(n.Node.Heredocs) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// printAST(n, 0)
|
||||
args := []string{}
|
||||
cur := n.Next
|
||||
for cur != nil {
|
||||
if cur.Node.Value != "" {
|
||||
args = append(args, cur.Node.Value)
|
||||
}
|
||||
cur = cur.Next
|
||||
}
|
||||
content := strings.Join(args, " ") + "\n" + n.Node.Heredocs[0].Content + n.Node.Heredocs[0].Name + "\n"
|
||||
return content, true
|
||||
}
|
||||
func formatBasic(n *ExtendedNode, c *Config) string {
|
||||
// Uppercases the command, and indent the following lines
|
||||
originalTrimmed := strings.TrimLeft(n.OriginalMultiline, " \t")
|
||||
|
||||
value, success := GetHeredoc(n)
|
||||
if !success {
|
||||
value = regexp.MustCompile(" ").Split(originalTrimmed, 2)[1]
|
||||
}
|
||||
return IndentFollowingLines(strings.ToUpper(n.Value)+" "+value, c.IndentSize)
|
||||
}
|
||||
|
||||
// marshalStringArray manually creates a JSON array string from a slice of strings
|
||||
// This avoids using encoding/json which causes reflection issues in WASM
|
||||
func marshalStringArray(items []string) string {
|
||||
if len(items) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString("[")
|
||||
|
||||
for i, item := range items {
|
||||
if i > 0 {
|
||||
result.WriteString(", ")
|
||||
}
|
||||
result.WriteString("\"")
|
||||
// Escape quotes and backslashes in the string
|
||||
escaped := strings.ReplaceAll(item, "\\", "\\\\")
|
||||
escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
|
||||
result.WriteString(escaped)
|
||||
result.WriteString("\"")
|
||||
}
|
||||
|
||||
result.WriteString("]")
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// parseJSONStringArray manually parses a JSON array string into a slice of strings
|
||||
// This avoids using encoding/json which causes reflection issues in WASM
|
||||
func parseJSONStringArray(jsonStr string) ([]string, error) {
|
||||
jsonStr = strings.TrimSpace(jsonStr)
|
||||
if !strings.HasPrefix(jsonStr, "[") || !strings.HasSuffix(jsonStr, "]") {
|
||||
return nil, fmt.Errorf("not a JSON array")
|
||||
}
|
||||
|
||||
// Remove brackets
|
||||
content := strings.TrimSpace(jsonStr[1 : len(jsonStr)-1])
|
||||
if content == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
var result []string
|
||||
var current strings.Builder
|
||||
inQuotes := false
|
||||
escaped := false
|
||||
|
||||
for i, char := range content {
|
||||
if escaped {
|
||||
switch char {
|
||||
case '"':
|
||||
current.WriteRune('"')
|
||||
case '\\':
|
||||
current.WriteRune('\\')
|
||||
case 'n':
|
||||
current.WriteRune('\n')
|
||||
case 't':
|
||||
current.WriteRune('\t')
|
||||
case 'r':
|
||||
current.WriteRune('\r')
|
||||
default:
|
||||
current.WriteRune('\\')
|
||||
current.WriteRune(char)
|
||||
}
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '"' {
|
||||
inQuotes = !inQuotes
|
||||
continue
|
||||
}
|
||||
|
||||
if !inQuotes && char == ',' {
|
||||
result = append(result, current.String())
|
||||
current.Reset()
|
||||
// Skip whitespace after comma
|
||||
for i+1 < len(content) && (content[i+1] == ' ' || content[i+1] == '\t') {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if inQuotes {
|
||||
current.WriteRune(char)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last item
|
||||
if current.Len() > 0 || len(result) > 0 {
|
||||
result = append(result, current.String())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getCmd(n *ExtendedNode, shouldSplitNode bool) []string {
|
||||
cmd := []string{}
|
||||
for node := n; node != nil; node = node.Next {
|
||||
// Split value by whitespace
|
||||
rawValue := strings.Trim(node.Node.Value, " \t")
|
||||
if len(node.Node.Flags) > 0 {
|
||||
cmd = append(cmd, node.Node.Flags...)
|
||||
}
|
||||
// log.Printf("ShouldSplitNode: %v\n", shouldSplitNode)
|
||||
if shouldSplitNode {
|
||||
parts, err := shlex.Split(rawValue)
|
||||
if err != nil {
|
||||
// Fallback: if splitting fails, use raw value as a single token to avoid exiting
|
||||
log.Printf("Error splitting: %s: %v\n", node.Node.Value, err)
|
||||
cmd = append(cmd, rawValue)
|
||||
} else {
|
||||
cmd = append(cmd, parts...)
|
||||
}
|
||||
} else {
|
||||
cmd = append(cmd, rawValue)
|
||||
}
|
||||
}
|
||||
// log.Printf("getCmd: %v\n", cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func shouldRunInShell(node string) bool {
|
||||
// https://docs.docker.com/reference/dockerfile/#entrypoint
|
||||
parts, err := shlex.Split(node)
|
||||
if err != nil {
|
||||
// If we cannot reliably split, heuristically decide based on common shell operators
|
||||
if strings.Contains(node, "&&") || strings.Contains(node, ";") || strings.Contains(node, "||") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
needsShell := false
|
||||
// This is a simplistic check to determine if we need to run in a full shell.
|
||||
for _, part := range parts {
|
||||
if part == "&&" || part == ";" || part == "||" {
|
||||
needsShell = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return needsShell
|
||||
}
|
||||
func formatEntrypoint(n *ExtendedNode, c *Config) string {
|
||||
// this can technically change behavior. https://docs.docker.com/reference/dockerfile/#understand-how-cmd-and-entrypoint-interact
|
||||
return formatCmd(n, c)
|
||||
}
|
||||
func formatCmd(n *ExtendedNode, c *Config) string {
|
||||
// printAST(n, 0)
|
||||
isJSON, ok := n.Node.Attributes["json"]
|
||||
if !ok {
|
||||
isJSON = false
|
||||
}
|
||||
|
||||
if !isJSON {
|
||||
doNotSplit := shouldRunInShell(n.Node.Next.Value)
|
||||
if doNotSplit {
|
||||
n.Next.Node.Flags = append(n.Next.Node.Flags, []string{"/bin/sh", "-c"}...)
|
||||
// Hacky workaround to tell getCmd to not split the command
|
||||
isJSON = true
|
||||
}
|
||||
}
|
||||
|
||||
cmd := getCmd(n.Next, !isJSON)
|
||||
bWithSpace := marshalStringArray(cmd)
|
||||
bWithSpace = strings.ReplaceAll(bWithSpace, "\",\"", "\", \"")
|
||||
return strings.ToUpper(n.Node.Value) + " " + bWithSpace + "\n"
|
||||
}
|
||||
|
||||
func formatSpaceSeparated(n *ExtendedNode, c *Config) string {
|
||||
isJSON, ok := n.Node.Attributes["json"]
|
||||
if !ok {
|
||||
isJSON = false
|
||||
}
|
||||
cmd, success := GetHeredoc(n)
|
||||
if !success {
|
||||
cmd = strings.Join(getCmd(n.Next, isJSON), " ")
|
||||
if len(n.Node.Flags) > 0 {
|
||||
cmd = strings.Join(n.Node.Flags, " ") + " " + cmd
|
||||
}
|
||||
cmd += "\n"
|
||||
}
|
||||
|
||||
return strings.ToUpper(n.Node.Value) + " " + cmd
|
||||
}
|
||||
|
||||
func formatLabel(n *ExtendedNode, c *Config) string {
|
||||
// Parse LABEL key-value pairs and sort them alphabetically by key
|
||||
originalTrimmed := strings.TrimLeft(n.OriginalMultiline, " \t")
|
||||
content := regexp.MustCompile(" ").Split(originalTrimmed, 2)[1]
|
||||
|
||||
// Parse key-value pairs
|
||||
labels := make(map[string]string)
|
||||
var keys []string
|
||||
|
||||
// Split by whitespace and parse key=value pairs
|
||||
parts := strings.Fields(content)
|
||||
for _, part := range parts {
|
||||
if strings.Contains(part, "=") {
|
||||
kv := strings.SplitN(part, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
key := strings.Trim(kv[0], "\"")
|
||||
value := strings.Trim(kv[1], "\"")
|
||||
labels[key] = value
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no key-value pairs found, fall back to basic formatting
|
||||
if len(keys) == 0 {
|
||||
return formatBasic(n, c)
|
||||
}
|
||||
|
||||
// Sort keys alphabetically
|
||||
slices.Sort(keys)
|
||||
|
||||
// Build sorted output
|
||||
var result strings.Builder
|
||||
result.WriteString(strings.ToUpper(n.Value))
|
||||
result.WriteString(" ")
|
||||
|
||||
for i, key := range keys {
|
||||
if i > 0 {
|
||||
result.WriteString(" ")
|
||||
}
|
||||
result.WriteString(key)
|
||||
result.WriteString("=\"")
|
||||
result.WriteString(labels[key])
|
||||
result.WriteString("\"")
|
||||
}
|
||||
|
||||
result.WriteString("\n")
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func formatMaintainer(n *ExtendedNode, c *Config) string {
|
||||
|
||||
// Get text between quotes
|
||||
maintainer := strings.Trim(n.Next.Node.Value, "\"")
|
||||
return "LABEL org.opencontainers.image.authors=\"" + maintainer + "\"\n"
|
||||
}
|
||||
|
||||
func GetFileLines(fileName string) ([]string, error) {
|
||||
// Open the file
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Read the file contents
|
||||
b := new(strings.Builder)
|
||||
io.Copy(b, f)
|
||||
fileLines := strings.SplitAfter(b.String(), "\n")
|
||||
|
||||
return fileLines, nil
|
||||
}
|
||||
|
||||
func StripWhitespace(lines string, rightOnly bool) string {
|
||||
// Split the string into lines by newlines
|
||||
// log.Printf("Lines: .%s.\n", lines)
|
||||
linesArray := strings.SplitAfter(lines, "\n")
|
||||
// Create a new slice to hold the stripped lines
|
||||
var strippedLines string
|
||||
// Iterate over each line
|
||||
for _, line := range linesArray {
|
||||
// Trim leading and trailing whitespace
|
||||
// log.Printf("Line .%s.\n", line)
|
||||
hadNewline := len(line) > 0 && line[len(line)-1] == '\n'
|
||||
if rightOnly {
|
||||
// Only trim trailing whitespace
|
||||
line = strings.TrimRight(line, " \t\n")
|
||||
} else {
|
||||
// Trim both leading and trailing whitespace
|
||||
line = strings.Trim(line, " \t\n")
|
||||
}
|
||||
|
||||
// log.Printf("Line2 .%s.", line)
|
||||
if hadNewline {
|
||||
line += "\n"
|
||||
}
|
||||
strippedLines += line
|
||||
}
|
||||
return strippedLines
|
||||
}
|
||||
|
||||
func FormatComments(lines []string) string {
|
||||
// Adds lines to the output, collapsing multiple newlines into a single newline
|
||||
// and removing leading / trailing whitespace. We can do this because
|
||||
// we are adding comments and we don't care about the formatting.
|
||||
missingContent := StripWhitespace(strings.Join(lines, ""), false)
|
||||
// Replace multiple newlines with a single newline
|
||||
re := regexp.MustCompile(`\n{3,}`)
|
||||
return re.ReplaceAllString(missingContent, "\n")
|
||||
}
|
||||
|
||||
func IndentFollowingLines(lines string, indentSize uint) string {
|
||||
// Split the input by lines
|
||||
allLines := strings.SplitAfter(lines, "\n")
|
||||
|
||||
// If there's only one line or no lines, return the original
|
||||
if len(allLines) <= 1 {
|
||||
return lines
|
||||
}
|
||||
|
||||
// Keep the first line as is
|
||||
result := allLines[0]
|
||||
// Indent all subsequent lines
|
||||
for i := 1; i < len(allLines); i++ {
|
||||
if allLines[i] != "" { // Skip empty lines
|
||||
// Remove existing indentation and add new indentation
|
||||
trimmedLine := strings.TrimLeft(allLines[i], " \t")
|
||||
allLines[i] = strings.Repeat(" ", int(indentSize)) + trimmedLine
|
||||
}
|
||||
|
||||
// Add to result (with newline except for the last line)
|
||||
result += allLines[i]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func formatBash(s string, c *Config) string {
|
||||
r := strings.NewReader(s)
|
||||
f, err := syntax.NewParser(syntax.KeepComments(true)).Parse(r, "")
|
||||
if err != nil {
|
||||
// On parse failure, return original input to avoid crashing the WASM runtime
|
||||
return s
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
syntax.NewPrinter(
|
||||
syntax.Minify(false),
|
||||
syntax.SingleLine(false),
|
||||
syntax.SpaceRedirects(c.SpaceRedirects),
|
||||
syntax.Indent(c.IndentSize),
|
||||
syntax.BinaryNextLine(true),
|
||||
).Print(buf, f)
|
||||
return buf.String()
|
||||
}
|
||||
553
frontend/src/common/prettier/plugins/docker/wasm_exec.js
Normal file
553
frontend/src/common/prettier/plugins/docker/wasm_exec.js
Normal file
@@ -0,0 +1,553 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// This file has been modified for use by the TinyGo compiler.
|
||||
|
||||
(() => {
|
||||
// Map multiple JavaScript environments to a single common API,
|
||||
// preferring web standards over Node.js API.
|
||||
//
|
||||
// Environments considered:
|
||||
// - Browsers
|
||||
// - Node.js
|
||||
// - Electron
|
||||
// - Parcel
|
||||
|
||||
if (typeof global !== "undefined") {
|
||||
// global already exists
|
||||
} else if (typeof window !== "undefined") {
|
||||
window.global = window;
|
||||
} else if (typeof self !== "undefined") {
|
||||
self.global = self;
|
||||
} else {
|
||||
throw new Error("cannot export Go (neither global, window nor self is defined)");
|
||||
}
|
||||
|
||||
if (!global.require && typeof require !== "undefined") {
|
||||
global.require = require;
|
||||
}
|
||||
|
||||
if (!global.fs && global.require) {
|
||||
global.fs = require("node:fs");
|
||||
}
|
||||
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!global.fs) {
|
||||
let outputBuf = "";
|
||||
global.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substr(0, nl));
|
||||
outputBuf = outputBuf.substr(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.process) {
|
||||
global.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!global.crypto) {
|
||||
const nodeCrypto = require("node:crypto");
|
||||
global.crypto = {
|
||||
getRandomValues(b) {
|
||||
nodeCrypto.randomFillSync(b);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.performance) {
|
||||
global.performance = {
|
||||
now() {
|
||||
const [sec, nsec] = process.hrtime();
|
||||
return sec * 1000 + nsec / 1000000;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.TextEncoder) {
|
||||
global.TextEncoder = require("node:util").TextEncoder;
|
||||
}
|
||||
|
||||
if (!global.TextDecoder) {
|
||||
global.TextDecoder = require("node:util").TextDecoder;
|
||||
}
|
||||
|
||||
// End of polyfills for common API.
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let reinterpretBuf = new DataView(new ArrayBuffer(8));
|
||||
var logLine = [];
|
||||
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
|
||||
|
||||
global.Go = class {
|
||||
constructor() {
|
||||
this._callbackTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const mem = () => {
|
||||
// The buffer may change when requesting more memory.
|
||||
return new DataView(this._inst.exports.memory.buffer);
|
||||
}
|
||||
|
||||
const unboxValue = (v_ref) => {
|
||||
reinterpretBuf.setBigInt64(0, v_ref, true);
|
||||
const f = reinterpretBuf.getFloat64(0, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = v_ref & 0xffffffffn;
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
|
||||
const loadValue = (addr) => {
|
||||
let v_ref = mem().getBigUint64(addr, true);
|
||||
return unboxValue(v_ref);
|
||||
}
|
||||
|
||||
const boxValue = (v) => {
|
||||
const nanHead = 0x7FF80000n;
|
||||
|
||||
if (typeof v === "number") {
|
||||
if (isNaN(v)) {
|
||||
return nanHead << 32n;
|
||||
}
|
||||
if (v === 0) {
|
||||
return (nanHead << 32n) | 1n;
|
||||
}
|
||||
reinterpretBuf.setFloat64(0, v, true);
|
||||
return reinterpretBuf.getBigInt64(0, true);
|
||||
}
|
||||
|
||||
switch (v) {
|
||||
case undefined:
|
||||
return 0n;
|
||||
case null:
|
||||
return (nanHead << 32n) | 2n;
|
||||
case true:
|
||||
return (nanHead << 32n) | 3n;
|
||||
case false:
|
||||
return (nanHead << 32n) | 4n;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = BigInt(this._values.length);
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 1n;
|
||||
switch (typeof v) {
|
||||
case "string":
|
||||
typeFlag = 2n;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3n;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4n;
|
||||
break;
|
||||
}
|
||||
return id | ((nanHead | typeFlag) << 32n);
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
let v_ref = boxValue(v);
|
||||
mem().setBigUint64(addr, v_ref, true);
|
||||
}
|
||||
|
||||
const loadSlice = (array, len, cap) => {
|
||||
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (array, len, cap) => {
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (ptr, len) => {
|
||||
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
wasi_snapshot_preview1: {
|
||||
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
|
||||
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
|
||||
let nwritten = 0;
|
||||
if (fd == 1) {
|
||||
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
|
||||
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
|
||||
let ptr = mem().getUint32(iov_ptr + 0, true);
|
||||
let len = mem().getUint32(iov_ptr + 4, true);
|
||||
nwritten += len;
|
||||
for (let i=0; i<len; i++) {
|
||||
let c = mem().getUint8(ptr+i);
|
||||
if (c == 13) { // CR
|
||||
// ignore
|
||||
} else if (c == 10) { // LF
|
||||
// write line
|
||||
let line = decoder.decode(new Uint8Array(logLine));
|
||||
logLine = [];
|
||||
console.log(line);
|
||||
} else {
|
||||
logLine.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('invalid file descriptor:', fd);
|
||||
}
|
||||
mem().setUint32(nwritten_ptr, nwritten, true);
|
||||
return 0;
|
||||
},
|
||||
fd_close: () => 0, // dummy
|
||||
fd_fdstat_get: () => 0, // dummy
|
||||
fd_seek: () => 0, // dummy
|
||||
proc_exit: (code) => {
|
||||
this.exited = true;
|
||||
this.exitCode = code;
|
||||
this._resolveExitPromise();
|
||||
throw wasmExit;
|
||||
},
|
||||
random_get: (bufPtr, bufLen) => {
|
||||
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
gojs: {
|
||||
// func ticks() int64
|
||||
"runtime.ticks": () => {
|
||||
return BigInt((timeOrigin + performance.now()) * 1e6);
|
||||
},
|
||||
|
||||
// func sleepTicks(timeout int64)
|
||||
"runtime.sleepTicks": (timeout) => {
|
||||
// Do not sleep, only reactivate scheduler after the given timeout.
|
||||
setTimeout(() => {
|
||||
if (this.exited) return;
|
||||
try {
|
||||
this._inst.exports.go_scheduler();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
}, Number(timeout)/1e6);
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (v_ref) => {
|
||||
// Note: TinyGo does not support finalizers so this is only called
|
||||
// for one specific case, by js.go:jsString. and can/might leak memory.
|
||||
const id = v_ref & 0xffffffffn;
|
||||
if (this._goRefCounts?.[id] !== undefined) {
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
} else {
|
||||
console.error("syscall/js.finalizeRef: unknown id", id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (value_ptr, value_len) => {
|
||||
value_ptr >>>= 0;
|
||||
const s = loadString(value_ptr, value_len);
|
||||
return boxValue(s);
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
|
||||
let prop = loadString(p_ptr, p_len);
|
||||
let v = unboxValue(v_ref);
|
||||
let result = Reflect.get(v, prop);
|
||||
return boxValue(result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const p = loadString(p_ptr, p_len);
|
||||
const x = unboxValue(x_ref);
|
||||
Reflect.set(v, p, x);
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const p = loadString(p_ptr, p_len);
|
||||
Reflect.deleteProperty(v, p);
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (v_ref, i) => {
|
||||
return boxValue(Reflect.get(unboxValue(v_ref), i));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
|
||||
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const name = loadString(m_ptr, m_len);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
try {
|
||||
const m = Reflect.get(v, name);
|
||||
storeValue(ret_addr, Reflect.apply(m, v, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr + 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||
try {
|
||||
const v = unboxValue(v_ref);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
storeValue(ret_addr, Reflect.apply(v, undefined, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr + 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
try {
|
||||
storeValue(ret_addr, Reflect.construct(v, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr+ 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (v_ref) => {
|
||||
return unboxValue(v_ref).length;
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
|
||||
const s = String(unboxValue(v_ref));
|
||||
const str = encoder.encode(s);
|
||||
storeValue(ret_addr, str);
|
||||
mem().setInt32(ret_addr + 8, str.length, true);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
|
||||
const str = unboxValue(v_ref);
|
||||
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
|
||||
return unboxValue(v_ref) instanceof unboxValue(t_ref);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
|
||||
let num_bytes_copied_addr = ret_addr;
|
||||
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||
|
||||
const dst = loadSlice(dest_addr, dest_len);
|
||||
const src = unboxValue(src_ref);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||
},
|
||||
|
||||
// copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
// Originally copied from upstream Go project, then modified:
|
||||
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
|
||||
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
|
||||
let num_bytes_copied_addr = ret_addr;
|
||||
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||
|
||||
const dst = unboxValue(dst_ref);
|
||||
const src = loadSlice(src_addr, src_len);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
|
||||
// For compatibility, we use both as long as Go 1.20 is supported.
|
||||
this.importObject.env = this.importObject.gojs;
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
this._inst = instance;
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
global,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map(); // mapping from JS values to reference ids
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
this.exitCode = 0;
|
||||
|
||||
if (this._inst.exports._start) {
|
||||
let exitPromise = new Promise((resolve, reject) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
|
||||
// Run program, but catch the wasmExit exception that's thrown
|
||||
// to return back here.
|
||||
try {
|
||||
this._inst.exports._start();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
|
||||
await exitPromise;
|
||||
return this.exitCode;
|
||||
} else {
|
||||
this._inst.exports._initialize();
|
||||
}
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
try {
|
||||
this._inst.exports.resume();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
global.require &&
|
||||
// global.require.main === module &&
|
||||
global.process &&
|
||||
global.process.versions &&
|
||||
!global.process.versions.electron
|
||||
) {
|
||||
if (process.argv.length != 3) {
|
||||
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const go = new Go();
|
||||
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
|
||||
let exitCode = await go.run(result.instance);
|
||||
process.exit(exitCode);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -60,7 +60,7 @@ function initGofmt(): Promise<void> {
|
||||
// Printer configuration
|
||||
const goPrinter: Printer<string> = {
|
||||
// @ts-expect-error -- Support async printer like shell plugin
|
||||
async print(path, options) {
|
||||
async print(path, _options) {
|
||||
try {
|
||||
// Wait for initialization to complete
|
||||
await initGofmt();
|
||||
|
||||
@@ -39,7 +39,7 @@ const groovyPrinter: Printer<string> = {
|
||||
return groovyBeautify(path.node, {
|
||||
width: options.printWidth || 80,
|
||||
}).trim();
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return path.node;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ type ModifierNode = JavaNonTerminal & {
|
||||
annotation?: AnnotationCstNode[];
|
||||
};
|
||||
};
|
||||
type IsTuple<T> = T extends [] ? true : T extends [infer First, ...infer Remain] ? IsTuple<Remain> : false;
|
||||
type IsTuple<T> = T extends [] ? true : T extends [infer _First, ...infer Remain] ? IsTuple<Remain> : false;
|
||||
type IndexProperties<T extends {
|
||||
length: number;
|
||||
}> = IsTuple<T> extends true ? Exclude<Partial<T>["length"], T["length"]> : number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export function format(input: string, filename: string, config?: Config): string;
|
||||
|
||||
interface LayoutConfig {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const format: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
export const __wbindgen_export_0: (a: number, b: number) => number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export function format(input: string, path?: string, config?: Config): string;
|
||||
|
||||
export interface Config {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const format: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
export const __wbindgen_export_0: (a: number, b: number) => number;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user