81 Commits

Author SHA1 Message Date
dc4b73406d 🐛 Fixed prettier plugin parser configuration issue 2025-09-21 23:23:43 +08:00
0012a5dc19 Added loading animation switch 2025-09-21 22:17:41 +08:00
3cda88371e 🐛 Fixed manual Chunks issue 2025-09-21 21:54:39 +08:00
0338351680 Improve app launch speed 2025-09-21 03:04:23 +08:00
e372a0dd7c 🎨 Modify the rendering logic of the checkbox 2025-09-21 01:14:33 +08:00
d597f379ff 🚚 Refactor directory structure 2025-09-21 00:11:40 +08:00
9222a52d91 Modified sql prettier plugin 2025-09-20 19:18:33 +08:00
c3670bb8cd Added dockerfile、lua prettier plugin 2025-09-20 01:30:16 +08:00
2ea3456ff7 Added dart prettier plugin 2025-09-19 19:41:48 +08:00
c9379f0edb Added python prettier plugin 2025-09-19 19:35:48 +08:00
f72010bd69 Added clang prettier plugin 2025-09-19 19:17:13 +08:00
cd027097f8 🐛 Fixed golang prettier plugin issue 2025-09-19 18:39:41 +08:00
9cbbf729c0 🔥 Remove powershell prettier plugin 2025-09-18 00:13:07 +08:00
landaiqing
c26c11e253 🐛 Fixed build issue 2025-09-17 09:54:43 +08:00
338ac358db 🚧 Modify toml,powershell prettier plugin(beta) 2025-09-17 00:12:39 +08:00
a83c7139c9 Added scala、powershell、groovy prettier plugin 2025-09-14 23:45:01 +08:00
42c7d11c09 📦 Optimized packaging 2025-09-13 20:25:19 +08:00
5ca5aa64c7 Added shell prettier plugin 2025-09-13 19:21:06 +08:00
eda7ef771e Added rust prettier plugin 2025-09-13 00:02:17 +08:00
593c4d7783 Added java prettier plugin 2025-09-12 23:01:19 +08:00
d24a522b32 Added php prettier plugin 2025-09-12 20:15:56 +08:00
41afb834ae Added sql prettier plugin 2025-09-12 00:52:19 +08:00
b745329e26 Added golang prettier plugin 2025-09-11 20:42:39 +08:00
1fb4f64cb3 Add update notifications 2025-09-06 01:21:02 +08:00
1f8e8981ce 🐛 Fixed version generation issues 2025-09-05 22:40:09 +08:00
a257d30dba 🎨 Modify configuration migration policy 2025-09-05 22:07:00 +08:00
97ee3b0667 🐛 Fixed configuration merge override issue 2025-09-05 00:36:33 +08:00
8e2bafba5f Optimize window snapping performance 2025-09-04 00:20:30 +08:00
6149bc133d 🐛 Fixed window pinning issue 2025-09-02 23:59:04 +08:00
5f22ee3b1f ♻️ Refactoring configuration migration service 2025-08-31 17:48:41 +08:00
fa72ff8061 Merge remote-tracking branch 'github/dependabot/go_modules/github.com/ulikunitz/xz-0.5.14' 2025-08-30 19:00:36 +08:00
65f24860e6 Added window snapping function toggle 2025-08-30 00:18:29 +08:00
dependabot[bot]
4881233211 ⬆️ Bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14
Bumps [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) from 0.5.12 to 0.5.14.
- [Commits](https://github.com/ulikunitz/xz/compare/v0.5.12...v0.5.14)

---
updated-dependencies:
- dependency-name: github.com/ulikunitz/xz
  dependency-version: 0.5.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 19:44:41 +00:00
bc01fdf362 Added window snapping feature 2025-08-24 16:07:48 +08:00
709998ff9c 🐛 Fixed hotkey crashes 2025-08-19 22:26:55 +08:00
6adeadeed4 🐛 Fixed document deadlock issue 2025-08-19 00:24:51 +08:00
7b70a39b23 🐛 Fixed hotkey service issues 2025-08-19 00:08:50 +08:00
873a3c0e60 Modify the theme storage schema 2025-08-17 19:34:35 +08:00
5b88efcfbe 🐛 Fixed the window toggle maximise issue 2025-08-17 14:51:39 +08:00
f37c659c89 🐛 Adjusted error message and icon clearing logic 2025-07-17 11:03:42 +08:00
9fff7bcfca Added the backup feature 2025-07-17 00:12:00 +08:00
b4b0ad9bba 🎨 Optimize storage logic 2025-07-13 22:32:58 +08:00
6d8fdf62f1 💡 Update docs 2025-07-13 11:58:53 +08:00
9f53d7421d Merge remote-tracking branch 'github/master' 2025-07-12 23:56:43 +08:00
80c8ecb4cf 💡 Update docs 2025-07-12 23:56:04 +08:00
d10059a82d Create CNAME 2025-07-12 22:25:19 +08:00
737f83cd5f 💡 Add docs 2025-07-12 22:14:35 +08:00
a720a4cfb8 Complete the custom editor theme 2025-07-11 23:03:28 +08:00
b5510d605c Add multi-window document functionality 2025-07-10 18:45:51 +08:00
4d62da912a ⬆️ Upgrade wails v3 from Alpha 9 to Alpha 10 2025-07-10 10:01:52 +08:00
b52e067d50 🍎 Fix build issues 2025-07-08 17:36:45 +08:00
8dce06c30e 🐛 Fixed the reboot issue on different platforms 2025-07-08 12:41:30 +08:00
b404434b5b 🎨 Updated 2025-07-07 16:09:09 +08:00
685897e828 🎨 Updated 2025-07-07 15:55:48 +08:00
4f1d70135e 📝 README.md 2025-07-07 15:41:46 +08:00
4dc424781b 📄 Change the license 2025-07-07 13:39:54 +08:00
7fcfc5e992 🎨 Refactoring the extension service and the keybinding service 2025-07-07 13:19:59 +08:00
7c2318a13f Add translation features 2025-07-06 23:41:15 +08:00
a2a332e735 Added context menu 2025-07-04 14:37:35 +08:00
ebee33ea7c Add self-updating service 2025-07-03 15:21:01 +08:00
81eb2c94ac 🐛 Fixed some issues 2025-07-02 12:10:46 +08:00
25e1a98932 🐛 Fixed block formatting issue 2025-07-01 23:55:15 +08:00
1ccee779ae 🐛 Fixed extension management issues 2025-07-01 23:25:24 +08:00
3e45e6aa9b Performance optimization 2025-07-01 20:11:27 +08:00
1604564e63 Complete multi-document mode 2025-07-01 18:16:05 +08:00
70d88dabba Use SQLite instead of JSON storage 2025-06-29 23:42:06 +08:00
6f8775472d Add selection box extension 2025-06-25 23:50:57 +08:00
a9b967aba4 🐛 Fixed the issue of text highlighting expansion 2025-06-25 22:53:00 +08:00
69957a16cf 🎨 binding keymap and extension 2025-06-25 21:09:21 +08:00
650884cb85 Improve extension management 2025-06-25 17:50:50 +08:00
8e91e3cf7c Add extension management page 2025-06-24 19:18:30 +08:00
f3bcb87828 Add extension management service 2025-06-24 14:16:53 +08:00
ea025e3f5d Add update service 2025-06-23 19:42:48 +08:00
4f8272e290 Add configuration merge service 2025-06-23 12:03:56 +08:00
d6dd34db87 ♻️ Refactor and clean up the code 2025-06-22 21:30:45 +08:00
eb9b037f8e 🎨 Optimize code 2025-06-22 15:08:38 +08:00
35c89e086e 🎨 Updated 2025-06-22 12:08:50 +08:00
77287bccfa 🎨 Updated 2025-06-21 19:05:08 +08:00
a92e5486b2 🐛 Fixed theme and packaging issues 2025-06-21 15:33:50 +08:00
1153c0a652 🐛 Fixed bug 2025-06-21 15:04:04 +08:00
145b868a44 🐛 Fixed the issue of deleting blocks 2025-06-21 00:34:22 +08:00
375 changed files with 49690 additions and 7556 deletions

83
LICENSE
View File

@@ -1,73 +1,18 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
MIT License
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (c) 2025 landaiqing
1. Definitions.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright 2025 landaiqing
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

174
README.md
View File

@@ -1,59 +1,163 @@
# Welcome to Your New Wails3 Project!
# <img src="./frontend/public/appicon.png" alt="voidraft Logo" width="32" height="32" style="vertical-align: middle;"> voidraft
Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running.
[中文](README_ZH.md) | **English**
## Getting Started
> *An elegant text snippet recording tool designed for developers.*
1. Navigate to your project directory in the terminal.
voidraft is a modern developer-focused text editor that allows you to record, organize, and manage various text snippets anytime, anywhere. Whether it's temporary code snippets, API responses, meeting notes, or daily to-do lists, voidraft provides a smooth and elegant editing experience.
2. To run your application in development mode, use the following command:
## Core Features
```
wails3 dev
```
### Developer-Friendly
This will start your application and enable hot-reloading for both frontend and backend changes.
- Multi-language code block support - Syntax highlighting for 30+ programming languages
- Smart language detection - Automatically recognizes code block language types
- Code formatting - Built-in Prettier support for one-click code beautification
- Block editing mode - Split content into independent code blocks, each with different language settings
- Multi-window support - edit multiple documents at the same time
- Support for custom themes - Custom editor themes
3. To build your application for production, use:
### Modern Interface
```
wails3 build
```
- Clean and elegant design - Simple yet sophisticated user interface
- Dark/Light themes - Adapts to different usage scenarios
- Multi-language support - Built-in internationalization with multiple language switching
This will create a production-ready executable in the `build` directory.
### Extension System
## Exploring Wails3 Features
- Modular architecture - Extensible editor based on CodeMirror 6
- Rich extensions - Includes various practical editor extensions
- Rainbow bracket matching
- VSCode-style search and replace
- Color picker
- Built-in translation tool
- Text highlighting
- Code folding
- Hyperlink support
- Checkbox support
- Minimap
Now that you have your project set up, it's time to explore the features that Wails3 offers:
## Quick Start
1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications.
### Download and Use
2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
1. Visit the releases page: https://github.com/landaiqing/voidraft/releases
2. Select and download the appropriate version
3. Run the installer and start using
```
go run .
```
### Development Environment
Note: Some examples may be under development during the alpha phase.
```bash
# Clone the project
git clone https://github.com/landaiqing/voidraft
cd voidraft
3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3alpha.wails.io/) for in-depth guides and API references.
# Install frontend dependencies
cd frontend
npm install
npm run build
cd ..
4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions).
# Start development server
wails3 dev
```
### Production Build
```bash
# Build application
wails3 package
```
After building, the executable will be generated in the `bin` directory.
## Technical Architecture
### Core Technologies
| Layer | Technology Stack |
|-------|------------------|
| Desktop Framework | Wails3 |
| Backend Language | Go 1.21+ |
| Frontend Framework | Vue 3 + TypeScript |
| Editor Core | CodeMirror 6 |
| Build Tool | Vite |
| Data Storage | SQLite |
## Project Structure
Take a moment to familiarize yourself with your project structure:
```
voidraft/
├── frontend/ # Vue 3 frontend application
│ ├── src/
│ │ ├── views/editor/ # Editor core views
│ │ │ ├── extensions/ # Editor extension system
│ │ │ │ ├── codeblock/ # Code block support
│ │ │ │ ├── translator/ # Translation tool
│ │ │ │ ├── minimap/ # Code minimap
│ │ │ │ ├── vscodeSearch/ # VSCode-style search
│ │ │ │ └── ... # More extensions
│ │ │ └── ...
│ │ ├── components/ # Reusable components
│ │ ├── stores/ # Pinia state management
│ │ └── utils/ # Utility functions
│ └── public/ # Static assets
├── internal/ # Go backend core
│ ├── services/ # Core business services
│ ├── models/ # Data model definitions
│ └── events/ # Event handling mechanisms
└── main.go # Application entry point
```
- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript)
- `main.go`: The entry point of your Go backend
- `app.go`: Define your application structure and methods here
- `wails.json`: Configuration file for your Wails project
## Development Roadmap
## Next Steps
### Platform Extension Plans
1. Modify the frontend in the `frontend/` directory to create your desired UI.
2. Add backend functionality in `main.go`.
3. Use `wails3 dev` to see your changes in real-time.
4. When ready, build your application with `wails3 build`.
| Platform | Status | Expected Time |
|----------|--------|---------------|
| macOS | Planned | Future versions |
| Linux | Planned | Future versions |
Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community.
### Planned Features
- ✅ Custom themes - Customize editor themes
- ✅ Multi-window support - Support editing multiple documents simultaneously
- ✅ Data synchronization - Cloud backup for documents
- [ ] Enhanced clipboard - Monitor and manage clipboard history
- [ ] Extension system - Support for custom plugins
## Acknowledgments
> Standing on the shoulders of giants, paying tribute to the open source spirit
The birth of voidraft is inseparable from the following excellent open source projects:
### Special Thanks
- **[Heynote](https://github.com/heyman/heynote/)** - voidraft is a feature-enhanced version based on Heynote's concept
- Inherits Heynote's elegant block editing philosophy
- Adds more practical features on the original foundation
- Rebuilt with modern technology stack
- Provides richer extension system and customization options
- Thanks to the Heynote team for bringing innovative ideas to the developer community
### Core Technology Stack
| Technology | Purpose | Link |
|------------|---------|------|
| Wails3 | Cross-platform desktop application framework | [wails.io](https://v3alpha.wails.io/) |
| Vue.js | Progressive frontend framework | [vuejs.org](https://vuejs.org/) |
| CodeMirror 6 | Modern code editor | [codemirror.net](https://codemirror.net/) |
| Prettier | Code formatting tool | [prettier.io](https://prettier.io/) |
| TypeScript | Type-safe JavaScript | [typescriptlang.org](https://www.typescriptlang.org/) |
## License
This project is open source under the [MIT License](LICENSE).
Welcome to Fork, Star, and contribute code.
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/landaiqing/voidraft.svg?style=social&label=Star)](https://github.com/yourusername/voidraft)
[![GitHub forks](https://img.shields.io/github/forks/landaiqing/voidraft.svg?style=social&label=Fork)](https://github.com/yourusername/voidraft)
*Made with ❤️ by landaiqing*

165
README_ZH.md Normal file
View File

@@ -0,0 +1,165 @@
# <img src="./frontend/public/appicon.png" alt="voidraft Logo" width="32" height="32" style="vertical-align: middle;"> voidraft
**中文** | [English](README.md)
> *一个专为开发者打造的优雅文本片段记录工具。*
voidraft 是一个现代化的开发者专用文本编辑器让你能够随时随地记录、整理和管理各种文本片段。无论是临时的代码片段、API 响应、会议笔记还是日常的待办事项voidraft 都能为你提供流畅而优雅的编辑体验。
## 核心特性
### 开发者友好
- 多语言代码块支持 - 支持 30+ 种编程语言的语法高亮
- 智能语言检测 - 自动识别代码块语言类型
- 代码格式化 - 内置 Prettier 支持,一键美化代码
- 块状编辑模式 - 将内容分割为独立的代码块,每个块可设置不同语言
- 支持多窗口 - 同时编辑多个文档
- 支持自定义主题 - 自定义编辑器主题
### 现代化界面
- 清新脱俗的设计 - 简洁而不失优雅的用户界面
- 深色/浅色主题 - 适应不同使用场景
- 多语言支持 - 内置国际化功能,支持多种语言切换
### 扩展系统
- 模块化架构 - 基于 CodeMirror 6 的可扩展编辑器
- 丰富的扩展 - 包含多种实用编辑器扩展
- 彩虹括号匹配
- VSCode 风格搜索替换
- 颜色选择器
- 内置翻译工具
- 文本高亮
- 代码折叠
- 超链接支持
- 复选框支持
- 小地图
## 快速开始
### 下载使用
1. 访问发布页面https://github.com/landaiqing/voidraft/releases
2. 选择适合的版本下载
3. 运行安装程序,开始使用
### 开发环境
```bash
# 克隆项目
git clone https://github.com/landaiqing/voidraft
cd voidraft
# 安装前端依赖
cd frontend
npm install
npm run build
cd ..
# 启动开发服务器
wails3 dev
```
### 生产构建
```bash
# 构建应用
wails3 package
```
构建完成后,可执行文件将生成在 `bin` 目录中。
## 技术架构
### 核心技术
| 层级 | 技术栈 |
|------|--------|
| 桌面框架 | Wails3 |
| 后端语言 | Go 1.21+ |
| 前端框架 | Vue 3 + TypeScript |
| 编辑器核心 | CodeMirror 6 |
| 构建工具 | Vite |
| 数据存储 | SQLite |
## 项目结构
```
voidraft/
├── frontend/ # Vue 3 前端应用
│ ├── src/
│ │ ├── views/editor/ # 编辑器核心视图
│ │ │ ├── extensions/ # 编辑器扩展系统
│ │ │ │ ├── codeblock/ # 代码块支持
│ │ │ │ ├── translator/ # 翻译工具
│ │ │ │ ├── minimap/ # 代码小地图
│ │ │ │ ├── vscodeSearch/ # VSCode风格搜索
│ │ │ │ └── ... # 更多扩展
│ │ │ └── ...
│ │ ├── components/ # 可复用组件
│ │ ├── stores/ # Pinia 状态管理
│ │ └── utils/ # 工具函数库
│ └── public/ # 静态资源文件
├── internal/ # Go 后端核心
│ ├── services/ # 核心业务服务
│ ├── models/ # 数据模型定义
│ └── events/ # 事件处理机制
└── main.go # 应用程序入口
```
## 开发路线图
### 平台扩展计划
| 平台 | 状态 | 预期时间 |
|------|------|----------|
| macOS | 计划中 | 后续版本 |
| Linux | 计划中 | 后续版本 |
### 计划添加的功能
- ✅ 自定义主题 - 自定义编辑器主题
- ✅ 多窗口支持 - 支持同时编辑多个文档
- ✅ 数据同步 - 文档云端备份
- [ ] 剪切板增强 - 监听和管理剪切板历史
- [ ] 扩展系统 - 支持自定义插件
## 致谢
> 站在巨人的肩膀上,致敬开源精神
voidraft 的诞生离不开以下优秀的开源项目:
### 特别感谢
- **[Heynote](https://github.com/heyman/heynote/)** - voidraft 是基于 Heynote 概念的功能增强版本
- 继承了 Heynote 优雅的块状编辑理念
- 在原有基础上增加了更多实用功能
- 采用现代化技术栈重新构建
- 提供更丰富的扩展系统和自定义选项
- 感谢 Heynote 团队为开发者社区带来的创新思路
### 核心技术栈
| 技术 | 用途 | 链接 |
|------|------|------------------------------------------------------|
| Wails3 | 跨平台桌面应用框架 | [wails.io](https://v3alpha.wails.io/) |
| Vue.js | 渐进式前端框架 | [vuejs.org](https://vuejs.org/) |
| CodeMirror 6 | 现代化代码编辑器 | [codemirror.net](https://codemirror.net/) |
| Prettier | 代码格式化工具 | [prettier.io](https://prettier.io/) |
| TypeScript | 类型安全的 JavaScript | [typescriptlang.org](https://www.typescriptlang.org/) |
## 许可证
本项目采用 [MIT 许可证](LICENSE) 开源。
欢迎 Fork、Star 和贡献代码。
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/landaiqing/voidraft.svg?style=social&label=Star)](https://github.com/yourusername/voidraft)
[![GitHub forks](https://img.shields.io/github/forks/landaiqing/voidraft.svg?style=social&label=Fork)](https://github.com/yourusername/voidraft)
*Made with ❤️ by landaiqing*

View File

@@ -12,13 +12,25 @@ vars:
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
tasks:
version:
summary: Generate version information
cmds:
- '{{if eq OS "windows"}}cmd /c ".\scripts\version.bat"{{else}}bash ./scripts/version.sh{{end}}'
sources:
- scripts/version.bat
- scripts/version.sh
generates:
- version.txt
build:
summary: Builds the application
deps: [version]
cmds:
- task: "{{OS}}:build"
package:
summary: Packages a production build of the application
deps: [version]
cmds:
- task: "{{OS}}:package"

View File

@@ -5,12 +5,12 @@ version: '3'
# This information is used to generate the build assets.
info:
companyName: "Voidraft" # The name of the company
productName: "Voidraft" # The name of the application
companyName: "voidraft" # The name of the company
productName: "voidraft" # The name of the application
productIdentifier: "landaiqing" # The unique product identifier
description: "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design" # The application description
copyright: "© 2025 Voidraft. All rights reserved." # Copyright text
comments: "Effortlessly capture and organize fleeting ideas with minimal design" # Comments
description: "voidraft" # The application description
copyright: "© 2025 voidraft. All rights reserved." # Copyright text
comments: "voidraft" # Comments
version: "0.0.1.0" # The application version
# Dev mode configuration

View File

@@ -4,7 +4,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>Voidraft</string>
<string>voidraft</string>
<key>CFBundleExecutable</key>
<string>voidraft</string>
<key>CFBundleIdentifier</key>
@@ -12,7 +12,7 @@
<key>CFBundleVersion</key>
<string>0.0.1.0</string>
<key>CFBundleGetInfoString</key>
<string>Effortlessly capture and organize fleeting ideas with minimal design</string>
<string>voidraft</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1.0</string>
<key>CFBundleIconFile</key>
@@ -22,7 +22,7 @@
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© 2025 Voidraft. All rights reserved.</string>
<string>© 2025 voidraft. All rights reserved.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>

View File

@@ -4,7 +4,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>Voidraft</string>
<string>voidraft</string>
<key>CFBundleExecutable</key>
<string>voidraft</string>
<key>CFBundleIdentifier</key>
@@ -12,7 +12,7 @@
<key>CFBundleVersion</key>
<string>0.0.1.0</string>
<key>CFBundleGetInfoString</key>
<string>Effortlessly capture and organize fleeting ideas with minimal design</string>
<string>voidraft</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1.0</string>
<key>CFBundleIconFile</key>
@@ -22,6 +22,6 @@
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© 2025 Voidraft. All rights reserved.</string>
<string>© 2025 voidraft. All rights reserved.</string>
</dict>
</plist>

View File

@@ -11,9 +11,12 @@ tasks:
- task: common:build:frontend
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.OUTPUT}}
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s{{else}}{{end}}'
VERSION_FLAGS:
sh: 'grep "VERSION=" version.txt | cut -d"=" -f2 | xargs -I {} echo "-X voidraft/internal/version.Version={}"'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env:

View File

@@ -11,9 +11,12 @@ tasks:
- task: common:build:frontend
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.BIN_DIR}}/{{.APP_NAME}}
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s{{else}}{{end}}'
VERSION_FLAGS:
sh: 'grep "VERSION=" version.txt | cut -d"=" -f2 | xargs -I {} echo "-X voidraft/internal/version.Version={}"'
env:
GOOS: linux
CGO_ENABLED: 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -10,9 +10,9 @@ version: "0.0.1.0"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
description: "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design"
vendor: "Voidraft"
homepage: "https://wails.io"
description: "voidraft"
vendor: "voidraft"
homepage: "https://voidraft.landaiqing.cn"
license: "MIT"
release: "1"

View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=voidraft
Exec=voidraft
Icon=appicon
Categories=Development;
Terminal=false
Keywords=wails
Version=1.0
StartupNotify=false

View File

@@ -14,13 +14,16 @@ tasks:
- task: common:generate:icons
cmds:
- task: generate:syso
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
- cmd: powershell Remove-item *.syso
platforms: [windows]
- cmd: rm -f *.syso
platforms: [linux, darwin]
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s -H windowsgui{{else}}{{end}}'
VERSION_FLAGS:
sh: 'powershell -Command "(Get-Content version.txt) -replace ''VERSION='', ''-X voidraft/internal/version.Version=''"'
env:
GOOS: windows
CGO_ENABLED: 1

View File

@@ -5,11 +5,11 @@
"info": {
"0000": {
"ProductVersion": "0.0.1.0",
"CompanyName": "Voidraft",
"FileDescription": "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design",
"LegalCopyright": "© 2025 Voidraft. All rights reserved.",
"ProductName": "Voidraft",
"Comments": "Effortlessly capture and organize fleeting ideas with minimal design"
"CompanyName": "voidraft",
"FileDescription": "voidraft",
"LegalCopyright": "© 2025 voidraft. All rights reserved.",
"ProductName": "voidraft",
"Comments": "voidraft"
}
}
}

View File

@@ -8,16 +8,16 @@
!define INFO_PROJECTNAME "voidraft"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "Voidraft"
!define INFO_COMPANYNAME "voidraft"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "Voidraft"
!define INFO_PRODUCTNAME "voidraft"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "0.0.1.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "© 2025 Voidraft. All rights reserved."
!define INFO_COPYRIGHT "© 2025 voidraft. All rights reserved."
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
voidraft.landaiqing.cn

75
docs/changelog.html Normal file
View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>voidraft - Changelog</title>
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="css/changelog.css">
<link rel="icon" href="img/favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="theme-dark">
<div class="container">
<!-- 主卡片 -->
<div class="card">
<div class="card-header">
<h1 class="card-title" data-en="voidraft Changelog" data-zh="voidraft 更新日志">voidraft Changelog</h1>
<div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
</button>
<button id="lang-toggle" class="btn btn-secondary">
<i class="fas fa-language"></i> 中/EN
</button>
</div>
</div>
<div class="card-content">
<!-- 导航区域 -->
<div class="nav-links">
<a href="index.html" class="btn btn-secondary">
<i class="fas fa-home"></i> <span data-en="Home" data-zh="首页">Home</span>
</a>
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary">
<i class="fab fa-github"></i> <span data-en="Source Code" data-zh="源代码">Source Code</span>
</a>
</div>
<!-- 加载中提示 -->
<div id="loading" class="loading-container">
<div class="loading-spinner"></div>
<p data-en="Loading releases..." data-zh="正在加载版本信息...">Loading releases...</p>
</div>
<!-- 更新日志内容 -->
<div id="changelog" class="changelog-container">
<!-- 通过JavaScript动态填充内容 -->
</div>
<!-- 错误信息 -->
<div id="error-message" class="error-container" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
<p data-en="Failed to load release information. Please try again later."
data-zh="加载版本信息失败,请稍后再试。">Failed to load release information. Please try again later.</p>
</div>
</div>
<!-- 页脚 -->
<footer class="footer">
<p class="footer-text" data-en="© 2025 voidraft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 voidraft - 专为开发者打造的优雅文本片段记录工具">© 2023-2024 voidraft - An elegant text snippet recording tool designed for developers</p>
<div class="footer-links">
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
</div>
</footer>
</div>
</div>
<script src="js/script.js"></script>
<script src="js/changelog.js"></script>
</body>
</html>

347
docs/css/changelog.css Normal file
View File

@@ -0,0 +1,347 @@
/* 更新日志页面样式 */
.nav-links {
margin-bottom: 30px;
display: flex;
gap: 15px;
}
.loading-container {
text-align: center;
padding: 40px 0;
background-color: transparent;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--primary-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.theme-dark .loading-spinner {
border-color: rgba(255, 255, 255, 0.1);
border-left-color: var(--primary-color);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-container {
text-align: center;
color: var(--error-color);
padding: 20px;
border: 2px dashed var(--error-color);
margin: 20px 0;
border-radius: 4px;
background-color: rgba(var(--card-bg-rgb), 0.7);
}
.error-container i {
font-size: 24px;
margin-bottom: 10px;
}
/* 更新日志容器 */
.changelog-container {
display: none;
position: relative;
z-index: 1;
}
.release {
margin-bottom: 40px;
border-left: 4px solid var(--primary-color);
padding-left: 20px;
background-color: rgba(var(--card-bg-rgb), 0.5);
padding: 15px 20px;
border-radius: 4px;
}
.release-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.release-version {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
}
.release-date {
color: var(--text-color);
opacity: 0.7;
font-size: 14px;
}
.release-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 10px;
background-color: var(--primary-color);
color: #000;
}
.release-badge.pre-release {
background-color: var(--warning-color);
}
.release-description {
margin-bottom: 20px;
line-height: 1.6;
}
.release-assets {
background-color: rgba(var(--light-bg-rgb), 0.7);
padding: 15px;
border-radius: 4px;
margin-top: 15px;
}
.release-assets-title {
font-size: 16px;
margin-bottom: 10px;
}
.asset-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.asset-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}
.asset-item:last-child {
border-bottom: none;
}
.asset-icon {
margin-right: 10px;
color: var(--accent-color);
}
.asset-name {
flex-grow: 1;
}
.asset-size {
font-size: 12px;
color: var(--text-color);
opacity: 0.7;
}
/* 资源下载按钮 */
.download-btn {
margin-left: 10px;
padding: 3px 10px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 12px;
transition: all 0.2s ease;
display: inline-block;
text-align: center;
}
.download-btn:hover {
background-color: var(--secondary-color);
}
.markdown-content {
line-height: 1.8;
overflow-wrap: break-word;
background-color: transparent;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
margin-top: 20px;
margin-bottom: 10px;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin: 10px 0;
}
.markdown-content li {
margin-bottom: 8px;
}
.markdown-content li:last-child {
margin-bottom: 0;
}
.markdown-content hr {
border: none;
border-top: 2px dashed var(--border-color);
margin: 20px 0;
}
.markdown-content br {
display: block;
content: "";
margin-top: 10px;
}
.markdown-content code {
font-family: 'IBM Plex Mono', monospace;
background-color: rgba(128, 128, 128, 0.1);
padding: 2px 4px;
border-radius: 3px;
font-size: 90%;
}
.markdown-content pre {
background-color: rgba(128, 128, 128, 0.1);
padding: 15px;
border-radius: 4px;
overflow-x: auto;
margin: 15px 0;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
}
.markdown-content a {
color: var(--primary-color);
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
.data-source {
padding: 10px 15px;
margin-bottom: 20px;
background-color: rgba(var(--light-bg-rgb), 0.7);
border-radius: 4px;
font-size: 14px;
text-align: right;
opacity: 0.7;
}
.data-source a {
color: var(--primary-color);
text-decoration: none;
font-weight: bold;
}
.data-source a:hover {
text-decoration: underline;
}
/* Markdown内容样式增强 */
.markdown-content blockquote {
border-left: 4px solid var(--primary-color);
padding: 10px 15px;
margin: 15px 0;
background-color: rgba(var(--light-bg-rgb), 0.5);
border-radius: 0 4px 4px 0;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin: 10px 0;
}
/* 移动设备响应式优化 */
@media (max-width: 768px) {
.release-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.release-assets {
padding: 12px 8px;
}
.asset-item {
flex-wrap: wrap;
padding: 12px 0;
position: relative;
}
.asset-name {
width: 100%;
margin-bottom: 8px;
word-break: break-all;
}
.asset-size {
margin-left: 25px;
}
.download-btn {
margin-left: 10px;
padding: 5px 12px;
}
}
@media (max-width: 480px) {
.release {
padding-left: 12px;
}
.asset-item {
flex-direction: column;
align-items: flex-start;
}
.asset-icon {
margin-bottom: 5px;
}
.asset-size {
margin-left: 0;
margin-top: 5px;
}
.download-btn {
margin-left: 0;
margin-top: 10px;
width: 100%;
text-align: center;
padding: 8px;
}
.markdown-content pre {
padding: 10px;
margin: 10px 0;
}
}
/* 确保日志页面页脚样式一致 */
.footer {
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.footer-text {
margin: 0 0 15px 0;
}

View File

@@ -0,0 +1,45 @@
/* cyrillic-ext */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -0,0 +1,27 @@
/* vietnamese */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

717
docs/css/styles.css Normal file
View File

@@ -0,0 +1,717 @@
@import url('./space-mono-font.css');
@import url('./ibm-plex-mono-font.css');
/* 浅色主题 */
:root {
--bg-color: #fefefe;
--text-color: #000000;
--primary-color: #F08080;
--primary-color-rgb: 240, 128, 128;
--secondary-color: #ff006e;
--accent-color: #073B4C;
--card-bg: #ffffff;
--card-bg-rgb: 255, 255, 255;
--border-color: #000000;
--light-bg: #f0f0f0;
--light-bg-rgb: 240, 240, 240;
--shadow-color: rgba(240, 128, 128, 0.5);
--success-color: #27c93f;
--warning-color: #FFD166;
--error-color: #ff006e;
--info-color: #118ab2;
--code-bg: #ffffff;
--code-bg-rgb: 255, 255, 255;
--preview-header-bg: #f0f0f0;
--preview-header-bg-rgb: 240, 240, 240;
--grid-color-1: rgba(0, 0, 0, 0.08);
--grid-color-2: rgba(0, 0, 0, 0.05);
--header-title-color: #000000;
}
/* 暗色主题变量 */
.theme-dark {
--bg-color: #121212;
--text-color: #ffffff;
--primary-color: #F08080;
--primary-color-rgb: 240, 128, 128;
--secondary-color: #ff006e;
--accent-color: #118ab2;
--card-bg: #1e1e1e;
--card-bg-rgb: 30, 30, 30;
--border-color: #ffffff;
--light-bg: #2a2a2a;
--light-bg-rgb: 42, 42, 42;
--shadow-color: rgba(240, 128, 128, 0.5);
--success-color: #27c93f;
--warning-color: #FFD166;
--error-color: #ff006e;
--info-color: #118ab2;
--code-bg: #1e1e1e;
--code-bg-rgb: 30, 30, 30;
--preview-header-bg: #252526;
--preview-header-bg-rgb: 37, 37, 38;
--grid-color-1: rgba(255, 255, 255, 0.08);
--grid-color-2: rgba(255, 255, 255, 0.05);
--header-title-color: #000000;
}
/* 主题切换和语言切换的过渡效果 */
.theme-transition,
.theme-transition *,
.lang-transition,
.lang-transition * {
transition: all 0.3s ease !important;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@keyframes gridMove {
0% {
background-position: 0px 0px, 0px 0px, 0px 0px, 0px 0px;
}
100% {
background-position: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
}
}
body {
background-color: var(--bg-color);
background-image:
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
background-position: center;
animation: gridMove 40s linear infinite;
font-family: 'Space Mono', monospace;
color: var(--text-color);
line-height: 1.6;
padding: 20px;
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 卡片容器 */
.card {
background-color: var(--card-bg);
background-image:
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
background-position: center;
border: 4px solid var(--border-color);
box-shadow: 12px 12px 0 var(--shadow-color);
margin-bottom: 40px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
z-index: 10;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 16px 16px 0 var(--shadow-color);
}
/* 卡片头部 */
.card-header {
background-color: rgba(var(--primary-color-rgb), 0.9);
border-bottom: 4px solid var(--border-color);
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.card-title {
font-size: 24px;
font-weight: bold;
margin: 0;
color: var(--header-title-color);
}
.card-controls {
display: flex;
gap: 10px;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: var(--secondary-color);
color: #fff;
text-decoration: none;
font-weight: bold;
border: 3px solid var(--border-color);
box-shadow: 4px 4px 0 var(--shadow-color);
transition: all 0.2s ease;
cursor: pointer;
font-family: 'Space Mono', monospace;
font-size: 14px;
}
.btn:hover {
background: var(--card-bg);
color: var(--primary-color);
border: 3px solid var(--primary-color);
box-shadow: none;
}
.btn-secondary {
background: var(--light-bg);
color: var(--text-color);
}
.btn-secondary:hover {
background: var(--card-bg);
color: var(--primary-color);
border: 3px solid var(--primary-color);
}
/* 卡片内容 */
.card-content {
padding: 30px;
position: relative;
z-index: 1;
background-color: rgba(var(--card-bg-rgb), 0.5);
}
/* Logo区域 */
.logo-container {
text-align: center;
margin-bottom: 40px;
}
.logo-frame {
width: 150px;
height: 150px;
background: var(--card-bg);
border: 4px solid var(--border-color);
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.logo-image {
width: 130px;
height: 130px;
object-fit: contain;
border: 2px solid var(--border-color);
}
.logo-text {
font-size: 32px;
font-weight: bold;
margin: 0;
}
.tagline {
font-size: 16px;
margin: 10px 0 0;
color: var(--accent-color);
}
/* 介绍区域 */
.intro-box {
border: 2px dashed var(--border-color);
padding: 20px;
background-color: rgba(var(--light-bg-rgb), 0.7);
margin-bottom: 30px;
text-align: center;
}
.intro-text {
font-size: 16px;
margin-bottom: 0;
}
/* 按钮组 */
.button-group {
display: flex;
justify-content: center;
gap: 20px;
margin: 30px 0;
}
/* 特性网格 */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 30px;
margin: 40px 0;
}
/* 特性卡片 */
.feature-card {
background-color: rgba(var(--card-bg-rgb), 0.8);
border: 3px solid var(--border-color);
box-shadow: 5px 5px 0 var(--shadow-color);
padding: 20px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feature-card:hover {
transform: translateY(-3px);
box-shadow: 7px 7px 0 var(--shadow-color);
}
.feature-icon {
font-size: 24px;
margin-bottom: 15px;
color: var(--secondary-color);
}
.feature-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.feature-desc {
font-size: 14px;
}
/* 预览区域 */
.preview-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 30px 0;
}
@media (max-width: 768px) {
.preview-container {
grid-template-columns: 1fr;
}
}
/* 预览窗口 */
.preview-window {
border: 3px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
margin: 10px;
flex: 1;
min-width: 300px;
background-color: rgba(var(--card-bg-rgb), 0.7);
display: flex;
flex-direction: column;
box-shadow: 5px 5px 0 var(--shadow-color);
}
/* 预览头部 */
.preview-header {
background-color: rgba(var(--preview-header-bg-rgb), 0.9);
padding: 10px;
display: flex;
align-items: center;
border-bottom: 2px solid var(--border-color);
}
.preview-controls {
display: flex;
gap: 6px;
margin-right: 15px;
}
.preview-btn {
width: 12px;
height: 12px;
border-radius: 50%;
border: 0.5px solid rgba(0, 0, 0, 0.1);
}
.preview-btn:nth-child(1) {
background-color: #ff5f56;
}
.preview-btn:nth-child(2) {
background-color: #ffbd2e;
}
.preview-btn:nth-child(3) {
background-color: #27c93f;
}
.preview-title {
font-size: 13px;
opacity: 0.8;
color: var(--text-color);
font-weight: normal;
}
/* 预览内容 */
.preview-content {
padding: 15px;
flex-grow: 1;
overflow: auto;
background-color: rgba(var(--code-bg-rgb), 0.5);
}
/* 代码块容器 */
.code-block-wrapper {
background-color: rgba(var(--code-bg-rgb), 0.8);
border: 2px solid var(--border-color);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
/* 块头部 */
.block-header {
background-color: rgba(var(--light-bg-rgb), 0.8);
padding: 8px 12px;
border-bottom: 2px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.block-language {
color: rgba(128, 128, 128, 0.8);
font-family: 'IBM Plex Mono', monospace;
display: flex;
align-items: center;
}
.block-language::before {
content: '';
display: inline-block;
width: 12px;
height: 12px;
margin-right: 5px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
.code-block {
font-family: 'IBM Plex Mono', monospace;
font-size: 13px;
line-height: 1.6;
margin: 0;
white-space: pre;
tab-size: 4;
-moz-tab-size: 4;
padding: 10px;
}
.theme-dark .code-block-wrapper {
border-color: rgba(255, 255, 255, 0.15);
}
.theme-dark .block-header {
background-color: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.15);
}
.theme-dark .block-language {
color: rgba(255, 255, 255, 0.6);
}
.theme-dark .block-language::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23aaa'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
}
.theme-dark .code-block {
color: #d4d4d4;
}
/* 代码高亮 */
.theme-dark .keyword { color: #c586c0; }
.theme-dark .function { color: #dcdcaa; }
.theme-dark .variable { color: #9cdcfe; }
.theme-dark .string { color: #ce9178; }
.theme-dark .comment { color: #6a9955; }
.theme-dark .class { color: #4ec9b0; }
.theme-dark .parameter { color: #9cdcfe; }
.theme-dark .built-in { color: #4ec9b0; }
/* 浅色主题代码高亮 */
.keyword { color: #af00db; }
.function { color: #795e26; }
.variable { color: #001080; }
.string { color: #a31515; }
.comment { color: #008000; }
.class { color: #267f99; }
.parameter { color: #001080; }
.built-in { color: #267f99; }
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border: none;
transition: opacity 0.3s ease;
}
.theme-dark .light-theme-img {
display: none !important;
}
.theme-dark .dark-theme-img {
display: block;
}
body:not(.theme-dark) .dark-theme-img {
display: none !important;
}
body:not(.theme-dark) .light-theme-img {
display: block !important;
}
/* 技术栈列表 */
.tech-list {
list-style: none;
padding: 0;
margin: 0;
}
/* 技术栈列表 */
.tech-item {
padding: 15px;
margin-bottom: 15px;
border: 2px solid var(--border-color);
background-color: rgba(var(--light-bg-rgb), 0.7);
display: flex;
align-items: center;
}
.tech-icon {
margin-right: 15px;
color: var(--secondary-color);
font-size: 20px;
width: 30px;
text-align: center;
}
.tech-name {
font-weight: bold;
margin-right: 10px;
}
.tech-desc {
font-size: 14px;
color: var(--accent-color);
}
/* 页脚 */
.footer {
border-top: 2px solid var(--border-color);
padding: 20px 0;
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
background-color: transparent;
position: relative;
z-index: 1;
}
.footer-text {
margin: 0 0 15px 0;
font-size: 14px;
opacity: 0.7;
}
.footer-links {
display: flex;
gap: 15px;
justify-content: center;
}
.footer-link {
color: var(--secondary-color);
text-decoration: none;
font-size: 14px;
transition: color 0.3s;
}
.footer-link:hover {
color: var(--primary-color);
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 768px) {
.button-group {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
text-align: center;
}
.features-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.card-header {
flex-direction: column;
gap: 15px;
}
.card-controls {
width: 100%;
}
.logo-frame {
width: 120px;
height: 120px;
}
.logo-image {
width: 100px;
height: 100px;
}
}
/* 针对移动设备的响应式优化 */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
padding: 10px;
}
.card {
margin-bottom: 30px;
}
.card-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.card-controls {
width: 100%;
justify-content: center;
}
.button-group {
flex-wrap: wrap;
gap: 15px;
}
/* 预览区域优化 */
.preview-content {
max-width: 100%;
overflow-x: auto;
}
.code-block {
white-space: pre-wrap;
word-break: break-word;
font-size: 13px;
line-height: 1.4;
}
.block-header {
padding: 6px 10px;
}
/* 日志界面导航链接优化 */
.nav-links {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.nav-links .btn {
width: 100%;
text-align: center;
}
}
@media (max-width: 480px) {
/* 特性卡片优化 */
.features-grid {
grid-template-columns: 1fr;
gap: 20px;
}
/* 预览窗口优化 */
.preview-container {
flex-direction: column;
}
.preview-window {
margin-bottom: 20px;
width: 100%;
}
/* 技术栈列表小屏幕优化 */
.tech-item {
flex-wrap: wrap;
}
.tech-desc {
width: 100%;
padding-left: 40px; /* 图标宽度+右边距 */
margin-top: 5px;
}
/* 日志界面资源列表项优化 */
.asset-item {
flex-wrap: wrap;
padding: 15px 0;
}
.asset-name {
width: 100%;
word-break: break-all;
margin-bottom: 10px;
}
.asset-size {
order: 2;
margin-top: 10px;
}
.download-btn {
order: 3;
margin-left: 0;
margin-top: 10px;
width: 100%;
text-align: center;
padding: 8px;
}
/* 页脚链接优化 */
.footer {
flex-direction: column;
text-align: center;
}
.footer-links {
margin-top: 15px;
justify-content: center;
}
}

Binary file not shown.

BIN
docs/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

256
docs/index.html Normal file
View File

@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>voidraft - An elegant text snippet recording tool designed for developers.</title>
<meta name="description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta name="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, voidraft">
<meta name="author" content="voidraft Team">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://landaiqing.github.io/voidraft/">
<!-- Internationalization / hreflang -->
<link rel="alternate" hreflang="en" href="https://landaiqing.github.io/voidraft/">
<link rel="alternate" hreflang="zh" href="https://landaiqing.github.io/voidraft/?lang=zh">
<link rel="alternate" hreflang="x-default" href="https://landaiqing.github.io/voidraft/">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://landaiqing.github.io/voidraft/">
<meta property="og:title" content="voidraft - An elegant text snippet recording tool designed for developers">
<meta property="og:description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="og:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<meta property="og:site_name" content="voidraft">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://landaiqing.github.io/voidraft/">
<meta property="twitter:title" content="voidraft - An elegant text snippet recording tool designed for developers">
<meta property="twitter:description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="twitter:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<link rel="stylesheet" href="./css/styles.css">
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "voidraft",
"description": "An elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.",
"url": "https://landaiqing.github.io/voidraft/",
"downloadUrl": "https://github.com/landaiqing/voidraft/releases",
"author": {
"@type": "Organization",
"name": "voidraft"
},
"operatingSystem": ["Windows", "macOS", "Linux"],
"applicationCategory": "DeveloperApplication",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"screenshot": "https://landaiqing.github.io/voidraft/img/screenshot-dark.png",
"softwareVersion": "Latest",
"programmingLanguage": ["Go", "TypeScript", "Vue.js"],
"codeRepository": "https://github.com/landaiqing/voidraft"
}
</script>
</head>
<body class="theme-dark">
<div class="container">
<!-- 主卡片 -->
<div class="card">
<div class="card-header">
<h1 class="card-title">voidraft</h1>
<div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
</button>
<button id="lang-toggle" class="btn btn-secondary">
<i class="fas fa-language"></i> 中/EN
</button>
</div>
</div>
<div class="card-content">
<!-- Logo和介绍 -->
<div class="logo-container">
<div class="logo-frame">
<img src="img/logo.png" alt="voidraft Logo" class="logo-image">
</div>
<h2 class="logo-text" data-en="voidraft" data-zh="voidraft">voidraft</h2>
<p class="tagline" data-en="An elegant text snippet recording tool" data-zh="优雅的文本片段记录工具">An elegant text snippet recording tool</p>
</div>
<div class="intro-box">
<p class="intro-text" data-en="Designed for developers to record, organize, and manage various text snippets anytime, anywhere." data-zh="专为开发者打造,随时随地记录、整理和管理各种文本片段。">Designed for developers to record, organize, and manage various text snippets anytime, anywhere.</p>
</div>
<div class="button-group">
<a href="https://github.com/landaiqing/voidraft/releases" class="btn" data-en="Download" data-zh="下载">
<i class="fas fa-download"></i> Download
</a>
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary" data-en="Source Code" data-zh="源代码">
<i class="fab fa-github"></i> Source Code
</a>
<a href="changelog.html" class="btn btn-secondary" data-en="Changelog" data-zh="更新日志">
<i class="fas fa-history"></i> Changelog
</a>
</div>
<!-- 特性部分 -->
<h2 data-en="Core Features" data-zh="核心特性">Core Features</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-code"></i>
</div>
<h3 class="feature-title" data-en="Developer-Friendly" data-zh="开发者友好">Developer-Friendly</h3>
<p class="feature-desc" data-en="Multi-language code blocks with syntax highlighting for 30+ programming languages" data-zh="多语言代码块支持为30+种编程语言提供语法高亮">Multi-language code blocks with syntax highlighting for 30+ programming languages</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-magic"></i>
</div>
<h3 class="feature-title" data-en="Code Formatting" data-zh="代码格式化">Code Formatting</h3>
<p class="feature-desc" data-en="Built-in Prettier support for one-click code beautification" data-zh="内置Prettier支持一键美化代码">Built-in Prettier support for one-click code beautification</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-palette"></i>
</div>
<h3 class="feature-title" data-en="Custom Themes" data-zh="自定义主题">Custom Themes</h3>
<p class="feature-desc" data-en="Dark/Light themes with full customization options" data-zh="深色/浅色主题,支持完全自定义">Dark/Light themes with full customization options</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-clone"></i>
</div>
<h3 class="feature-title" data-en="Multi-Window" data-zh="多窗口支持">Multi-Window</h3>
<p class="feature-desc" data-en="Edit multiple documents simultaneously" data-zh="同时编辑多个文档">Edit multiple documents simultaneously</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-layer-group"></i>
</div>
<h3 class="feature-title" data-en="Block Editing" data-zh="块状编辑">Block Editing</h3>
<p class="feature-desc" data-en="Split content into independent code blocks with different language settings" data-zh="将内容分割为独立的代码块,每个块可设置不同语言">Split content into independent code blocks with different language settings</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-puzzle-piece"></i>
</div>
<h3 class="feature-title" data-en="Extensions" data-zh="丰富扩展">Extensions</h3>
<p class="feature-desc" data-en="Rainbow brackets, VSCode-style search, color picker, translation tool, and more" data-zh="彩虹括号、VSCode风格搜索、颜色选择器、翻译工具等多种扩展">Rainbow brackets, VSCode-style search, color picker, translation tool, and more</p>
</div>
</div>
<!-- 预览部分 -->
<h2 data-en="Preview" data-zh="预览">Preview</h2>
<div class="preview-container">
<div class="preview-window">
<div class="preview-header">
<div class="preview-controls">
<span class="preview-btn"></span>
<span class="preview-btn"></span>
<span class="preview-btn"></span>
</div>
<div class="preview-title">voidraft</div>
</div>
<div class="preview-content">
<div class="code-block-wrapper">
<div class="block-header">
<div class="block-language">javascript</div>
</div>
<pre class="code-block">
<span class="keyword">function</span> <span class="function">createDocument</span>() {
<span class="keyword">const</span> <span class="variable">doc</span> = <span class="keyword">new</span> <span class="class">Document</span>();
<span class="variable">doc</span>.<span class="function">addCodeBlock</span>(<span class="string">'javascript'</span>, <span class="string">`
<span class="keyword">function</span> <span class="function">greeting</span>(<span class="parameter">name</span>) {
<span class="keyword">return</span> <span class="string">`Hello, </span>${<span class="parameter">name</span>}<span class="string">!`</span>;
}
<span class="built-in">console</span>.<span class="function">log</span>(<span class="function">greeting</span>(<span class="string">'World'</span>));
`</span>);
<span class="keyword">return</span> <span class="variable">doc</span>;
}</pre>
</div>
<div class="code-block-wrapper" style="margin-top: 10px;">
<div class="block-header">
<div class="block-language">text</div>
</div>
<pre class="code-block">
<span class="comment">// voidraft - An elegant text snippet recording tool</span>
<span class="comment">// Multi-language support | Code formatting | Custom themes</span>
<span class="comment">// A modern text editor designed for developers</span></pre>
</div>
</div>
</div>
<div class="preview-window">
<img src="img/screenshot-dark.png" alt="voidraft 界面预览" class="preview-image dark-theme-img">
<img src="img/screenshot-light.png" alt="voidraft 界面预览" class="preview-image light-theme-img" style="display: none;">
</div>
</div>
<!-- 技术栈部分 -->
<h2 data-en="Technical Stack" data-zh="技术栈">Technical Stack</h2>
<ul class="tech-list">
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-desktop"></i></div>
<span class="tech-name">Wails3</span>
<span class="tech-desc" data-en="Cross-platform desktop application framework" data-zh="跨平台桌面应用框架">Cross-platform desktop application framework</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-cogs"></i></div>
<span class="tech-name">Go 1.21+</span>
<span class="tech-desc" data-en="Fast and efficient backend language" data-zh="快速高效的后端语言">Fast and efficient backend language</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fab fa-vuejs"></i></div>
<span class="tech-name">Vue 3 + TypeScript</span>
<span class="tech-desc" data-en="Modern frontend framework" data-zh="现代化前端框架">Modern frontend framework</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-edit"></i></div>
<span class="tech-name">CodeMirror 6</span>
<span class="tech-desc" data-en="Modern code editor with extension support" data-zh="支持扩展的现代化代码编辑器">Modern code editor with extension support</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-database"></i></div>
<span class="tech-name">SQLite</span>
<span class="tech-desc" data-en="Lightweight database for document storage" data-zh="轻量级文档存储数据库">Lightweight database for document storage</span>
</li>
</ul>
</div>
<!-- 页脚 -->
<footer class="footer">
<p class="footer-text" data-en="© 2025 voidraft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 voidraft - 专为开发者打造的优雅文本片段记录工具">© 2025 voidraft - An elegant text snippet recording tool designed for developers</p>
<div class="footer-links">
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
</div>
</footer>
</div>
</div>
<script src="js/script.js"></script>
</body>
</html>

705
docs/js/changelog.js Normal file
View File

@@ -0,0 +1,705 @@
/**
* voidraft - Changelog Script
* 从GitHub API获取发布信息支持Gitea备用源
*/
/**
* 仓库配置类
*/
class RepositoryConfig {
constructor() {
this.repos = {
github: {
owner: 'landaiqing',
name: 'voidraft',
apiUrl: 'https://api.github.com/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://github.com/landaiqing/voidraft/releases'
},
gitea: {
owner: 'landaiqing',
name: 'voidraft',
domain: 'git.landaiqing.cn',
apiUrl: 'https://git.landaiqing.cn/api/v1/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://git.landaiqing.cn/landaiqing/voidraft/releases'
}
};
}
/**
* 获取仓库配置
* @param {string} source - 'github' 或 'gitea'
*/
getRepo(source) {
return this.repos[source];
}
/**
* 获取所有仓库配置
*/
getAllRepos() {
return this.repos;
}
}
/**
* 国际化消息管理类
*/
class I18nMessages {
constructor() {
this.messages = {
loading: {
en: 'Loading releases...',
zh: '正在加载版本信息...'
},
noReleases: {
en: 'No release information found',
zh: '没有找到版本发布信息'
},
fetchError: {
en: 'Failed to load release information. Please try again later.',
zh: '无法获取版本信息,请稍后再试'
},
githubApiError: {
en: 'GitHub API returned an error status: ',
zh: 'GitHub API返回错误状态: '
},
giteaApiError: {
en: 'Gitea API returned an error status: ',
zh: 'Gitea API返回错误状态: '
},
dataSource: {
en: 'Data source: ',
zh: '数据来源: '
},
downloads: {
en: 'Downloads',
zh: '下载资源'
},
download: {
en: 'Download',
zh: '下载'
},
preRelease: {
en: 'Pre-release',
zh: '预发布'
}
};
}
/**
* 获取消息
* @param {string} key - 消息键
* @param {string} lang - 语言代码
*/
getMessage(key, lang = 'en') {
return this.messages[key] && this.messages[key][lang] || this.messages[key]['en'] || '';
}
/**
* 获取当前语言
*/
getCurrentLang() {
return window.currentLang || 'en';
}
}
/**
* API客户端类
*/
class APIClient {
constructor(repositoryConfig, i18nMessages) {
this.repositoryConfig = repositoryConfig;
this.i18nMessages = i18nMessages;
}
/**
* 从指定源获取发布信息
* @param {string} source - 'github' 或 'gitea'
*/
async fetchReleases(source) {
const repo = this.repositoryConfig.getRepo(source);
const errorMessageKey = source === 'github' ? 'githubApiError' : 'giteaApiError';
const options = {
headers: { 'Accept': 'application/json' }
};
if (source === 'github') {
return this.fetchFromGitHub(repo, options, errorMessageKey);
} else {
return this.fetchFromGitea(repo, options, errorMessageKey);
}
}
/**
* 从GitHub获取数据
* @param {Object} repo - 仓库配置
* @param {Object} options - 请求选项
* @param {string} errorMessageKey - 错误消息键
*/
async fetchFromGitHub(repo, options, errorMessageKey) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
options.signal = controller.signal;
options.headers['Accept'] = 'application/vnd.github.v3+json';
try {
const response = await fetch(repo.apiUrl, options);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
}
const releases = await response.json();
if (!releases || releases.length === 0) {
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
}
return releases;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
/**
* 从Gitea获取数据
* @param {Object} repo - 仓库配置
* @param {Object} options - 请求选项
* @param {string} errorMessageKey - 错误消息键
*/
async fetchFromGitea(repo, options, errorMessageKey) {
const response = await fetch(repo.apiUrl, options);
if (!response.ok) {
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
}
const releases = await response.json();
if (!releases || releases.length === 0) {
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
}
return releases;
}
}
/**
* UI管理类
*/
class UIManager {
constructor(i18nMessages) {
this.i18nMessages = i18nMessages;
this.elements = {
loading: document.getElementById('loading'),
changelog: document.getElementById('changelog'),
error: document.getElementById('error-message')
};
}
/**
* 显示加载状态
*/
showLoading() {
this.elements.loading.style.display = 'block';
this.elements.error.style.display = 'none';
this.elements.changelog.innerHTML = '';
}
/**
* 隐藏加载状态
*/
hideLoading() {
this.elements.loading.style.display = 'none';
}
/**
* 显示错误消息
* @param {string} message - 错误消息
*/
showError(message) {
const errorMessageElement = this.elements.error.querySelector('p');
if (errorMessageElement) {
errorMessageElement.textContent = message;
} else {
this.elements.error.textContent = message;
}
this.elements.error.style.display = 'block';
this.hideLoading();
}
/**
* 显示发布信息
* @param {Array} releases - 发布信息数组
* @param {string} source - 数据源
*/
displayReleases(releases, source) {
this.hideLoading();
// 清除现有内容
this.elements.changelog.innerHTML = '';
// 创建数据源元素
const sourceElement = this.createSourceElement(source);
this.elements.changelog.appendChild(sourceElement);
// 创建发布信息元素
releases.forEach(release => {
const releaseElement = this.createReleaseElement(release, source);
this.elements.changelog.appendChild(releaseElement);
});
this.elements.changelog.style.display = 'block';
}
/**
* 创建数据源元素
* @param {string} source - 数据源
*/
createSourceElement(source) {
const sourceElement = document.createElement('div');
sourceElement.className = 'data-source';
// 创建带有国际化支持的源标签
const sourceLabel = document.createElement('span');
sourceLabel.setAttribute('data-en', this.i18nMessages.getMessage('dataSource', 'en'));
sourceLabel.setAttribute('data-zh', this.i18nMessages.getMessage('dataSource', 'zh'));
sourceLabel.textContent = this.i18nMessages.getMessage('dataSource', this.i18nMessages.getCurrentLang());
// 创建链接
const sourceLink = document.createElement('a');
const repositoryConfig = new RepositoryConfig();
sourceLink.href = repositoryConfig.getRepo(source).releasesUrl;
sourceLink.textContent = source === 'github' ? 'GitHub' : 'Gitea';
sourceLink.target = '_blank';
// 组装元素
sourceElement.appendChild(sourceLabel);
sourceElement.appendChild(sourceLink);
return sourceElement;
}
/**
* 创建发布信息元素
* @param {Object} release - 发布信息对象
* @param {string} source - 数据源
*/
createReleaseElement(release, source) {
const releaseElement = document.createElement('div');
releaseElement.className = 'release';
// 格式化发布日期
const releaseDate = new Date(release.published_at || release.created_at);
const formattedDate = DateFormatter.formatDate(releaseDate);
// 创建头部
const headerElement = this.createReleaseHeader(release, formattedDate);
releaseElement.appendChild(headerElement);
// 添加发布说明
if (release.body) {
const descriptionElement = document.createElement('div');
descriptionElement.className = 'release-description markdown-content';
descriptionElement.innerHTML = MarkdownParser.parseMarkdown(release.body);
releaseElement.appendChild(descriptionElement);
}
// 添加下载资源
const assets = AssetManager.getAssetsFromRelease(release, source);
if (assets && assets.length > 0) {
const assetsElement = this.createAssetsElement(assets);
releaseElement.appendChild(assetsElement);
}
return releaseElement;
}
/**
* 创建发布信息头部
*/
createReleaseHeader(release, formattedDate) {
const headerElement = document.createElement('div');
headerElement.className = 'release-header';
// 版本元素
const versionElement = document.createElement('div');
versionElement.className = 'release-version';
// 版本文本
const versionText = document.createElement('span');
versionText.textContent = release.name || release.tag_name;
versionElement.appendChild(versionText);
// 预发布标记
if (release.prerelease) {
const preReleaseTag = document.createElement('span');
preReleaseTag.className = 'release-badge pre-release';
preReleaseTag.setAttribute('data-en', this.i18nMessages.getMessage('preRelease', 'en'));
preReleaseTag.setAttribute('data-zh', this.i18nMessages.getMessage('preRelease', 'zh'));
preReleaseTag.textContent = this.i18nMessages.getMessage('preRelease', this.i18nMessages.getCurrentLang());
versionElement.appendChild(preReleaseTag);
}
// 日期元素
const dateElement = document.createElement('div');
dateElement.className = 'release-date';
dateElement.textContent = formattedDate;
headerElement.appendChild(versionElement);
headerElement.appendChild(dateElement);
return headerElement;
}
/**
* 创建资源文件元素
* @param {Array} assets - 资源文件数组
*/
createAssetsElement(assets) {
const assetsElement = document.createElement('div');
assetsElement.className = 'release-assets';
// 资源标题
const assetsTitle = document.createElement('div');
assetsTitle.className = 'release-assets-title';
assetsTitle.setAttribute('data-en', this.i18nMessages.getMessage('downloads', 'en'));
assetsTitle.setAttribute('data-zh', this.i18nMessages.getMessage('downloads', 'zh'));
assetsTitle.textContent = this.i18nMessages.getMessage('downloads', this.i18nMessages.getCurrentLang());
// 资源列表
const assetList = document.createElement('ul');
assetList.className = 'asset-list';
// 添加每个资源
assets.forEach(asset => {
const assetItem = this.createAssetItem(asset);
assetList.appendChild(assetItem);
});
assetsElement.appendChild(assetsTitle);
assetsElement.appendChild(assetList);
return assetsElement;
}
/**
* 创建资源文件项
* @param {Object} asset - 资源文件对象
*/
createAssetItem(asset) {
const assetItem = document.createElement('li');
assetItem.className = 'asset-item';
// 文件图标
const iconElement = document.createElement('i');
iconElement.className = `asset-icon fas fa-${FileIconHelper.getFileIcon(asset.name)}`;
// 文件名
const nameElement = document.createElement('span');
nameElement.className = 'asset-name';
nameElement.textContent = asset.name;
// 文件大小
const sizeElement = document.createElement('span');
sizeElement.className = 'asset-size';
sizeElement.textContent = FileSizeFormatter.formatFileSize(asset.size);
// 下载链接
const downloadLink = document.createElement('a');
downloadLink.className = 'download-btn';
downloadLink.href = asset.browser_download_url;
downloadLink.target = '_blank';
downloadLink.setAttribute('data-en', this.i18nMessages.getMessage('download', 'en'));
downloadLink.setAttribute('data-zh', this.i18nMessages.getMessage('download', 'zh'));
downloadLink.textContent = this.i18nMessages.getMessage('download', this.i18nMessages.getCurrentLang());
// 组装资源项
assetItem.appendChild(iconElement);
assetItem.appendChild(nameElement);
assetItem.appendChild(sizeElement);
assetItem.appendChild(downloadLink);
return assetItem;
}
}
/**
* 资源管理器类
*/
class AssetManager {
/**
* 从发布信息中获取资源文件
* @param {Object} release - 发布信息对象
* @param {string} source - 数据源
*/
static getAssetsFromRelease(release, source) {
let assets = [];
if (source === 'github') {
assets = release.assets || [];
} else { // Gitea
assets = release.assets || [];
// 检查Gitea特定的资源结构
if (!assets.length && release.attachments) {
assets = release.attachments.map(attachment => ({
name: attachment.name,
size: attachment.size,
browser_download_url: attachment.browser_download_url
}));
}
}
return assets;
}
}
/**
* 文件图标助手类
*/
class FileIconHelper {
/**
* 根据文件扩展名获取图标
* @param {string} filename - 文件名
*/
static getFileIcon(filename) {
const extension = filename.split('.').pop().toLowerCase();
const iconMap = {
'exe': 'download',
'msi': 'download',
'dmg': 'download',
'pkg': 'download',
'deb': 'download',
'rpm': 'download',
'tar': 'file-archive',
'gz': 'file-archive',
'zip': 'file-archive',
'7z': 'file-archive',
'rar': 'file-archive',
'pdf': 'file-pdf',
'txt': 'file-alt',
'md': 'file-alt',
'json': 'file-code',
'xml': 'file-code',
'yml': 'file-code',
'yaml': 'file-code'
};
return iconMap[extension] || 'file';
}
}
/**
* 文件大小格式化器类
*/
class FileSizeFormatter {
/**
* 格式化文件大小
* @param {number} bytes - 字节数
*/
static formatFileSize(bytes) {
if (!bytes) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
}
/**
* 日期格式化器类
*/
class DateFormatter {
/**
* 格式化日期
* @param {Date} date - 日期对象
*/
static formatDate(date) {
const options = {
year: 'numeric',
month: 'long',
day: 'numeric'
};
const lang = window.currentLang || 'en';
const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
return date.toLocaleDateString(locale, options);
}
}
/**
* Markdown解析器类
*/
class MarkdownParser {
/**
* 简单的Markdown解析
* @param {string} markdown - Markdown文本
*/
static parseMarkdown(markdown) {
if (!markdown) return '';
// 预处理:保留原始换行符,用特殊标记替换
const preservedLineBreaks = '___LINE_BREAK___';
markdown = markdown.replace(/\n/g, preservedLineBreaks);
// 引用块 - > text
markdown = markdown.replace(/&gt;\s*(.*?)(?=&gt;|$)/g, '<blockquote>$1</blockquote>');
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
// 链接 - [text](url)
markdown = markdown.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// 标题 - # Heading
markdown = markdown.replace(/^### (.*?)(?=___LINE_BREAK___|$)/gm, '<h3>$1</h3>');
markdown = markdown.replace(/^## (.*?)(?=___LINE_BREAK___|$)/gm, '<h2>$1</h2>');
markdown = markdown.replace(/^# (.*?)(?=___LINE_BREAK___|$)/gm, '<h1>$1</h1>');
// 粗体 - **text**
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// 斜体 - *text*
markdown = markdown.replace(/\*(.*?)\*/g, '<em>$1</em>');
// 代码块 - ```code```
markdown = markdown.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// 行内代码 - `code`
markdown = markdown.replace(/`([^`]+)`/g, '<code>$1</code>');
// 处理列表项
// 先将每个列表项转换为HTML
markdown = markdown.replace(/- (.*?)(?=___LINE_BREAK___- |___LINE_BREAK___$|$)/g, '<li>$1</li>');
markdown = markdown.replace(/\* (.*?)(?=___LINE_BREAK___\* |___LINE_BREAK___$|$)/g, '<li>$1</li>');
markdown = markdown.replace(/\d+\. (.*?)(?=___LINE_BREAK___\d+\. |___LINE_BREAK___$|$)/g, '<li>$1</li>');
// 然后将连续的列表项包装在ul或ol中
const listItemRegex = /<li>.*?<\/li>/g;
const listItems = markdown.match(listItemRegex) || [];
if (listItems.length > 0) {
// 将连续的列表项组合在一起
let lastIndex = 0;
let result = '';
let inList = false;
listItems.forEach(item => {
const itemIndex = markdown.indexOf(item, lastIndex);
// 添加列表项之前的内容
if (itemIndex > lastIndex) {
result += markdown.substring(lastIndex, itemIndex);
}
// 如果不在列表中,开始一个新列表
if (!inList) {
result += '<ul>';
inList = true;
}
// 添加列表项
result += item;
// 更新lastIndex
lastIndex = itemIndex + item.length;
// 检查下一个内容是否是列表项
const nextItemIndex = markdown.indexOf('<li>', lastIndex);
if (nextItemIndex === -1 || nextItemIndex > lastIndex + 20) { // 如果下一个列表项不紧邻
result += '</ul>';
inList = false;
}
});
// 添加剩余内容
if (lastIndex < markdown.length) {
result += markdown.substring(lastIndex);
}
markdown = result;
}
// 处理水平分隔线
markdown = markdown.replace(/---/g, '<hr>');
// 恢复换行符
markdown = markdown.replace(/___LINE_BREAK___/g, '<br>');
// 处理段落
markdown = markdown.replace(/<br><br>/g, '</p><p>');
// 包装在段落标签中
if (!markdown.startsWith('<p>')) {
markdown = `<p>${markdown}</p>`;
}
return markdown;
}
}
/**
* 更新日志主应用类
*/
class ChangelogApp {
constructor() {
this.repositoryConfig = new RepositoryConfig();
this.i18nMessages = new I18nMessages();
this.apiClient = new APIClient(this.repositoryConfig, this.i18nMessages);
this.uiManager = new UIManager(this.i18nMessages);
this.init();
}
/**
* 初始化应用
*/
init() {
this.uiManager.showLoading();
// 首先尝试GitHub API
this.apiClient.fetchReleases('github')
.then(releases => {
this.uiManager.displayReleases(releases, 'github');
})
.catch(() => {
// GitHub失败时尝试Gitea
return this.apiClient.fetchReleases('gitea')
.then(releases => {
this.uiManager.displayReleases(releases, 'gitea');
});
})
.catch(error => {
console.error('获取发布信息失败:', error);
this.uiManager.showError(this.i18nMessages.getMessage('fetchError', this.i18nMessages.getCurrentLang()));
});
// 监听语言变化事件
document.addEventListener('languageChanged', () => this.updateUI());
}
/**
* 更新UI元素当语言变化时
*/
updateUI() {
const elementsToUpdate = document.querySelectorAll('[data-en][data-zh]');
const currentLang = this.i18nMessages.getCurrentLang();
elementsToUpdate.forEach(element => {
const text = element.getAttribute(`data-${currentLang}`);
if (text) {
element.textContent = text;
}
});
}
}
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
new ChangelogApp();
});

443
docs/js/script.js Normal file
View File

@@ -0,0 +1,443 @@
/**
* voidraft - Website Script
*/
/**
* 主题管理类
*/
class ThemeManager {
constructor() {
this.themeToggle = document.getElementById('theme-toggle');
this.currentTheme = this.getInitialTheme();
this.init();
}
/**
* 获取初始主题
*/
getInitialTheme() {
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const savedTheme = localStorage.getItem('theme');
return savedTheme || (prefersDarkScheme.matches ? 'dark' : 'light');
}
/**
* 初始化主题管理器
*/
init() {
if (!this.themeToggle) return;
this.setTheme(this.currentTheme);
this.bindEvents();
}
/**
* 绑定事件
*/
bindEvents() {
this.themeToggle.addEventListener('click', () => {
this.toggleTheme();
});
}
/**
* 切换主题
*/
toggleTheme() {
document.body.classList.add('theme-transition');
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
this.saveTheme(newTheme);
setTimeout(() => document.body.classList.remove('theme-transition'), 300);
}
/**
* 设置主题
* @param {string} theme - 'dark' 或 'light'
*/
setTheme(theme) {
this.currentTheme = theme;
const isDark = theme === 'dark';
document.body.classList.toggle('theme-dark', isDark);
document.body.classList.toggle('theme-light', !isDark);
this.updateToggleIcon(isDark);
}
/**
* 更新切换按钮图标
* @param {boolean} isDark - 是否为暗色主题
*/
updateToggleIcon(isDark) {
if (this.themeToggle) {
const icon = this.themeToggle.querySelector('i');
if (icon) {
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
}
}
/**
* 保存主题到本地存储
* @param {string} theme - 主题名称
*/
saveTheme(theme) {
localStorage.setItem('theme', theme);
}
}
/**
* 语言管理类
*/
class LanguageManager {
constructor() {
this.langToggle = document.getElementById('lang-toggle');
this.currentLang = this.getInitialLanguage();
this.init();
}
/**
* 获取初始语言
*/
getInitialLanguage() {
const urlParams = new URLSearchParams(window.location.search);
const urlLang = urlParams.get('lang');
const savedLang = localStorage.getItem('lang');
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
return urlLang || savedLang || browserLang;
}
/**
* 初始化语言管理器
*/
init() {
if (!this.langToggle) return;
window.currentLang = this.currentLang;
this.setLanguage(this.currentLang);
this.bindEvents();
}
/**
* 绑定事件
*/
bindEvents() {
this.langToggle.addEventListener('click', () => {
this.toggleLanguage();
});
}
/**
* 切换语言
*/
toggleLanguage() {
document.body.classList.add('lang-transition');
const newLang = this.currentLang === 'zh' ? 'en' : 'zh';
this.setLanguage(newLang);
this.saveLanguage(newLang);
this.updateURL(newLang);
this.notifyLanguageChange(newLang);
setTimeout(() => document.body.classList.remove('lang-transition'), 300);
}
/**
* 设置页面语言
* @param {string} lang - 'zh' 或 'en'
*/
setLanguage(lang) {
this.currentLang = lang;
window.currentLang = lang;
this.updatePageElements(lang);
this.updateHTMLLang(lang);
this.updateToggleButton(lang);
}
/**
* 更新页面元素文本
* @param {string} lang - 语言代码
*/
updatePageElements(lang) {
document.querySelectorAll('[data-zh][data-en]').forEach(el => {
el.textContent = el.getAttribute(`data-${lang}`);
});
}
/**
* 更新HTML语言属性
* @param {string} lang - 语言代码
*/
updateHTMLLang(lang) {
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
}
/**
* 更新切换按钮文本
* @param {string} lang - 语言代码
*/
updateToggleButton(lang) {
if (this.langToggle) {
const text = lang === 'zh' ? 'EN/中' : '中/EN';
this.langToggle.innerHTML = `<i class="fas fa-language"></i> ${text}`;
}
}
/**
* 保存语言到本地存储
* @param {string} lang - 语言代码
*/
saveLanguage(lang) {
localStorage.setItem('lang', lang);
}
/**
* 更新URL参数
* @param {string} lang - 语言代码
*/
updateURL(lang) {
const newUrl = new URL(window.location);
if (lang === 'zh') {
newUrl.searchParams.set('lang', 'zh');
} else {
newUrl.searchParams.delete('lang');
}
window.history.replaceState({}, '', newUrl);
}
/**
* 通知语言变更
* @param {string} lang - 语言代码
*/
notifyLanguageChange(lang) {
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
}
/**
* 获取当前语言
*/
getCurrentLanguage() {
return this.currentLang;
}
}
/**
* SEO管理类
*/
class SEOManager {
constructor(languageManager) {
this.languageManager = languageManager;
this.metaTexts = {
en: {
description: 'voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.',
title: 'voidraft - An elegant text snippet recording tool designed for developers.',
ogTitle: 'voidraft - An elegant text snippet recording tool designed for developers'
},
zh: {
description: 'voidraft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
title: 'voidraft - 专为开发者打造的优雅文本片段记录工具',
ogTitle: 'voidraft - 专为开发者打造的优雅文本片段记录工具'
}
};
this.init();
}
/**
* 初始化SEO管理器
*/
init() {
this.bindEvents();
this.updateMetaTags(this.languageManager.getCurrentLanguage());
}
/**
* 绑定事件
*/
bindEvents() {
window.addEventListener('languageChanged', (event) => {
this.updateMetaTags(event.detail.lang);
});
}
/**
* 更新SEO元标签
* @param {string} lang - 当前语言
*/
updateMetaTags(lang) {
const texts = this.metaTexts[lang];
this.updateMetaDescription(texts.description);
this.updateOpenGraphTags(texts.ogTitle, texts.description);
this.updateTwitterCardTags(texts.ogTitle, texts.description);
this.updatePageTitle(texts.title);
}
/**
* 更新meta描述
* @param {string} description - 描述文本
*/
updateMetaDescription(description) {
const metaDesc = document.querySelector('meta[name="description"]');
if (metaDesc) {
metaDesc.content = description;
}
}
/**
* 更新Open Graph标签
* @param {string} title - 标题
* @param {string} description - 描述
*/
updateOpenGraphTags(title, description) {
const ogTitle = document.querySelector('meta[property="og:title"]');
const ogDesc = document.querySelector('meta[property="og:description"]');
if (ogTitle) ogTitle.content = title;
if (ogDesc) ogDesc.content = description;
}
/**
* 更新Twitter Card标签
* @param {string} title - 标题
* @param {string} description - 描述
*/
updateTwitterCardTags(title, description) {
const twitterTitle = document.querySelector('meta[property="twitter:title"]');
const twitterDesc = document.querySelector('meta[property="twitter:description"]');
if (twitterTitle) twitterTitle.content = title;
if (twitterDesc) twitterDesc.content = description;
}
/**
* 更新页面标题
* @param {string} title - 标题
*/
updatePageTitle(title) {
document.title = title;
}
}
/**
* UI效果管理类
*/
class UIEffects {
constructor() {
this.init();
}
/**
* 初始化UI效果
*/
init() {
this.initCardEffects();
}
/**
* 初始化卡片悬停效果
*/
initCardEffects() {
const cards = document.querySelectorAll('.feature-card');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
this.animateCardHover(card, true);
});
card.addEventListener('mouseleave', () => {
this.animateCardHover(card, false);
});
});
}
/**
* 卡片悬停动画
* @param {Element} card - 卡片元素
* @param {boolean} isHover - 是否悬停
*/
animateCardHover(card, isHover) {
if (isHover) {
card.style.transform = 'translateY(-8px)';
card.style.boxShadow = '7px 7px 0 var(--shadow-color)';
} else {
card.style.transform = 'translateY(0)';
card.style.boxShadow = '5px 5px 0 var(--shadow-color)';
}
}
}
/**
* voidraft主应用类
*/
class voidraftApp {
constructor() {
this.themeManager = null;
this.languageManager = null;
this.seoManager = null;
this.uiEffects = null;
this.init();
}
/**
* 初始化应用
*/
init() {
this.initializeManagers();
this.showConsoleBranding();
}
/**
* 初始化各个管理器
*/
initializeManagers() {
this.themeManager = new ThemeManager();
this.languageManager = new LanguageManager();
this.seoManager = new SEOManager(this.languageManager);
this.uiEffects = new UIEffects();
}
/**
* 显示控制台品牌信息
*/
showConsoleBranding() {
console.log('%c voidraft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;');
console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;');
}
/**
* 获取主题管理器
*/
getThemeManager() {
return this.themeManager;
}
/**
* 获取语言管理器
*/
getLanguageManager() {
return this.languageManager;
}
/**
* 获取SEO管理器
*/
getSEOManager() {
return this.seoManager;
}
/**
* 获取UI效果管理器
*/
getUIEffects() {
return this.uiEffects;
}
}
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
window.voidRaftApp = new voidraftApp();
});

View File

@@ -11,15 +11,55 @@ import * as slog$0 from "../../../../../../log/slog/models.js";
export class App {
/**
* The main application menu
* Manager pattern for organized API
*/
"ApplicationMenu": Menu | null;
"Window": WindowManager | null;
"ContextMenu": ContextMenuManager | null;
"KeyBinding": KeyBindingManager | null;
"Browser": BrowserManager | null;
"Env": EnvironmentManager | null;
"Dialog": DialogManager | null;
"Event": EventManager | null;
"Menu": MenuManager | null;
"Screen": ScreenManager | null;
"Clipboard": ClipboardManager | null;
"SystemTray": SystemTrayManager | null;
"Logger": slog$0.Logger | null;
/** Creates a new App instance. */
constructor($$source: Partial<App> = {}) {
if (!("ApplicationMenu" in $$source)) {
this["ApplicationMenu"] = null;
if (!("Window" in $$source)) {
this["Window"] = null;
}
if (!("ContextMenu" in $$source)) {
this["ContextMenu"] = null;
}
if (!("KeyBinding" in $$source)) {
this["KeyBinding"] = null;
}
if (!("Browser" in $$source)) {
this["Browser"] = null;
}
if (!("Env" in $$source)) {
this["Env"] = null;
}
if (!("Dialog" in $$source)) {
this["Dialog"] = null;
}
if (!("Event" in $$source)) {
this["Event"] = null;
}
if (!("Menu" in $$source)) {
this["Menu"] = null;
}
if (!("Screen" in $$source)) {
this["Screen"] = null;
}
if (!("Clipboard" in $$source)) {
this["Clipboard"] = null;
}
if (!("SystemTray" in $$source)) {
this["SystemTray"] = null;
}
if (!("Logger" in $$source)) {
this["Logger"] = null;
@@ -34,31 +74,308 @@ export class App {
static createFrom($$source: any = {}): App {
const $$createField0_0 = $$createType1;
const $$createField1_0 = $$createType3;
const $$createField2_0 = $$createType5;
const $$createField3_0 = $$createType7;
const $$createField4_0 = $$createType9;
const $$createField5_0 = $$createType11;
const $$createField6_0 = $$createType13;
const $$createField7_0 = $$createType15;
const $$createField8_0 = $$createType17;
const $$createField9_0 = $$createType19;
const $$createField10_0 = $$createType21;
const $$createField11_0 = $$createType23;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("ApplicationMenu" in $$parsedSource) {
$$parsedSource["ApplicationMenu"] = $$createField0_0($$parsedSource["ApplicationMenu"]);
if ("Window" in $$parsedSource) {
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
}
if ("ContextMenu" in $$parsedSource) {
$$parsedSource["ContextMenu"] = $$createField1_0($$parsedSource["ContextMenu"]);
}
if ("KeyBinding" in $$parsedSource) {
$$parsedSource["KeyBinding"] = $$createField2_0($$parsedSource["KeyBinding"]);
}
if ("Browser" in $$parsedSource) {
$$parsedSource["Browser"] = $$createField3_0($$parsedSource["Browser"]);
}
if ("Env" in $$parsedSource) {
$$parsedSource["Env"] = $$createField4_0($$parsedSource["Env"]);
}
if ("Dialog" in $$parsedSource) {
$$parsedSource["Dialog"] = $$createField5_0($$parsedSource["Dialog"]);
}
if ("Event" in $$parsedSource) {
$$parsedSource["Event"] = $$createField6_0($$parsedSource["Event"]);
}
if ("Menu" in $$parsedSource) {
$$parsedSource["Menu"] = $$createField7_0($$parsedSource["Menu"]);
}
if ("Screen" in $$parsedSource) {
$$parsedSource["Screen"] = $$createField8_0($$parsedSource["Screen"]);
}
if ("Clipboard" in $$parsedSource) {
$$parsedSource["Clipboard"] = $$createField9_0($$parsedSource["Clipboard"]);
}
if ("SystemTray" in $$parsedSource) {
$$parsedSource["SystemTray"] = $$createField10_0($$parsedSource["SystemTray"]);
}
if ("Logger" in $$parsedSource) {
$$parsedSource["Logger"] = $$createField1_0($$parsedSource["Logger"]);
$$parsedSource["Logger"] = $$createField11_0($$parsedSource["Logger"]);
}
return new App($$parsedSource as Partial<App>);
}
}
export class Menu {
/**
* BrowserManager manages browser-related operations
*/
export class BrowserManager {
/** Creates a new Menu instance. */
constructor($$source: Partial<Menu> = {}) {
/** Creates a new BrowserManager instance. */
constructor($$source: Partial<BrowserManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new Menu instance from a string or object.
* Creates a new BrowserManager instance from a string or object.
*/
static createFrom($$source: any = {}): Menu {
static createFrom($$source: any = {}): BrowserManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Menu($$parsedSource as Partial<Menu>);
return new BrowserManager($$parsedSource as Partial<BrowserManager>);
}
}
/**
* ClipboardManager manages clipboard operations
*/
export class ClipboardManager {
/** Creates a new ClipboardManager instance. */
constructor($$source: Partial<ClipboardManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new ClipboardManager instance from a string or object.
*/
static createFrom($$source: any = {}): ClipboardManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ClipboardManager($$parsedSource as Partial<ClipboardManager>);
}
}
/**
* ContextMenuManager manages all context menu operations
*/
export class ContextMenuManager {
/** Creates a new ContextMenuManager instance. */
constructor($$source: Partial<ContextMenuManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new ContextMenuManager instance from a string or object.
*/
static createFrom($$source: any = {}): ContextMenuManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ContextMenuManager($$parsedSource as Partial<ContextMenuManager>);
}
}
/**
* DialogManager manages dialog-related operations
*/
export class DialogManager {
/** Creates a new DialogManager instance. */
constructor($$source: Partial<DialogManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new DialogManager instance from a string or object.
*/
static createFrom($$source: any = {}): DialogManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new DialogManager($$parsedSource as Partial<DialogManager>);
}
}
/**
* EnvironmentManager manages environment-related operations
*/
export class EnvironmentManager {
/** Creates a new EnvironmentManager instance. */
constructor($$source: Partial<EnvironmentManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new EnvironmentManager instance from a string or object.
*/
static createFrom($$source: any = {}): EnvironmentManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new EnvironmentManager($$parsedSource as Partial<EnvironmentManager>);
}
}
/**
* EventManager manages event-related operations
*/
export class EventManager {
/** Creates a new EventManager instance. */
constructor($$source: Partial<EventManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new EventManager instance from a string or object.
*/
static createFrom($$source: any = {}): EventManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new EventManager($$parsedSource as Partial<EventManager>);
}
}
/**
* KeyBindingManager manages all key binding operations
*/
export class KeyBindingManager {
/** Creates a new KeyBindingManager instance. */
constructor($$source: Partial<KeyBindingManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new KeyBindingManager instance from a string or object.
*/
static createFrom($$source: any = {}): KeyBindingManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new KeyBindingManager($$parsedSource as Partial<KeyBindingManager>);
}
}
/**
* MenuManager manages menu-related operations
*/
export class MenuManager {
/** Creates a new MenuManager instance. */
constructor($$source: Partial<MenuManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new MenuManager instance from a string or object.
*/
static createFrom($$source: any = {}): MenuManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new MenuManager($$parsedSource as Partial<MenuManager>);
}
}
export class ScreenManager {
/** Creates a new ScreenManager instance. */
constructor($$source: Partial<ScreenManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new ScreenManager instance from a string or object.
*/
static createFrom($$source: any = {}): ScreenManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ScreenManager($$parsedSource as Partial<ScreenManager>);
}
}
/**
* ServiceOptions provides optional parameters for calls to [NewService].
*/
export class ServiceOptions {
/**
* Name can be set to override the name of the service
* for logging and debugging purposes.
*
* If empty, it will default
* either to the value obtained through the [ServiceName] interface,
* or to the type name.
*/
"Name": string;
/**
* If the service instance implements [http.Handler],
* it will be mounted on the internal asset server
* at the prefix specified by Route.
*/
"Route": string;
/**
* MarshalError will be called if non-nil
* to marshal to JSON the error values returned by this service's methods.
*
* MarshalError is not allowed to fail,
* but it may return a nil slice to fall back
* to the globally configured error handler.
*
* If the returned slice is not nil, it must contain valid JSON.
*/
"MarshalError": any;
/** Creates a new ServiceOptions instance. */
constructor($$source: Partial<ServiceOptions> = {}) {
if (!("Name" in $$source)) {
this["Name"] = "";
}
if (!("Route" in $$source)) {
this["Route"] = "";
}
if (!("MarshalError" in $$source)) {
this["MarshalError"] = null;
}
Object.assign(this, $$source);
}
/**
* Creates a new ServiceOptions instance from a string or object.
*/
static createFrom($$source: any = {}): ServiceOptions {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ServiceOptions($$parsedSource as Partial<ServiceOptions>);
}
}
/**
* SystemTrayManager manages system tray-related operations
*/
export class SystemTrayManager {
/** Creates a new SystemTrayManager instance. */
constructor($$source: Partial<SystemTrayManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new SystemTrayManager instance from a string or object.
*/
static createFrom($$source: any = {}): SystemTrayManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new SystemTrayManager($$parsedSource as Partial<SystemTrayManager>);
}
}
@@ -79,8 +396,48 @@ export class WebviewWindow {
}
}
/**
* WindowManager manages all window-related operations
*/
export class WindowManager {
/** Creates a new WindowManager instance. */
constructor($$source: Partial<WindowManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new WindowManager instance from a string or object.
*/
static createFrom($$source: any = {}): WindowManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new WindowManager($$parsedSource as Partial<WindowManager>);
}
}
// Private type creation functions
const $$createType0 = Menu.createFrom;
const $$createType0 = WindowManager.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = slog$0.Logger.createFrom;
const $$createType2 = ContextMenuManager.createFrom;
const $$createType3 = $Create.Nullable($$createType2);
const $$createType4 = KeyBindingManager.createFrom;
const $$createType5 = $Create.Nullable($$createType4);
const $$createType6 = BrowserManager.createFrom;
const $$createType7 = $Create.Nullable($$createType6);
const $$createType8 = EnvironmentManager.createFrom;
const $$createType9 = $Create.Nullable($$createType8);
const $$createType10 = DialogManager.createFrom;
const $$createType11 = $Create.Nullable($$createType10);
const $$createType12 = EventManager.createFrom;
const $$createType13 = $Create.Nullable($$createType12);
const $$createType14 = MenuManager.createFrom;
const $$createType15 = $Create.Nullable($$createType14);
const $$createType16 = ScreenManager.createFrom;
const $$createType17 = $Create.Nullable($$createType16);
const $$createType18 = ClipboardManager.createFrom;
const $$createType19 = $Create.Nullable($$createType18);
const $$createType20 = SystemTrayManager.createFrom;
const $$createType21 = $Create.Nullable($$createType20);
const $$createType22 = slog$0.Logger.createFrom;
const $$createType23 = $Create.Nullable($$createType22);

View File

@@ -0,0 +1,64 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* Service represents the notifications service
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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 "../../application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* RemoveBadge removes the badge label from the application icon.
*/
export function RemoveBadge(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2374916939) as any;
return $resultPromise;
}
/**
* ServiceName returns the name of the service.
*/
export function ServiceName(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2428202016) as any;
return $resultPromise;
}
/**
* ServiceShutdown is called when the service is unloaded.
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3893755233) as any;
return $resultPromise;
}
/**
* ServiceStartup is called when the service is loaded.
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4078800764, options) as any;
return $resultPromise;
}
/**
* SetBadge sets the badge label on the application icon.
*/
export function SetBadge(label: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(784276339, label) as any;
return $resultPromise;
}
export function SetCustomBadge(label: string, options: $models.Options): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3058653106, label, options) as any;
return $resultPromise;
}

View File

@@ -0,0 +1,9 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as BadgeService from "./badgeservice.js";
export {
BadgeService
};
export * from "./models.js";

View File

@@ -0,0 +1,58 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as color$0 from "../../../../../../../image/color/models.js";
export class Options {
"TextColour": color$0.RGBA;
"BackgroundColour": color$0.RGBA;
"FontName": string;
"FontSize": number;
"SmallFontSize": number;
/** Creates a new Options instance. */
constructor($$source: Partial<Options> = {}) {
if (!("TextColour" in $$source)) {
this["TextColour"] = (new color$0.RGBA());
}
if (!("BackgroundColour" in $$source)) {
this["BackgroundColour"] = (new color$0.RGBA());
}
if (!("FontName" in $$source)) {
this["FontName"] = "";
}
if (!("FontSize" in $$source)) {
this["FontSize"] = 0;
}
if (!("SmallFontSize" in $$source)) {
this["SmallFontSize"] = 0;
}
Object.assign(this, $$source);
}
/**
* Creates a new Options instance from a string or object.
*/
static createFrom($$source: any = {}): Options {
const $$createField0_0 = $$createType0;
const $$createField1_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("TextColour" in $$parsedSource) {
$$parsedSource["TextColour"] = $$createField0_0($$parsedSource["TextColour"]);
}
if ("BackgroundColour" in $$parsedSource) {
$$parsedSource["BackgroundColour"] = $$createField1_0($$parsedSource["BackgroundColour"]);
}
return new Options($$parsedSource as Partial<Options>);
}
}
// Private type creation functions
const $$createType0 = color$0.RGBA.createFrom;

View File

@@ -0,0 +1,9 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as NotificationService from "./notificationservice.js";
export {
NotificationService
};
export * from "./models.js";

View File

@@ -0,0 +1,107 @@
// 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";
/**
* NotificationAction represents an action button for a notification.
*/
export class NotificationAction {
"id"?: string;
"title"?: string;
/**
* (macOS-specific)
*/
"destructive"?: boolean;
/** Creates a new NotificationAction instance. */
constructor($$source: Partial<NotificationAction> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new NotificationAction instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationAction {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new NotificationAction($$parsedSource as Partial<NotificationAction>);
}
}
/**
* NotificationCategory groups actions for notifications.
*/
export class NotificationCategory {
"id"?: string;
"actions"?: NotificationAction[];
"hasReplyField"?: boolean;
"replyPlaceholder"?: string;
"replyButtonTitle"?: string;
/** Creates a new NotificationCategory instance. */
constructor($$source: Partial<NotificationCategory> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new NotificationCategory instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationCategory {
const $$createField1_0 = $$createType1;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("actions" in $$parsedSource) {
$$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
}
return new NotificationCategory($$parsedSource as Partial<NotificationCategory>);
}
}
/**
* NotificationOptions contains configuration for a notification
*/
export class NotificationOptions {
"id": string;
"title": string;
/**
* (macOS and Linux only)
*/
"subtitle"?: string;
"body"?: string;
"categoryId"?: string;
"data"?: { [_: string]: any };
/** Creates a new NotificationOptions instance. */
constructor($$source: Partial<NotificationOptions> = {}) {
if (!("id" in $$source)) {
this["id"] = "";
}
if (!("title" in $$source)) {
this["title"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new NotificationOptions instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationOptions {
const $$createField5_0 = $$createType2;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("data" in $$parsedSource) {
$$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
}
return new NotificationOptions($$parsedSource as Partial<NotificationOptions>);
}
}
// Private type creation functions
const $$createType0 = NotificationAction.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Map($Create.Any, $Create.Any);

View File

@@ -0,0 +1,110 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* Service represents the notifications service
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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 "../../application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
export function CheckNotificationAuthorization(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(2216952893) as any;
return $resultPromise;
}
/**
* OnNotificationResponse registers a callback function that will be called when
* a notification response is received from the user.
*/
export function OnNotificationResponse(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1642697808, callback) as any;
return $resultPromise;
}
export function RegisterNotificationCategory(category: $models.NotificationCategory): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2917562919, category) as any;
return $resultPromise;
}
export function RemoveAllDeliveredNotifications(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3956282340) as any;
return $resultPromise;
}
export function RemoveAllPendingNotifications(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(108821341) as any;
return $resultPromise;
}
export function RemoveDeliveredNotification(identifier: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(975691940, identifier) as any;
return $resultPromise;
}
export function RemoveNotification(identifier: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3966653866, identifier) as any;
return $resultPromise;
}
export function RemoveNotificationCategory(categoryID: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2032615554, categoryID) as any;
return $resultPromise;
}
export function RemovePendingNotification(identifier: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3729049703, identifier) as any;
return $resultPromise;
}
/**
* Public methods that delegate to the implementation.
*/
export function RequestNotificationAuthorization(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(3933442950) as any;
return $resultPromise;
}
export function SendNotification(options: $models.NotificationOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3968228732, options) as any;
return $resultPromise;
}
export function SendNotificationWithActions(options: $models.NotificationOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1886542847, options) as any;
return $resultPromise;
}
/**
* ServiceName returns the name of the service.
*/
export function ServiceName(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2704532675) as any;
return $resultPromise;
}
/**
* ServiceShutdown is called when the service is unloaded.
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2550195434) as any;
return $resultPromise;
}
/**
* ServiceStartup is called when the service is loaded.
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4047820929, options) as any;
return $resultPromise;
}

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,46 @@
// 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";
/**
* RGBA represents a traditional 32-bit alpha-premultiplied color, having 8
* bits for each of red, green, blue and alpha.
*
* An alpha-premultiplied color component C has been scaled by alpha (A), so
* has valid values 0 <= C <= A.
*/
export class RGBA {
"R": number;
"G": number;
"B": number;
"A": number;
/** Creates a new RGBA instance. */
constructor($$source: Partial<RGBA> = {}) {
if (!("R" in $$source)) {
this["R"] = 0;
}
if (!("G" in $$source)) {
this["G"] = 0;
}
if (!("B" in $$source)) {
this["B"] = 0;
}
if (!("A" in $$source)) {
this["A"] = 0;
}
Object.assign(this, $$source);
}
/**
* Creates a new RGBA instance from a string or object.
*/
static createFrom($$source: any = {}): RGBA {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new RGBA($$parsedSource as Partial<RGBA>);
}
}

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,71 @@
// 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";
/**
* LanguageInfo 语言信息结构体
*/
export class LanguageInfo {
/**
* 语言代码
*/
"Code": string;
/**
* 语言名称
*/
"Name": string;
/** Creates a new LanguageInfo instance. */
constructor($$source: Partial<LanguageInfo> = {}) {
if (!("Code" in $$source)) {
this["Code"] = "";
}
if (!("Name" in $$source)) {
this["Name"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new LanguageInfo instance from a string or object.
*/
static createFrom($$source: any = {}): LanguageInfo {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new LanguageInfo($$parsedSource as Partial<LanguageInfo>);
}
}
/**
* TranslatorType 翻译器类型
*/
export enum TranslatorType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* GoogleTranslatorType 谷歌翻译器
*/
GoogleTranslatorType = "google",
/**
* BingTranslatorType 必应翻译器
*/
BingTranslatorType = "bing",
/**
* YoudaoTranslatorType 有道翻译器
*/
YoudaoTranslatorType = "youdao",
/**
* DeeplTranslatorType DeepL翻译器
*/
DeeplTranslatorType = "deepl",
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* BackupService 提供基于Git的备份功能
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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 models$0 from "../models/models.js";
/**
* HandleConfigChange 处理备份配置变更
*/
export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(395287784, config) as any;
return $resultPromise;
}
/**
* Initialize 初始化备份服务
*/
export function Initialize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1052437974) as any;
return $resultPromise;
}
/**
* PushToRemote 推送本地更改到远程仓库
*/
export function PushToRemote(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(262644139) as any;
return $resultPromise;
}
/**
* Reinitialize 重新初始化备份服务,用于响应配置变更
*/
export function Reinitialize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(301562543) as any;
return $resultPromise;
}
/**
* ServiceShutdown 服务关闭时的清理工作
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(422131801) as any;
return $resultPromise;
}
/**
* StartAutoBackup 启动自动备份定时器
*/
export function StartAutoBackup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3035755449) as any;
return $resultPromise;
}
/**
* StopAutoBackup 停止自动备份
*/
export function StopAutoBackup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2641894021) as any;
return $resultPromise;
}

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* ConfigService 提供基于 Viper 的配置管理功能
* ConfigService 应用配置服务
* @module
*/
@@ -34,6 +34,30 @@ export function GetConfig(): Promise<models$0.AppConfig | null> & { cancel(): vo
return $typingPromise;
}
/**
* GetConfigDir 获取配置目录
*/
export function GetConfigDir(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2275626561) as any;
return $resultPromise;
}
/**
* GetSettingsPath 获取设置文件路径
*/
export function GetSettingsPath(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2175583370) as any;
return $resultPromise;
}
/**
* MigrateConfig 执行配置迁移
*/
export function MigrateConfig(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(434292783) as any;
return $resultPromise;
}
/**
* ResetConfig 强制重置所有配置为默认值
*/
@@ -42,6 +66,14 @@ export function ResetConfig(): Promise<void> & { cancel(): void } {
return $resultPromise;
}
/**
* ServiceShutdown 关闭服务
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3963562361) as any;
return $resultPromise;
}
/**
* Set 设置配置项
*/
@@ -50,6 +82,14 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
return $resultPromise;
}
/**
* SetBackupConfigChangeCallback 设置备份配置变更回调
*/
export function SetBackupConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3264871659, callback) as any;
return $resultPromise;
}
/**
* SetDataPathChangeCallback 设置数据路径配置变更回调
*/
@@ -66,6 +106,14 @@ export function SetHotkeyChangeCallback(callback: any): Promise<void> & { cancel
return $resultPromise;
}
/**
* SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
*/
export function SetWindowSnapConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2324961653, callback) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.AppConfig.createFrom;
const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -0,0 +1,47 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* DatabaseService provides shared database functionality
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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";
/**
* OnDataPathChanged handles data path changes
*/
export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3652863491) as any;
return $resultPromise;
}
/**
* RegisterModel 注册模型与表的映射关系
*/
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
return $resultPromise;
}
/**
* ServiceShutdown shuts down the service when the application closes
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3907893632) as any;
return $resultPromise;
}
/**
* ServiceStartup initializes the service when the application starts
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2067840771, options) as any;
return $resultPromise;
}

View File

@@ -22,6 +22,14 @@ export function SelectDirectory(): Promise<string> & { cancel(): void } {
return $resultPromise;
}
/**
* SelectFile 打开文件选择对话框
*/
export function SelectFile(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(37302920) as any;
return $resultPromise;
}
/**
* SetWindow 设置绑定的窗口
*/

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* DocumentService 提供文档管理功能
* DocumentService provides document management functionality
* @module
*/
@@ -10,23 +10,18 @@
// @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";
/**
* ForceSave 强制保存
* CreateDocument creates a new document and returns the created document with ID
*/
export function ForceSave(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2767091023) as any;
return $resultPromise;
}
/**
* GetActiveDocument 获取活动文档
*/
export function GetActiveDocument(): Promise<models$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1785823398) as any;
export function CreateDocument(title: string): Promise<models$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3360680842, title) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -35,45 +30,106 @@ export function GetActiveDocument(): Promise<models$0.Document | null> & { cance
}
/**
* Initialize 初始化服务
* DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
*/
export function Initialize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3418008221) as any;
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(412287269, id) as any;
return $resultPromise;
}
/**
* OnDataPathChanged 处理数据路径变更
* GetDocumentByID gets a document by ID
*/
export function OnDataPathChanged(oldPath: string, newPath: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(269349439, oldPath, newPath) as any;
export function GetDocumentByID(id: number): Promise<models$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3468193232, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
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;
}
/**
* ReloadDocument 重新加载文档
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
*/
export function ReloadDocument(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3093415283) as any;
export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3073950297) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ListDeletedDocumentsMeta lists all deleted document metadata
*/
export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(490143787) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* LockDocument 锁定文档,防止删除
*/
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1889494473, id) as any;
return $resultPromise;
}
/**
* ServiceShutdown 关闭服务
* RestoreDocument restores a deleted document
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(638578044) as any;
export function RestoreDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(784200778, id) as any;
return $resultPromise;
}
/**
* UpdateActiveDocumentContent 更新文档内容
* ServiceStartup initializes the service when the application starts
*/
export function UpdateActiveDocumentContent(content: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1486276638, content) as any;
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1474135487, options) as any;
return $resultPromise;
}
/**
* UnlockDocument 解锁文档
*/
export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(222307930, id) as any;
return $resultPromise;
}
/**
* UpdateDocumentContent updates the content of a document
*/
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
return $resultPromise;
}
/**
* UpdateDocumentTitle updates the title of a document
*/
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Document.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);

View File

@@ -0,0 +1,74 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* ExtensionService 扩展管理服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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";
/**
* GetAllExtensions 获取所有扩展配置
*/
export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3094292124) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ResetAllExtensionsToDefault 重置所有扩展到默认状态
*/
export function ResetAllExtensionsToDefault(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(270611949) as any;
return $resultPromise;
}
/**
* ResetExtensionToDefault 重置扩展到默认状态
*/
export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(868308101, id) as any;
return $resultPromise;
}
/**
* ServiceStartup 启动时调用
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(40324057, options) as any;
return $resultPromise;
}
/**
* UpdateExtensionEnabled 更新扩展启用状态
*/
export function UpdateExtensionEnabled(id: models$0.ExtensionID, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
return $resultPromise;
}
/**
* UpdateExtensionState 更新扩展状态
*/
export function UpdateExtensionState(id: models$0.ExtensionID, enabled: boolean, config: models$0.ExtensionConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2917946454, id, enabled, config) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Extension.createFrom;
const $$createType1 = $Create.Array($$createType0);

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* HotkeyService 全局热键服务
* HotkeyService Windows全局热键服务
* @module
*/
@@ -18,7 +18,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
import * as models$0 from "../models/models.js";
/**
* GetCurrentHotkey 获取当前注册的热键
* GetCurrentHotkey 获取当前热键
*/
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2572811187) as any;
@@ -32,13 +32,13 @@ export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { can
/**
* Initialize 初始化热键服务
*/
export function Initialize(app: application$0.App | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3671360458, app) as any;
export function Initialize(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3671360458, app, mainWindow) as any;
return $resultPromise;
}
/**
* IsRegistered 检查是否已注册热键
* IsRegistered 检查是否已注册
*/
export function IsRegistered(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(106954156) as any;
@@ -54,21 +54,13 @@ export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise<voi
}
/**
* ServiceShutdown 关闭热键服务
* ServiceShutdown 关闭服务
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(157291181) as any;
return $resultPromise;
}
/**
* ToggleWindow 切换窗口显示/隐藏 - 通过事件通知前端处理
*/
export function ToggleWindow(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1318185132) as any;
return $resultPromise;
}
/**
* UnregisterHotkey 取消注册全局热键
*/

View File

@@ -1,23 +1,41 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as BackupService from "./backupservice.js";
import * as ConfigService from "./configservice.js";
import * as DatabaseService from "./databaseservice.js";
import * as DialogService from "./dialogservice.js";
import * as DocumentService from "./documentservice.js";
import * as ExtensionService from "./extensionservice.js";
import * as HotkeyService from "./hotkeyservice.js";
import * as KeyBindingService from "./keybindingservice.js";
import * as MigrationService from "./migrationservice.js";
import * as SelfUpdateService from "./selfupdateservice.js";
import * as StartupService from "./startupservice.js";
import * as SystemService from "./systemservice.js";
import * as TestService from "./testservice.js";
import * as ThemeService from "./themeservice.js";
import * as TranslationService from "./translationservice.js";
import * as TrayService from "./trayservice.js";
import * as WindowService from "./windowservice.js";
export {
BackupService,
ConfigService,
DatabaseService,
DialogService,
DocumentService,
ExtensionService,
HotkeyService,
KeyBindingService,
MigrationService,
SelfUpdateService,
StartupService,
SystemService,
TrayService
TestService,
ThemeService,
TranslationService,
TrayService,
WindowService
};
export * from "./models.js";

View File

@@ -10,38 +10,13 @@
// @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";
/**
* DisableKeyBinding 禁用快捷键
*/
export function DisableKeyBinding(command: models$0.KeyBindingCommand): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1594003006, command) as any;
return $resultPromise;
}
/**
* EnableKeyBinding 启用快捷键
*/
export function EnableKeyBinding(command: models$0.KeyBindingCommand): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1462644129, command) as any;
return $resultPromise;
}
/**
* ExportKeyBindings 导出快捷键配置
*/
export function ExportKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(4089030977) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetAllKeyBindings 获取所有快捷键配置
*/
@@ -55,89 +30,13 @@ export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel()
}
/**
* GetKeyBindingByCommand 根据命令获取快捷键
* ServiceStartup 启动时调用
*/
export function GetKeyBindingByCommand(command: models$0.KeyBindingCommand): Promise<models$0.KeyBinding | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3066982544, command) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetKeyBindingCategories 获取所有快捷键分类
*/
export function GetKeyBindingCategories(): Promise<models$0.KeyBindingCategory[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3141399810) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType3($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetKeyBindingConfig 获取完整快捷键配置
*/
export function GetKeyBindingConfig(): Promise<models$0.KeyBindingConfig | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3804318356) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType5($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetKeyBindingsByCategory 根据分类获取快捷键
*/
export function GetKeyBindingsByCategory(category: models$0.KeyBindingCategory): Promise<models$0.KeyBinding[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1686146606, category) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ImportKeyBindings 导入快捷键配置
*/
export function ImportKeyBindings(keyBindings: models$0.KeyBinding[]): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(642201520, keyBindings) as any;
return $resultPromise;
}
/**
* ResetAllKeyBindings 重置所有快捷键到默认值
*/
export function ResetAllKeyBindings(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2771372645) as any;
return $resultPromise;
}
/**
* ResetKeyBinding 重置快捷键到默认值
*/
export function ResetKeyBinding(command: models$0.KeyBindingCommand): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3466323405, command) as any;
return $resultPromise;
}
/**
* UpdateKeyBinding 更新快捷键
*/
export function UpdateKeyBinding(command: models$0.KeyBindingCommand, newKey: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1469368983, command, newKey) as any;
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2057121990, options) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.KeyBinding.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Nullable($$createType0);
const $$createType3 = $Create.Array($Create.Any);
const $$createType4 = models$0.KeyBindingConfig.createFrom;
const $$createType5 = $Create.Nullable($$createType4);

View File

@@ -5,6 +5,10 @@
// @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 application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* MemoryStats 内存统计信息
*/
@@ -76,19 +80,8 @@ export class MemoryStats {
* MigrationProgress 迁移进度信息
*/
export class MigrationProgress {
/**
* 迁移状态
*/
"status": MigrationStatus;
/**
* 进度百分比 (0-100)
*/
"progress": number;
/**
* 错误信息
*/
"error"?: string;
/** Creates a new MigrationProgress instance. */
@@ -121,18 +114,150 @@ export enum MigrationStatus {
*/
$zero = "",
/**
* 迁移中
*/
MigrationStatusMigrating = "migrating",
/**
* 完成
*/
MigrationStatusCompleted = "completed",
/**
* 失败
*/
MigrationStatusFailed = "failed",
};
/**
* SelfUpdateResult 自我更新结果
*/
export class SelfUpdateResult {
/**
* 是否有更新
*/
"hasUpdate": boolean;
/**
* 当前版本
*/
"currentVersion": string;
/**
* 最新版本
*/
"latestVersion": string;
/**
* 是否已应用更新
*/
"updateApplied": boolean;
/**
* 下载链接
*/
"assetURL": string;
/**
* 发布说明
*/
"releaseNotes": string;
/**
* 错误信息
*/
"error": string;
/**
* 更新源github/gitea
*/
"source": string;
/** Creates a new SelfUpdateResult instance. */
constructor($$source: Partial<SelfUpdateResult> = {}) {
if (!("hasUpdate" in $$source)) {
this["hasUpdate"] = false;
}
if (!("currentVersion" in $$source)) {
this["currentVersion"] = "";
}
if (!("latestVersion" in $$source)) {
this["latestVersion"] = "";
}
if (!("updateApplied" in $$source)) {
this["updateApplied"] = false;
}
if (!("assetURL" in $$source)) {
this["assetURL"] = "";
}
if (!("releaseNotes" in $$source)) {
this["releaseNotes"] = "";
}
if (!("error" in $$source)) {
this["error"] = "";
}
if (!("source" in $$source)) {
this["source"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new SelfUpdateResult instance from a string or object.
*/
static createFrom($$source: any = {}): SelfUpdateResult {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new SelfUpdateResult($$parsedSource as Partial<SelfUpdateResult>);
}
}
/**
* WindowInfo 窗口信息(简化版)
*/
export class WindowInfo {
"Window": application$0.WebviewWindow | null;
"DocumentID": number;
"Title": string;
/** Creates a new WindowInfo instance. */
constructor($$source: Partial<WindowInfo> = {}) {
if (!("Window" in $$source)) {
this["Window"] = null;
}
if (!("DocumentID" in $$source)) {
this["DocumentID"] = 0;
}
if (!("Title" in $$source)) {
this["Title"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new WindowInfo instance from a string or object.
*/
static createFrom($$source: any = {}): WindowInfo {
const $$createField0_0 = $$createType1;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("Window" in $$parsedSource) {
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
}
return new WindowInfo($$parsedSource as Partial<WindowInfo>);
}
}
/**
* WindowSnapService 窗口吸附服务
*/
export class WindowSnapService {
/** Creates a new WindowSnapService instance. */
constructor($$source: Partial<WindowSnapService> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new WindowSnapService instance from a string or object.
*/
static createFrom($$source: any = {}): WindowSnapService {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new WindowSnapService($$parsedSource as Partial<WindowSnapService>);
}
}
// Private type creation functions
const $$createType0 = application$0.WebviewWindow.createFrom;
const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -0,0 +1,51 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* SelfUpdateService 自我更新服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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 $models from "./models.js";
/**
* ApplyUpdate 应用更新
*/
export function ApplyUpdate(): Promise<$models.SelfUpdateResult | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2009328394) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* CheckForUpdates 检查更新
*/
export function CheckForUpdates(): Promise<$models.SelfUpdateResult | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(438757208) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* RestartApplication 重启应用程序
*/
export function RestartApplication(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3341481538) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $models.SelfUpdateResult.createFrom;
const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -0,0 +1,19 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* StartupService 开机启动服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* SetEnabled 设置开机启动状态
*/
export function SetEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2911601468, enabled) as any;
return $resultPromise;
}

View File

@@ -0,0 +1,55 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* TestService 测试服务 - 仅在开发环境使用
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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";
/**
* ClearAll 清除所有测试状态
*/
export function ClearAll(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2179720854) as any;
return $resultPromise;
}
/**
* ServiceStartup 服务启动时调用
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(617408198, options) as any;
return $resultPromise;
}
/**
* TestBadge 测试Badge功能
*/
export function TestBadge(text: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4242952145, text) as any;
return $resultPromise;
}
/**
* TestNotification 测试通知功能
*/
export function TestNotification(title: string, subtitle: string, body: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1697553289, title, subtitle, body) as any;
return $resultPromise;
}
/**
* TestUpdateNotification 测试更新通知
*/
export function TestUpdateNotification(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3091730060) as any;
return $resultPromise;
}

View File

@@ -0,0 +1,104 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* ThemeService 主题服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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";
/**
* CreateTheme 创建新主题
*/
export function CreateTheme(theme: models$0.Theme | null): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3274757686, theme) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetAllThemes 获取所有主题
*/
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(2425053076) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetDefaultThemes 获取默认主题
*/
export function GetDefaultThemes(): Promise<{ [_: string]: models$0.Theme | null }> & { cancel(): void } {
let $resultPromise = $Call.ByID(3801788118) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType3($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetThemeByType 根据类型获取默认主题
*/
export function GetThemeByType(themeType: models$0.ThemeType): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1680465265, themeType) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ResetThemeColors 重置主题颜色为默认值
*/
export function ResetThemeColors(themeType: models$0.ThemeType): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(342461245, themeType) as any;
return $resultPromise;
}
/**
* ServiceShutdown 服务关闭
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1676749034) as any;
return $resultPromise;
}
/**
* ServiceStartup 服务启动时初始化
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2915959937, options) as any;
return $resultPromise;
}
/**
* UpdateThemeColors 更新主题颜色
*/
export function UpdateThemeColors(themeType: models$0.ThemeType, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2750902529, themeType, colors) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Theme.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);
const $$createType3 = $Create.Map($Create.Any, $$createType1);

View File

@@ -0,0 +1,78 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* TranslationService 翻译服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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 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")
* @returns {map[string]string} 语言代码到名称的映射
* @returns {error} 可能的错误
*/
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 $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* IsLanguageSupported 检查指定的语言代码是否受支持
*/
export function IsLanguageSupported(translatorType: translator$0.TranslatorType, languageCode: string): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(2819945417, translatorType, languageCode) as any;
return $resultPromise;
}
/**
* TranslateWith 使用指定翻译器进行翻译
* @param {string} text - 待翻译文本
* @param {string} from - 源语言代码 (如 "en", "zh", "auto")
* @param {string} to - 目标语言代码 (如 "en", "zh")
* @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl")
* @returns {string} 翻译后的文本
* @returns {error} 可能的错误
*/
export function TranslateWith(text: string, $from: string, to: string, translatorType: string): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(3577923623, text, $from, to, translatorType) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $Create.Array($Create.Any);
const $$createType1 = translator$0.LanguageInfo.createFrom;
const $$createType2 = $Create.Map($Create.Any, $$createType1);

View File

@@ -0,0 +1,75 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* WindowService 窗口管理服务(专注于窗口生命周期管理)
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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";
/**
* GetOpenWindows 获取所有打开的窗口信息
*/
export function GetOpenWindows(): Promise<$models.WindowInfo[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1464997251) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开
*/
export function IsDocumentWindowOpen(documentID: number): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(1735611839, documentID) as any;
return $resultPromise;
}
/**
* OpenDocumentWindow 为指定文档ID打开新窗口
*/
export function OpenDocumentWindow(documentID: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(494716471, documentID) as any;
return $resultPromise;
}
/**
* ServiceShutdown 实现服务关闭接口
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(202192783) as any;
return $resultPromise;
}
/**
* SetAppReferences 设置应用和主窗口引用
*/
export function SetAppReferences(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1120840759, app, mainWindow) as any;
return $resultPromise;
}
/**
* SetWindowSnapService 设置窗口吸附服务引用
*/
export function SetWindowSnapService(snapService: $models.WindowSnapService | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1105193745, snapService) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $models.WindowInfo.createFrom;
const $$createType1 = $Create.Array($$createType0);

View File

@@ -9,7 +9,9 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
LoadingScreen: typeof import('./src/components/loading/LoadingScreen.vue')['default']
MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default']
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']

View File

@@ -9,5 +9,6 @@
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script src="/math.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -5,75 +5,88 @@
"type": "module",
"scripts": {
"dev": "vite --host --mode development",
"build:dev": "vue-tsc && vite build --minify false --mode development",
"build": "vue-tsc && vite build --mode production",
"build:dev": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" vue-tsc && cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" vite build --minify false --mode development",
"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"
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-angular": "^0.1.4",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-html": "^6.4.10",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-less": "^6.0.2",
"@codemirror/lang-lezer": "^6.0.1",
"@codemirror/lang-liquid": "^6.2.3",
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/lang-lezer": "^6.0.2",
"@codemirror/lang-liquid": "^6.3.0",
"@codemirror/lang-markdown": "^6.3.4",
"@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-sql": "^6.9.0",
"@codemirror/lang-sql": "^6.9.1",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-wast": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.1",
"@codemirror/language": "^6.11.3",
"@codemirror/language-data": "^6.5.1",
"@codemirror/legacy-modes": "^6.5.1",
"@codemirror/lint": "^6.8.5",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.37.2",
"@codemirror/view": "^6.38.2",
"@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@types/uuid": "^10.0.0",
"@vueuse/core": "^13.3.0",
"codemirror": "^6.0.1",
"@prettier/plugin-xml": "^3.4.2",
"@reteps/dockerfmt": "^0.3.6",
"@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",
"lezer": "^0.13.5",
"java-parser": "^3.0.1",
"jsox": "^1.2.123",
"linguist-languages": "^9.0.0",
"php-parser": "^3.2.5",
"pinia": "^3.0.3",
"prettier": "^3.5.3",
"sass": "^1.89.2",
"uuid": "^11.1.0",
"vue": "^3.5.17",
"vue-i18n": "^11.1.6",
"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",
"vue-i18n": "^11.1.12",
"vue-pick-colors": "^1.8.0",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"@lezer/generator": "^1.7.3",
"@types/node": "^24.0.3",
"@vitejs/plugin-vue": "^5.2.4",
"@eslint/js": "^9.35.0",
"@lezer/generator": "^1.8.0",
"@types/node": "^24.3.1",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.1",
"@wailsio/runtime": "latest",
"eslint": "^9.29.0",
"eslint-plugin-vue": "^10.2.0",
"globals": "^16.2.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1",
"unplugin-vue-components": "^28.7.0",
"vite": "^6.3.5",
"vue-eslint-parser": "^10.1.3",
"vue-tsc": "^2.2.10"
"cross-env": "^7.0.3",
"eslint": "^9.35.0",
"eslint-plugin-vue": "^10.4.0",
"globals": "^16.4.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.43.0",
"unplugin-vue-components": "^29.0.0",
"vite": "^7.1.5",
"vite-plugin-node-polyfills": "^0.24.0",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.6"
}
}

View File

@@ -1,7 +1,5 @@
importScripts("guesslang.min.js")
const LANGUAGES = ["json", "py", "html", "sql", "md", "java", "php", "css", "xml", "cpp", "rs", "cs", "rb", "sh", "yaml", "toml", "go", "clj", "ex", "erl", "js", "ts", "swift", "kt", "groovy", "ps1", "dart", "scala"]
const guessLang = new self.GuessLang()
function sendResult(language, confidence, idx) {
@@ -27,20 +25,13 @@ onmessage = (event) => {
guessLang.runModel(content).then((result) => {
if (result.length > 0) {
const lang = result[0]
if (LANGUAGES.includes(lang.languageId) && lang.confidence > 0.15) {
sendResult(lang.languageId, lang.confidence, idx)
// 返回置信度最高的结果
const bestResult = result[0]
if (bestResult.confidence > 0.15) {
sendResult(bestResult.languageId, bestResult.confidence, idx)
return
}
}
for (let lang of result) {
if (LANGUAGES.includes(lang.languageId) && lang.confidence > 0.5) {
sendResult(lang.languageId, lang.confidence, idx)
return
}
}
sendResult("text", 0.0, idx)
}).catch(() => {
sendResult("text", 0.0, idx)

3
frontend/public/math.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,19 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useConfigStore } from '@/stores/configStore';
import { useSystemStore } from '@/stores/systemStore';
import { useKeybindingStore } from '@/stores/keybindingStore';
import {onMounted} 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';
const configStore = useConfigStore();
const systemStore = useSystemStore();
const keybindingStore = useKeybindingStore();
const themeStore = useThemeStore();
const updateStore = useUpdateStore();
const backupStore = useBackupStore();
// 应用启动时加载配置和初始化系统信息
onMounted(async () => {
@@ -18,13 +24,21 @@ onMounted(async () => {
keybindingStore.loadKeyBindings(),
]);
// 初始化语言和主题
await configStore.initializeLanguage();
themeStore.initializeTheme();
// 初始化备份服务
await backupStore.initialize();
// 启动时检查更新
await updateStore.checkOnStartup();
});
</script>
<template>
<div class="app-container">
<WindowTitleBar />
<WindowTitleBar/>
<div class="app-content">
<router-view/>
</div>

View File

@@ -23,6 +23,16 @@
--dark-scrollbar-track: #2a2a2a;
--dark-scrollbar-thumb: #555555;
--dark-scrollbar-thumb-hover: #666666;
--dark-selection-bg: rgba(181, 206, 168, 0.1);
--dark-selection-text: #b5cea8;
--dark-danger-color: #ff6b6b;
--dark-bg-primary: #1a1a1a;
--dark-bg-hover: #2a2a2a;
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
--dark-loading-color: #fff;
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--dark-loading-done-color: #6f6;
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
/* 浅色主题颜色变量 */
--light-toolbar-bg: #f8f9fa;
@@ -45,6 +55,16 @@
--light-scrollbar-track: #f1f3f4;
--light-scrollbar-thumb: #c1c1c1;
--light-scrollbar-thumb-hover: #a8a8a8;
--light-selection-bg: rgba(59, 130, 246, 0.15);
--light-selection-text: #2563eb;
--light-danger-color: #dc3545;
--light-bg-primary: #ffffff;
--light-bg-hover: #f1f3f4;
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--light-loading-color: #1a3c1a;
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--light-loading-done-color: #008800;
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* 默认使用深色主题 */
--toolbar-bg: var(--dark-toolbar-bg);
@@ -68,6 +88,17 @@
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
color-scheme: light dark;
}
@@ -96,6 +127,16 @@
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
}
}
@@ -123,6 +164,16 @@
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb);
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg);
--selection-text: var(--light-selection-text);
--text-danger: var(--light-danger-color);
--bg-primary: var(--light-bg-primary);
--bg-hover: var(--light-bg-hover);
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
--voidraft-loading-color: var(--light-loading-color);
--voidraft-loading-glow: var(--light-loading-glow);
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay);
}
}
@@ -149,6 +200,16 @@
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb);
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg);
--selection-text: var(--light-selection-text);
--text-danger: var(--light-danger-color);
--bg-primary: var(--light-bg-primary);
--bg-hover: var(--light-bg-hover);
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
--voidraft-loading-color: var(--light-loading-color);
--voidraft-loading-glow: var(--light-loading-glow);
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay);
}
/* 手动选择深色主题 */
@@ -174,4 +235,14 @@
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
import fs from "node:fs/promises";
import initAsync from "./clang-format.js";
const wasm = new URL("./clang-format.wasm", import.meta.url);
export default function (init = fs.readFile(wasm)) {
return initAsync(init);
}
export * from "./clang-format.js";

View File

@@ -0,0 +1,8 @@
import initAsync from "./clang-format.js";
import wasm from "./clang-format.wasm?url";
export default function (input = wasm) {
return initAsync(input);
}
export * from "./clang-format.js";

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
Module.preRun = function customPreRun() {
ENV.PWD = process.cwd();
}

View File

@@ -0,0 +1,858 @@
#!/usr/bin/env python3
#
# ===- git-clang-format - ClangFormat Git Integration -------*- python -*--=== #
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ===----------------------------------------------------------------------=== #
r"""
clang-format git integration
============================
This file provides a clang-format integration for git. Put it somewhere in your
path and ensure that it is executable. Then, "git clang-format" will invoke
clang-format on the changes in current files or a specific commit.
For further details, run:
git clang-format -h
Requires Python version >=3.8
"""
from __future__ import absolute_import, division, print_function
import argparse
import collections
import contextlib
import errno
import os
import re
import subprocess
import sys
usage = "git clang-format [OPTIONS] [<commit>] [<commit>|--staged] [--] [<file>...]"
desc = """
If zero or one commits are given, run clang-format on all lines that differ
between the working directory and <commit>, which defaults to HEAD. Changes are
only applied to the working directory, or in the stage/index.
Examples:
To format staged changes, i.e everything that's been `git add`ed:
git clang-format
To also format everything touched in the most recent commit:
git clang-format HEAD~1
If you're on a branch off main, to format everything touched on your branch:
git clang-format main
If two commits are given (requires --diff), run clang-format on all lines in the
second <commit> that differ from the first <commit>.
The following git-config settings set the default of the corresponding option:
clangFormat.binary
clangFormat.commit
clangFormat.extensions
clangFormat.style
"""
# Name of the temporary index file in which save the output of clang-format.
# This file is created within the .git directory.
temp_index_basename = "clang-format-index"
Range = collections.namedtuple("Range", "start, count")
def main():
config = load_git_config()
# In order to keep '--' yet allow options after positionals, we need to
# check for '--' ourselves. (Setting nargs='*' throws away the '--', while
# nargs=argparse.REMAINDER disallows options after positionals.)
argv = sys.argv[1:]
try:
idx = argv.index("--")
except ValueError:
dash_dash = []
else:
dash_dash = argv[idx:]
argv = argv[:idx]
default_extensions = ",".join(
[
# From clang/lib/Frontend/FrontendOptions.cpp, all lower case
"c",
"h", # C
"m", # ObjC
"mm", # ObjC++
"cc",
"cp",
"cpp",
"c++",
"cxx",
"hh",
"hpp",
"hxx",
"inc", # C++
"ccm",
"cppm",
"cxxm",
"c++m", # C++ Modules
"cu",
"cuh", # CUDA
"cl", # OpenCL
# Other languages that clang-format supports
"proto",
"protodevel", # Protocol Buffers
"java", # Java
"js",
"mjs",
"cjs", # JavaScript
"ts", # TypeScript
"cs", # C Sharp
"json",
"ipynb", # Json
"sv",
"svh",
"v",
"vh", # Verilog
"td", # TableGen
"txtpb",
"textpb",
"pb.txt",
"textproto",
"asciipb", # TextProto
]
)
p = argparse.ArgumentParser(
usage=usage,
formatter_class=argparse.RawDescriptionHelpFormatter,
description=desc,
)
p.add_argument(
"--binary",
default=config.get("clangformat.binary", "clang-format"),
help="path to clang-format",
),
p.add_argument(
"--commit",
default=config.get("clangformat.commit", "HEAD"),
help="default commit to use if none is specified",
),
p.add_argument(
"--diff",
action="store_true",
help="print a diff instead of applying the changes",
)
p.add_argument(
"--diffstat",
action="store_true",
help="print a diffstat instead of applying the changes",
)
p.add_argument(
"--extensions",
default=config.get("clangformat.extensions", default_extensions),
help=(
"comma-separated list of file extensions to format, "
"excluding the period and case-insensitive"
),
),
p.add_argument(
"-f",
"--force",
action="store_true",
help="allow changes to unstaged files",
)
p.add_argument(
"-p", "--patch", action="store_true", help="select hunks interactively"
)
p.add_argument(
"-q",
"--quiet",
action="count",
default=0,
help="print less information",
)
p.add_argument(
"--staged",
"--cached",
action="store_true",
help="format lines in the stage instead of the working dir",
)
p.add_argument(
"--style",
default=config.get("clangformat.style", None),
help="passed to clang-format",
),
p.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="print extra information",
)
p.add_argument(
"--diff_from_common_commit",
action="store_true",
help=(
"diff from the last common commit for commits in "
"separate branches rather than the exact point of the "
"commits"
),
)
# We gather all the remaining positional arguments into 'args' since we need
# to use some heuristics to determine whether or not <commit> was present.
# However, to print pretty messages, we make use of metavar and help.
p.add_argument(
"args",
nargs="*",
metavar="<commit>",
help="revision from which to compute the diff",
)
p.add_argument(
"ignored",
nargs="*",
metavar="<file>...",
help="if specified, only consider differences in these files",
)
opts = p.parse_args(argv)
opts.verbose -= opts.quiet
del opts.quiet
commits, files = interpret_args(opts.args, dash_dash, opts.commit)
if len(commits) > 2:
die("at most two commits allowed; %d given" % len(commits))
if len(commits) == 2:
if opts.staged:
die("--staged is not allowed when two commits are given")
if not opts.diff:
die("--diff is required when two commits are given")
elif opts.diff_from_common_commit:
die("--diff_from_common_commit is only allowed when two commits are given")
if os.path.dirname(opts.binary):
opts.binary = os.path.abspath(opts.binary)
changed_lines = compute_diff_and_extract_lines(
commits, files, opts.staged, opts.diff_from_common_commit
)
if opts.verbose >= 1:
ignored_files = set(changed_lines)
filter_by_extension(changed_lines, opts.extensions.lower().split(","))
# The computed diff outputs absolute paths, so we must cd before accessing
# those files.
cd_to_toplevel()
filter_symlinks(changed_lines)
filter_ignored_files(changed_lines, binary=opts.binary)
if opts.verbose >= 1:
ignored_files.difference_update(changed_lines)
if ignored_files:
print(
"Ignoring the following files (wrong extension, symlink, or "
"ignored by clang-format):"
)
for filename in ignored_files:
print(" %s" % filename)
if changed_lines:
print("Running clang-format on the following files:")
for filename in changed_lines:
print(" %s" % filename)
if not changed_lines:
if opts.verbose >= 0:
print("no modified files to format")
return 0
if len(commits) > 1:
old_tree = commits[1]
revision = old_tree
elif opts.staged:
old_tree = create_tree_from_index(changed_lines)
revision = ""
else:
old_tree = create_tree_from_workdir(changed_lines)
revision = None
new_tree = run_clang_format_and_save_to_tree(
changed_lines, revision, binary=opts.binary, style=opts.style
)
if opts.verbose >= 1:
print("old tree: %s" % old_tree)
print("new tree: %s" % new_tree)
if old_tree == new_tree:
if opts.verbose >= 0:
print("clang-format did not modify any files")
return 0
if opts.diff:
return print_diff(old_tree, new_tree)
if opts.diffstat:
return print_diffstat(old_tree, new_tree)
changed_files = apply_changes(
old_tree, new_tree, force=opts.force, patch_mode=opts.patch
)
if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1:
print("changed files:")
for filename in changed_files:
print(" %s" % filename)
return 1
def load_git_config(non_string_options=None):
"""Return the git configuration as a dictionary.
All options are assumed to be strings unless in `non_string_options`, in
which is a dictionary mapping option name (in lower case) to either "--bool"
or "--int"."""
if non_string_options is None:
non_string_options = {}
out = {}
for entry in run("git", "config", "--list", "--null").split("\0"):
if entry:
if "\n" in entry:
name, value = entry.split("\n", 1)
else:
# A setting with no '=' ('\n' with --null) is implicitly 'true'
name = entry
value = "true"
if name in non_string_options:
value = run("git", "config", non_string_options[name], name)
out[name] = value
return out
def interpret_args(args, dash_dash, default_commit):
"""Interpret `args` as "[commits] [--] [files]" and return (commits, files).
It is assumed that "--" and everything that follows has been removed from
args and placed in `dash_dash`.
If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its
left (if present) are taken as commits. Otherwise, the arguments are
checked from left to right if they are commits or files. If commits are not
given, a list with `default_commit` is used."""
if dash_dash:
if len(args) == 0:
commits = [default_commit]
else:
commits = args
for commit in commits:
object_type = get_object_type(commit)
if object_type not in ("commit", "tag"):
if object_type is None:
die("'%s' is not a commit" % commit)
else:
die(
"'%s' is a %s, but a commit was expected"
% (commit, object_type)
)
files = dash_dash[1:]
elif args:
commits = []
while args:
if not disambiguate_revision(args[0]):
break
commits.append(args.pop(0))
if not commits:
commits = [default_commit]
files = args
else:
commits = [default_commit]
files = []
return commits, files
def disambiguate_revision(value):
"""Returns True if `value` is a revision, False if it is a file, or dies."""
# If `value` is ambiguous (neither a commit nor a file), the following
# command will die with an appropriate error message.
run("git", "rev-parse", value, verbose=False)
object_type = get_object_type(value)
if object_type is None:
return False
if object_type in ("commit", "tag"):
return True
die("`%s` is a %s, but a commit or filename was expected" % (value, object_type))
def get_object_type(value):
"""Returns a string description of an object's type, or None if it is not
a valid git object."""
cmd = ["git", "cat-file", "-t", value]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
return None
return convert_string(stdout.strip())
def compute_diff_and_extract_lines(commits, files, staged, diff_common_commit):
"""Calls compute_diff() followed by extract_lines()."""
diff_process = compute_diff(commits, files, staged, diff_common_commit)
changed_lines = extract_lines(diff_process.stdout)
diff_process.stdout.close()
diff_process.wait()
if diff_process.returncode != 0:
# Assume error was already printed to stderr.
sys.exit(2)
return changed_lines
def compute_diff(commits, files, staged, diff_common_commit):
"""Return a subprocess object producing the diff from `commits`.
The return value's `stdin` file object will produce a patch with the
differences between the working directory (or stage if --staged is used) and
the first commit if a single one was specified, or the difference between
both specified commits, filtered on `files` (if non-empty).
Zero context lines are used in the patch."""
git_tool = "diff-index"
extra_args = []
if len(commits) == 2:
git_tool = "diff-tree"
if diff_common_commit:
commits = [f"{commits[0]}...{commits[1]}"]
elif staged:
extra_args += ["--cached"]
cmd = ["git", git_tool, "-p", "-U0"] + extra_args + commits + ["--"]
cmd.extend(files)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
p.stdin.close()
return p
def extract_lines(patch_file):
"""Extract the changed lines in `patch_file`.
The return value is a dictionary mapping filename to a list of (start_line,
line_count) pairs.
The input must have been produced with ``-U0``, meaning unidiff format with
zero lines of context. The return value is a dict mapping filename to a
list of line `Range`s."""
matches = {}
for line in patch_file:
line = convert_string(line)
match = re.search(r"^\+\+\+\ [^/]+/(.*)", line)
if match:
filename = match.group(1).rstrip("\r\n\t")
match = re.search(r"^@@ -[0-9,]+ \+(\d+)(,(\d+))?", line)
if match:
start_line = int(match.group(1))
line_count = 1
if match.group(3):
line_count = int(match.group(3))
if line_count == 0:
line_count = 1
if start_line == 0:
continue
matches.setdefault(filename, []).append(Range(start_line, line_count))
return matches
def filter_by_extension(dictionary, allowed_extensions):
"""Delete every key in `dictionary` that doesn't have an allowed extension.
`allowed_extensions` must be a collection of lowercase file extensions,
excluding the period."""
allowed_extensions = frozenset(allowed_extensions)
for filename in list(dictionary.keys()):
base_ext = filename.rsplit(".", 1)
if len(base_ext) == 1 and "" in allowed_extensions:
continue
if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:
del dictionary[filename]
def filter_symlinks(dictionary):
"""Delete every key in `dictionary` that is a symlink."""
for filename in list(dictionary.keys()):
if os.path.islink(filename):
del dictionary[filename]
def filter_ignored_files(dictionary, binary):
"""Delete every key in `dictionary` that is ignored by clang-format."""
ignored_files = run(binary, "-list-ignored", *dictionary.keys())
if not ignored_files:
return
ignored_files = ignored_files.split("\n")
for filename in ignored_files:
del dictionary[filename]
def cd_to_toplevel():
"""Change to the top level of the git repository."""
toplevel = run("git", "rev-parse", "--show-toplevel")
os.chdir(toplevel)
def create_tree_from_workdir(filenames):
"""Create a new git tree with the given files from the working directory.
Returns the object ID (SHA-1) of the created tree."""
return create_tree(filenames, "--stdin")
def create_tree_from_index(filenames):
# Copy the environment, because the files have to be read from the original
# index.
env = os.environ.copy()
def index_contents_generator():
for filename in filenames:
git_ls_files_cmd = [
"git",
"ls-files",
"--stage",
"-z",
"--",
filename,
]
git_ls_files = subprocess.Popen(
git_ls_files_cmd,
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
stdout = git_ls_files.communicate()[0]
yield convert_string(stdout.split(b"\0")[0])
return create_tree(index_contents_generator(), "--index-info")
def run_clang_format_and_save_to_tree(
changed_lines, revision=None, binary="clang-format", style=None
):
"""Run clang-format on each file and save the result to a git tree.
Returns the object ID (SHA-1) of the created tree."""
# Copy the environment when formatting the files in the index, because the
# files have to be read from the original index.
env = os.environ.copy() if revision == "" else None
def iteritems(container):
try:
return container.iteritems() # Python 2
except AttributeError:
return container.items() # Python 3
def index_info_generator():
for filename, line_ranges in iteritems(changed_lines):
if revision is not None:
if len(revision) > 0:
git_metadata_cmd = [
"git",
"ls-tree",
"%s:%s" % (revision, os.path.dirname(filename)),
os.path.basename(filename),
]
else:
git_metadata_cmd = [
"git",
"ls-files",
"--stage",
"--",
filename,
]
git_metadata = subprocess.Popen(
git_metadata_cmd,
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
stdout = git_metadata.communicate()[0]
mode = oct(int(stdout.split()[0], 8))
else:
mode = oct(os.stat(filename).st_mode)
# Adjust python3 octal format so that it matches what git expects
if mode.startswith("0o"):
mode = "0" + mode[2:]
blob_id = clang_format_to_blob(
filename,
line_ranges,
revision=revision,
binary=binary,
style=style,
env=env,
)
yield "%s %s\t%s" % (mode, blob_id, filename)
return create_tree(index_info_generator(), "--index-info")
def create_tree(input_lines, mode):
"""Create a tree object from the given input.
If mode is '--stdin', it must be a list of filenames. If mode is
'--index-info' is must be a list of values suitable for "git update-index
--index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other
mode is invalid."""
assert mode in ("--stdin", "--index-info")
cmd = ["git", "update-index", "--add", "-z", mode]
with temporary_index_file():
p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
for line in input_lines:
p.stdin.write(to_bytes("%s\0" % line))
p.stdin.close()
if p.wait() != 0:
die("`%s` failed" % " ".join(cmd))
tree_id = run("git", "write-tree")
return tree_id
def clang_format_to_blob(
filename,
line_ranges,
revision=None,
binary="clang-format",
style=None,
env=None,
):
"""Run clang-format on the given file and save the result to a git blob.
Runs on the file in `revision` if not None, or on the file in the working
directory if `revision` is None. Revision can be set to an empty string to
run clang-format on the file in the index.
Returns the object ID (SHA-1) of the created blob."""
clang_format_cmd = [binary]
if style:
clang_format_cmd.extend(["--style=" + style])
clang_format_cmd.extend(
[
"--lines=%s:%s" % (start_line, start_line + line_count - 1)
for start_line, line_count in line_ranges
]
)
if revision is not None:
clang_format_cmd.extend(["--assume-filename=" + filename])
git_show_cmd = [
"git",
"cat-file",
"blob",
"%s:%s" % (revision, filename),
]
git_show = subprocess.Popen(
git_show_cmd, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
git_show.stdin.close()
clang_format_stdin = git_show.stdout
else:
clang_format_cmd.extend([filename])
git_show = None
clang_format_stdin = subprocess.PIPE
try:
clang_format = subprocess.Popen(
clang_format_cmd, stdin=clang_format_stdin, stdout=subprocess.PIPE
)
if clang_format_stdin == subprocess.PIPE:
clang_format_stdin = clang_format.stdin
except OSError as e:
if e.errno == errno.ENOENT:
die('cannot find executable "%s"' % binary)
else:
raise
clang_format_stdin.close()
hash_object_cmd = [
"git",
"hash-object",
"-w",
"--path=" + filename,
"--stdin",
]
hash_object = subprocess.Popen(
hash_object_cmd, stdin=clang_format.stdout, stdout=subprocess.PIPE
)
clang_format.stdout.close()
stdout = hash_object.communicate()[0]
if hash_object.returncode != 0:
die("`%s` failed" % " ".join(hash_object_cmd))
if clang_format.wait() != 0:
die("`%s` failed" % " ".join(clang_format_cmd))
if git_show and git_show.wait() != 0:
die("`%s` failed" % " ".join(git_show_cmd))
return convert_string(stdout).rstrip("\r\n")
@contextlib.contextmanager
def temporary_index_file(tree=None):
"""Context manager for setting GIT_INDEX_FILE to a temporary file and
deleting the file afterward."""
index_path = create_temporary_index(tree)
old_index_path = os.environ.get("GIT_INDEX_FILE")
os.environ["GIT_INDEX_FILE"] = index_path
try:
yield
finally:
if old_index_path is None:
del os.environ["GIT_INDEX_FILE"]
else:
os.environ["GIT_INDEX_FILE"] = old_index_path
os.remove(index_path)
def create_temporary_index(tree=None):
"""Create a temporary index file and return the created file's path.
If `tree` is not None, use that as the tree to read in. Otherwise, an
empty index is created."""
gitdir = run("git", "rev-parse", "--git-dir")
path = os.path.join(gitdir, temp_index_basename)
if tree is None:
tree = "--empty"
run("git", "read-tree", "--index-output=" + path, tree)
return path
def print_diff(old_tree, new_tree):
"""Print the diff between the two trees to stdout."""
# We use the porcelain 'diff' and not plumbing 'diff-tree' because the
# output is expected to be viewed by the user, and only the former does nice
# things like color and pagination.
#
# We also only print modified files since `new_tree` only contains the files
# that were modified, so unmodified files would show as deleted without the
# filter.
return subprocess.run(
["git", "diff", "--diff-filter=M", "--exit-code", old_tree, new_tree]
).returncode
def print_diffstat(old_tree, new_tree):
"""Print the diffstat between the two trees to stdout."""
# We use the porcelain 'diff' and not plumbing 'diff-tree' because the
# output is expected to be viewed by the user, and only the former does nice
# things like color and pagination.
#
# We also only print modified files since `new_tree` only contains the files
# that were modified, so unmodified files would show as deleted without the
# filter.
return subprocess.run(
[
"git",
"diff",
"--diff-filter=M",
"--exit-code",
"--stat",
old_tree,
new_tree,
]
).returncode
def apply_changes(old_tree, new_tree, force=False, patch_mode=False):
"""Apply the changes in `new_tree` to the working directory.
Bails if there are local changes in those files and not `force`. If
`patch_mode`, runs `git checkout --patch` to select hunks interactively."""
changed_files = (
run(
"git",
"diff-tree",
"--diff-filter=M",
"-r",
"-z",
"--name-only",
old_tree,
new_tree,
)
.rstrip("\0")
.split("\0")
)
if not force:
unstaged_files = run("git", "diff-files", "--name-status", *changed_files)
if unstaged_files:
print(
"The following files would be modified but have unstaged changes:",
file=sys.stderr,
)
print(unstaged_files, file=sys.stderr)
print("Please commit, stage, or stash them first.", file=sys.stderr)
sys.exit(2)
if patch_mode:
# In patch mode, we could just as well create an index from the new tree
# and checkout from that, but then the user will be presented with a
# message saying "Discard ... from worktree". Instead, we use the old
# tree as the index and checkout from new_tree, which gives the slightly
# better message, "Apply ... to index and worktree". This is not quite
# right, since it won't be applied to the user's index, but oh well.
with temporary_index_file(old_tree):
subprocess.run(["git", "checkout", "--patch", new_tree], check=True)
index_tree = old_tree
else:
with temporary_index_file(new_tree):
run("git", "checkout-index", "-f", "--", *changed_files)
return changed_files
def run(*args, **kwargs):
stdin = kwargs.pop("stdin", "")
verbose = kwargs.pop("verbose", True)
strip = kwargs.pop("strip", True)
for name in kwargs:
raise TypeError("run() got an unexpected keyword argument '%s'" % name)
p = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
)
stdout, stderr = p.communicate(input=stdin)
stdout = convert_string(stdout)
stderr = convert_string(stderr)
if p.returncode == 0:
if stderr:
if verbose:
print("`%s` printed to stderr:" % " ".join(args), file=sys.stderr)
print(stderr.rstrip(), file=sys.stderr)
if strip:
stdout = stdout.rstrip("\r\n")
return stdout
if verbose:
print("`%s` returned %s" % (" ".join(args), p.returncode), file=sys.stderr)
if stderr:
print(stderr.rstrip(), file=sys.stderr)
sys.exit(2)
def die(message):
print("error:", message, file=sys.stderr)
sys.exit(2)
def to_bytes(str_input):
# Encode to UTF-8 to get binary data.
if isinstance(str_input, bytes):
return str_input
return str_input.encode("utf-8")
def to_string(bytes_input):
if isinstance(bytes_input, str):
return bytes_input
return bytes_input.encode("utf-8")
def convert_string(bytes_input):
try:
return to_string(bytes_input.decode("utf-8"))
except AttributeError: # 'str' object has no attribute 'decode'.
return str(bytes_input)
except UnicodeError:
return str(bytes_input)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,216 @@
/**
* Prettier Plugin for C/C++/C#/Java/Protobuf formatting using clang-format WebAssembly
*
* This plugin provides support for formatting multiple languages using the clang-format WASM implementation.
* Supported languages:
* - C / C++
* - Objective-C / Objective-C++
* - C#
* - Java
* - Protocol Buffer (Protobuf)
*
* It supports various file extensions and common clang-format styles.
*/
import type { Plugin, Parser, Printer } from 'prettier';
// Import the clang-format WASM module
import clangFormatInit, { format } from './clang-format-vite.js';
const parserName = 'clang-format';
// Language configuration
const languages = [
{
name: 'C',
aliases: ['c'],
parsers: ['c'],
extensions: ['.c', '.h'],
filenames: ['*.c', '*.h'],
aceMode: 'c_cpp',
tmScope: 'source.c',
linguistLanguageId: 50,
vscodeLanguageIds: ['c']
},
{
name: 'C++',
aliases: ['cpp', 'cxx', 'cc'],
parsers: ['cpp'],
extensions: ['.cpp', '.cxx', '.cc', '.hpp', '.hxx', '.hh', '.C', '.H'],
filenames: ['*.cpp', '*.cxx', '*.cc', '*.hpp', '*.hxx', '*.hh', '*.C', '*.H'],
aceMode: 'c_cpp',
tmScope: 'source.cpp',
linguistLanguageId: 43,
vscodeLanguageIds: ['cpp']
},
{
name: 'Objective-C',
aliases: ['objc', 'objectivec'],
parsers: ['objective-c'],
extensions: ['.m'],
filenames: ['*.m'],
aceMode: 'objectivec',
tmScope: 'source.objc',
linguistLanguageId: 259,
vscodeLanguageIds: ['objective-c']
},
{
name: 'Objective-C++',
aliases: ['objcpp', 'objectivecpp'],
parsers: ['objective-cpp'],
extensions: ['.mm'],
filenames: ['*.mm'],
aceMode: 'objectivec',
tmScope: 'source.objcpp',
linguistLanguageId: 260,
vscodeLanguageIds: ['objective-cpp']
},
{
name: 'C#',
aliases: ['csharp', 'cs'],
parsers: ['cs'],
extensions: ['.cs'],
filenames: ['*.cs'],
aceMode: 'csharp',
tmScope: 'source.cs',
linguistLanguageId: 42,
vscodeLanguageIds: ['csharp']
},
{
name: 'Java',
aliases: ['java'],
parsers: ['java'],
extensions: ['.java'],
filenames: ['*.java'],
aceMode: 'java',
tmScope: 'source.java',
linguistLanguageId: 181,
vscodeLanguageIds: ['java']
},
{
name: 'Protocol Buffer',
aliases: ['protobuf', 'proto'],
parsers: ['proto'],
extensions: ['.proto'],
filenames: ['*.proto'],
aceMode: 'protobuf',
tmScope: 'source.proto',
linguistLanguageId: 297,
vscodeLanguageIds: ['proto']
}
];
// Parser configuration
const clangParser: Parser<string> = {
astFormat: parserName,
parse: (text: string) => text,
locStart: () => 0,
locEnd: (node: string) => node.length,
};
// Lazy initialize clang-format WASM module
let initPromise: Promise<void> | null = null;
let isInitialized = false;
function initClangFormat(): Promise<void> {
if (isInitialized) {
return Promise.resolve();
}
if (!initPromise) {
initPromise = (async () => {
try {
await clangFormatInit();
isInitialized = true;
} catch (error) {
console.warn('Failed to initialize clang-format WASM module:', error);
initPromise = null;
throw error;
}
})();
}
return initPromise;
}
// Printer configuration
const clangPrinter: Printer<string> = {
// @ts-expect-error -- Support async printer like shell plugin
async print(path, options) {
try {
// Wait for initialization to complete
await initClangFormat();
const text = (path as any).getValue ? (path as any).getValue() : path.node;
const style = getClangStyle(options);
// Format using clang-format (synchronous call)
const formatted = format(text, options.filename, style);
return formatted.trim();
} catch (error) {
console.warn('clang-format failed:', error);
// Return original text if formatting fails
return (path as any).getValue ? (path as any).getValue() : path.node;
}
},
};
// Helper function to determine clang-format style
function getClangStyle(options: any): string {
// You can extend this to support more options
const style = options.clangStyle || 'LLVM';
// Support common styles
const validStyles = ['LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit', 'Microsoft', 'GNU'];
if (validStyles.includes(style)) {
return style;
}
// Default to LLVM style
return 'LLVM';
}
// Plugin options
const options = {
clangStyle: {
since: '0.0.1',
category: 'Format' as const,
type: 'choice' as const,
default: 'LLVM',
description: 'The clang-format style to use',
choices: [
{ value: 'LLVM', description: 'LLVM coding standards' },
{ value: 'Google', description: "Google's C++ style guide" },
{ value: 'Chromium', description: "Chromium's style guide" },
{ value: 'Mozilla', description: "Mozilla's style guide" },
{ value: 'WebKit', description: "WebKit's style guide" },
{ value: 'Microsoft', description: "Microsoft's style guide" },
{ value: 'GNU', description: 'GNU coding standards' }
]
},
filename: {
// since: '0.1.0',
category: 'Config',
type: 'string',
default: undefined,
description: 'Custom filename to use for web_fmt processing (affects language detection)',
}
};
// Plugin object
const clangPlugin: Plugin = {
languages,
parsers: {
[parserName]: clangParser,
},
printers: {
[parserName]: clangPrinter,
},
...options,
};
export default clangPlugin;
export { languages };
export const parsers = clangPlugin.parsers;
export const printers = clangPlugin.printers;

View File

@@ -0,0 +1,105 @@
cmake_minimum_required(VERSION 3.20)
project(clang-format-wasm)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Set Emscripten flags for WASM
if(EMSCRIPTEN)
set(CMAKE_EXECUTABLE_SUFFIX ".js")
# Common Emscripten flags
set(EMSCRIPTEN_FLAGS
-O3
-sWASM=1
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']
-sALLOW_MEMORY_GROWTH=1
-sMODULARIZE=1
-sEXPORT_NAME='Module'
-sENVIRONMENT=web,webview,worker
-sUSE_ES6_IMPORT_META=0
--no-entry
)
# Library-specific flags
set(LIB_EMSCRIPTEN_FLAGS
${EMSCRIPTEN_FLAGS}
-sEXPORTED_FUNCTIONS=['_malloc','_free']
--bind
)
# CLI-specific flags
set(CLI_EMSCRIPTEN_FLAGS
${EMSCRIPTEN_FLAGS}
-sEXPORTED_FUNCTIONS=['_main']
-sINVOKE_RUN=0
-sNODERAWFS=1
)
endif()
# Find LLVM
find_package(LLVM REQUIRED CONFIG)
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")
# Include LLVM headers
include_directories(${LLVM_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})
# Find Clang
find_package(Clang REQUIRED CONFIG)
# Get LLVM components
llvm_map_components_to_libnames(llvm_libs support core)
# Define source files
set(LIB_SOURCES
lib.cc
lib.h
binding.cc
)
set(CLI_SOURCES
cli.cc
)
# Create library target
add_executable(clang-format-wasm ${LIB_SOURCES})
# Link against Clang and LLVM libraries
target_link_libraries(clang-format-wasm
clangFormat
clangToolingCore
clangBasic
clangRewrite
${llvm_libs}
)
# Create CLI target
add_executable(clang-format-cli ${CLI_SOURCES})
target_link_libraries(clang-format-cli
clangFormat
clangToolingCore
clangBasic
clangRewrite
${llvm_libs}
)
# Set Emscripten flags
if(EMSCRIPTEN)
# Configure library target
set_target_properties(clang-format-wasm PROPERTIES
COMPILE_FLAGS "${LIB_EMSCRIPTEN_FLAGS}"
LINK_FLAGS "${LIB_EMSCRIPTEN_FLAGS}"
OUTPUT_NAME "clang-format-esm"
)
# Configure CLI target
set_target_properties(clang-format-cli PROPERTIES
COMPILE_FLAGS "${CLI_EMSCRIPTEN_FLAGS}"
LINK_FLAGS "${CLI_EMSCRIPTEN_FLAGS}"
OUTPUT_NAME "clang-format-cli"
)
endif()

View File

@@ -0,0 +1,117 @@
#include "CustomFileSystem.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/Path.h"
#include <emscripten.h>
#include <iostream>
#include <string>
#include <system_error>
using namespace llvm;
using namespace llvm::vfs;
namespace {
bool isRunningOnWindows() {
return EM_ASM_INT({return process.platform == 'win32' ? 1 : 0}) == 1;
}
std::error_code current_path(SmallVectorImpl<char> &result) {
result.clear();
const char *pwd = ::getenv("PWD");
result.append(pwd, pwd + strlen(pwd));
return {};
}
} // namespace
namespace llvm {
namespace vfs {
sys::path::Style getPathStyle() {
static sys::path::Style cachedStyle = sys::path::Style::native;
if (cachedStyle == sys::path::Style::native) {
cachedStyle = isRunningOnWindows() ? sys::path::Style::windows
: sys::path::Style::posix;
}
return cachedStyle;
}
void make_absolute(const Twine &current_directory,
SmallVectorImpl<char> &path) {
StringRef p(path.data(), path.size());
auto pathStyle = getPathStyle();
bool rootDirectory = sys::path::has_root_directory(p, pathStyle);
bool rootName = sys::path::has_root_name(p, pathStyle);
// Already absolute.
if ((rootName || is_style_posix(pathStyle)) && rootDirectory)
return;
// All of the following conditions will need the current directory.
SmallString<128> current_dir;
current_directory.toVector(current_dir);
// Relative path. Prepend the current directory.
if (!rootName && !rootDirectory) {
// Append path to the current directory.
sys::path::append(current_dir, pathStyle, p);
// Set path to the result.
path.swap(current_dir);
return;
}
if (!rootName && rootDirectory) {
StringRef cdrn = sys::path::root_name(current_dir, pathStyle);
SmallString<128> curDirRootName(cdrn.begin(), cdrn.end());
sys::path::append(curDirRootName, pathStyle, p);
// Set path to the result.
path.swap(curDirRootName);
return;
}
if (rootName && !rootDirectory) {
StringRef pRootName = sys::path::root_name(p, pathStyle);
StringRef bRootDirectory =
sys::path::root_directory(current_dir, pathStyle);
StringRef bRelativePath = sys::path::relative_path(current_dir, pathStyle);
StringRef pRelativePath = sys::path::relative_path(p, pathStyle);
SmallString<128> res;
sys::path::append(res, pathStyle, pRootName, bRootDirectory, bRelativePath,
pRelativePath);
path.swap(res);
return;
}
llvm_unreachable("All rootName and rootDirectory combinations should have "
"occurred above!");
}
std::error_code make_absolute(SmallVectorImpl<char> &path) {
if (sys::path::is_absolute(path, getPathStyle()))
return {};
SmallString<128> current_dir;
if (std::error_code ec = current_path(current_dir))
return ec;
make_absolute(current_dir, path);
return {};
}
CustomFileSystem::CustomFileSystem(IntrusiveRefCntPtr<FileSystem> FS)
: ProxyFileSystem(std::move(FS)) {}
std::error_code
CustomFileSystem::makeAbsolute(SmallVectorImpl<char> &Path) const {
return make_absolute(Path);
}
} // namespace vfs
} // namespace llvm

View File

@@ -0,0 +1,27 @@
#ifndef CUSTOM_FILE_SYSTEM_H
#define CUSTOM_FILE_SYSTEM_H
#include "llvm/ADT/IntrusiveRefCntPtr.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/ErrorOr.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/VirtualFileSystem.h"
namespace llvm {
namespace vfs {
sys::path::Style getPathStyle();
std::error_code make_absolute(SmallVectorImpl<char> &path);
class CustomFileSystem : public ProxyFileSystem {
public:
CustomFileSystem(IntrusiveRefCntPtr<FileSystem> FS);
std::error_code makeAbsolute(SmallVectorImpl<char> &Path) const override;
};
} // namespace vfs
} // namespace llvm
#endif // CUSTOM_FILE_SYSTEM_H

View File

@@ -0,0 +1,100 @@
# Clang Format WASM Plugin
这是一个基于 clang-format WebAssembly 的 Prettier 插件,支持格式化 C/C++/C#/Java/Protobuf 代码。
## 目录结构
```
clang/
├── src/ # 源码目录
│ ├── scripts/ # 构建和工具脚本
│ │ ├── build.sh # 主构建脚本
│ │ ├── gen_patch.sh # 补丁生成脚本
│ │ └── cli.patch # CLI 修改补丁
│ ├── *.cc # C++ 源文件
│ ├── *.h # C++ 头文件
│ ├── CMakeLists.txt # CMake 构建配置
│ ├── package.json # NPM 包配置
│ ├── clang-format.d.ts # TypeScript 类型定义
│ ├── template.js # JavaScript 模板
│ └── clang-format-diff.py # Python 差异工具
├── *.js # 编译后的 JavaScript 文件
├── *.wasm # 编译后的 WebAssembly 文件
├── *.cjs # CommonJS 格式的 CLI 工具
├── git-clang-format # Git 集成工具
└── index.ts # 插件入口文件
```
## 构建说明
### 前提条件
- Install LLVM and Clang (version 18 or later)
- Install CMake (version 3.27 or later)
- Install Ninja (version 1.11 or later)
### 构建步骤
1. Clone this repository
2. 进入源码目录:
```bash
cd src
```
3. 运行构建脚本:
```bash
./scripts/build.sh
```
构建脚本会:
- 创建 `build` 目录并编译源码
- 将编译结果复制到上级目录(插件目录)
- 生成 WebAssembly 文件和 JavaScript 绑定
- 复制必要的工具和类型定义文件
### 输出文件
构建完成后,插件目录下会包含:
- `clang-format.wasm` - WebAssembly 库文件
- `clang-format.js` - JavaScript 绑定文件
- `clang-format-cli.cjs` - CLI 工具
- `clang-format-cli.wasm` - CLI WebAssembly 文件
- `git-clang-format` - Git 集成工具
- `clang-format-diff.py` - 差异工具
## 开发说明
### 修改源码
- C++ 源文件位于 `src/` 目录下
- 修改后运行 `./scripts/build.sh` 重新构建
- 类型定义文件 `src/clang-format.d.ts` 需要与实际 API 保持同步
### 生成补丁
如果修改了 CLI 相关代码,可以使用:
```bash
./scripts/gen_patch.sh
```
生成补丁文件 `scripts/cli.patch`。
## 使用说明
插件会自动加载编译后的 WebAssembly 文件,支持以下语言:
- C/C++
- Objective-C/C++
- C#
- Java
- Protocol Buffer
支持的 clang-format 样式:
- LLVM
- Google
- Chromium
- Mozilla
- WebKit
- Microsoft
- GNU
- 自定义样式

View File

@@ -0,0 +1,26 @@
#include "lib.h"
#include <emscripten/bind.h>
using namespace emscripten;
EMSCRIPTEN_BINDINGS(my_module) {
register_vector<unsigned>("RangeList");
value_object<Result>("Result")
.field("error", &Result::error)
.field("content", &Result::content);
function<std::string>("version", &version);
function<Result, const std::string, const std::string, const std::string>(
"format", &format);
function<Result, const std::string, const std::string, const std::string,
const std::vector<unsigned>>("format_byte", &format_byte);
function<Result, const std::string, const std::string, const std::string,
const std::vector<unsigned>>("format_line", &format_line);
function<void, const std::string>("set_fallback_style", &set_fallback_style);
function<void, bool>("set_sort_includes", &set_sort_includes);
function<Result, const std::string, const std::string, const std::string>(
"dump_config", &dump_config);
}
int main(void) {}

View File

@@ -0,0 +1,197 @@
#!/usr/bin/env python3
#
# ===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===#
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ===------------------------------------------------------------------------===#
"""
This script reads input from a unified diff and reformats all the changed
lines. This is useful to reformat all the lines touched by a specific patch.
Example usage for git/svn users:
git diff -U0 --no-color --relative HEAD^ | {clang_format_diff} -p1 -i
svn diff --diff-cmd=diff -x-U0 | {clang_format_diff} -i
It should be noted that the filename contained in the diff is used unmodified
to determine the source file to update. Users calling this script directly
should be careful to ensure that the path in the diff is correct relative to the
current working directory.
"""
from __future__ import absolute_import, division, print_function
import argparse
import difflib
import re
import subprocess
import sys
if sys.version_info.major >= 3:
from io import StringIO
else:
from io import BytesIO as StringIO
def main():
parser = argparse.ArgumentParser(
description=__doc__.format(clang_format_diff="%(prog)s"),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-i",
action="store_true",
default=False,
help="apply edits to files instead of displaying a diff",
)
parser.add_argument(
"-p",
metavar="NUM",
default=0,
help="strip the smallest prefix containing P slashes",
)
parser.add_argument(
"-regex",
metavar="PATTERN",
default=None,
help="custom pattern selecting file paths to reformat "
"(case sensitive, overrides -iregex)",
)
parser.add_argument(
"-iregex",
metavar="PATTERN",
default=r".*\.(?:cpp|cc|c\+\+|cxx|cppm|ccm|cxxm|c\+\+m|c|cl|h|hh|hpp"
r"|hxx|m|mm|inc|js|ts|proto|protodevel|java|cs|json|ipynb|s?vh?)",
help="custom pattern selecting file paths to reformat "
"(case insensitive, overridden by -regex)",
)
parser.add_argument(
"-sort-includes",
action="store_true",
default=False,
help="let clang-format sort include blocks",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="be more verbose, ineffective without -i",
)
parser.add_argument(
"-style",
help="formatting style to apply (LLVM, GNU, Google, Chromium, "
"Microsoft, Mozilla, WebKit)",
)
parser.add_argument(
"-fallback-style",
help="The name of the predefined style used as a"
"fallback in case clang-format is invoked with"
"-style=file, but can not find the .clang-format"
"file to use.",
)
parser.add_argument(
"-binary",
default="clang-format",
help="location of binary to use for clang-format",
)
args = parser.parse_args()
# Extract changed lines for each file.
filename = None
lines_by_file = {}
for line in sys.stdin:
match = re.search(r"^\+\+\+\ (.*?/){%s}(.+)" % args.p, line.rstrip())
if match:
filename = match.group(2)
if filename is None:
continue
if args.regex is not None:
if not re.match("^%s$" % args.regex, filename):
continue
else:
if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE):
continue
match = re.search(r"^@@.*\+(\d+)(?:,(\d+))?", line)
if match:
start_line = int(match.group(1))
line_count = 1
if match.group(2):
line_count = int(match.group(2))
# The input is something like
#
# @@ -1, +0,0 @@
#
# which means no lines were added.
if line_count == 0:
continue
# Also format lines range if line_count is 0 in case of deleting
# surrounding statements.
end_line = start_line
if line_count != 0:
end_line += line_count - 1
lines_by_file.setdefault(filename, []).extend(
["--lines", str(start_line) + ":" + str(end_line)]
)
# Reformat files containing changes in place.
has_diff = False
for filename, lines in lines_by_file.items():
if args.i and args.verbose:
print("Formatting {}".format(filename))
command = [args.binary, filename]
if args.i:
command.append("-i")
if args.sort_includes:
command.append("--sort-includes")
command.extend(lines)
if args.style:
command.extend(["--style", args.style])
if args.fallback_style:
command.extend(["--fallback-style", args.fallback_style])
try:
p = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=None,
stdin=subprocess.PIPE,
universal_newlines=True,
)
except OSError as e:
# Give the user more context when clang-format isn't
# found/isn't executable, etc.
raise RuntimeError(
'Failed to run "%s" - %s"' % (" ".join(command), e.strerror)
)
stdout, _stderr = p.communicate()
if p.returncode != 0:
return p.returncode
if not args.i:
with open(filename) as f:
code = f.readlines()
formatted_code = StringIO(stdout).readlines()
diff = difflib.unified_diff(
code,
formatted_code,
filename,
filename,
"(before formatting)",
"(after formatting)",
)
diff_string = "".join(diff)
if len(diff_string) > 0:
has_diff = True
sys.stdout.write(diff_string)
if has_diff:
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,175 @@
export type InitInput =
| RequestInfo
| URL
| Response
| BufferSource
| WebAssembly.Module;
export default function init(input?: InitInput): Promise<void>;
/**
* The style to use for formatting.
* Supported style values are:
* - `LLVM` - A style complying with the LLVM coding standards.
* - `Google` - A style complying with Googles C++ style guide.
* - `Chromium` - A style complying with Chromiums style guide.
* - `Mozilla` - A style complying with Mozillas style guide.
* - `WebKit` - A style complying with WebKits style guide.
* - `Microsoft` - A style complying with Microsofts style guide.
* - `GNU` - A style complying with the GNU coding standards.
* - A string starting with `{`, for example: `{BasedOnStyle: Chromium, IndentWidth: 4, ...}`.
* - A string which represents `.clang-format` content.
*
*/
export type Style =
| "LLVM"
| "Google"
| "Chromium"
| "Mozilla"
| "WebKit"
| "Microsoft"
| "GNU"
| (string & {});
/**
* The filename to use for determining the language.
*/
export type Filename =
| "main.c"
| "main.cc"
| "main.cxx"
| "main.cpp"
| "main.java"
| "main.js"
| "main.mjs"
| "main.ts"
| "main.json"
| "main.m"
| "main.mm"
| "main.proto"
| "main.cs"
| (string & {});
/**
* Formats the given content using the specified style.
*
* @param {string} content - The content to format.
* @param {Filename} filename - The filename to use for determining the language.
* @param {Style} style - The style to use for formatting.
* Supported style values are:
* - `LLVM` - A style complying with the LLVM coding standards.
* - `Google` - A style complying with Googles C++ style guide.
* - `Chromium` - A style complying with Chromiums style guide.
* - `Mozilla` - A style complying with Mozillas style guide.
* - `WebKit` - A style complying with WebKits style guide.
* - `Microsoft` - A style complying with Microsofts style guide.
* - `GNU` - A style complying with the GNU coding standards.
* - A string starting with `{`, for example: `{BasedOnStyle: Chromium, IndentWidth: 4, ...}`.
* - A string which represents `.clang-format` content.
*
* @returns {string} The formatted content.
* @throws {Error}
*
* @see {@link https://clang.llvm.org/docs/ClangFormatStyleOptions.html}
*/
export declare function format(
content: string,
filename?: Filename,
style?: Style,
): string;
/**
* Both the startLine and endLine are 1-based.
*/
export type LineRange = [startLine: number, endLine: number];
/**
* Both the offset and length are measured in bytes.
*/
export type ByteRange = [offset: number, length: number];
/**
* Formats the specified range of lines in the given content using the specified style.
*
* @param {string} content - The content to format.
* @param {LineRange[]} range - Array<[startLine, endLine]> - The range of lines to format.
* Both startLine and endLine are 1-based.
* Multiple ranges can be formatted by specifying several lines arguments.
* @param {Filename} filename - The filename to use for determining the language.
* @param {Style} style - The style to use for formatting.
* Supported style values are:
* - `LLVM` - A style complying with the LLVM coding standards.
* - `Google` - A style complying with Googles C++ style guide.
* - `Chromium` - A style complying with Chromiums style guide.
* - `Mozilla` - A style complying with Mozillas style guide.
* - `WebKit` - A style complying with WebKits style guide.
* - `Microsoft` - A style complying with Microsofts style guide.
* - `GNU` - A style complying with the GNU coding standards.
* - A string starting with `{`, for example: `{BasedOnStyle: Chromium, IndentWidth: 4, ...}`.
* - A string which represents `.clang-format` content.
*
* @returns {string} The formatted content.
* @throws {Error}
*
* @see {@link https://clang.llvm.org/docs/ClangFormatStyleOptions.html}
*/
export declare function format_line_range(
content: string,
range: ByteRange[] | [[offset: number]],
filename?: Filename,
style?: Style,
): string;
/**
* @deprecated Use `format_line_range` instead.
*/
export declare function formatLineRange(
content: string,
range: ByteRange[] | [[offset: number]],
filename?: Filename,
style?: Style,
): string;
/**
* Formats the specified range of bytes in the given content using the specified style.
*
* @param {string} content - The content to format.
* @param {ByteRange[]} range - Array<[offset, length]> - The range of bytes to format.
* @param {Filename} filename - The filename to use for determining the language.
* @param {Style} style - The style to use for formatting.
* Supported style values are:
* - `LLVM` - A style complying with the LLVM coding standards.
* - `Google` - A style complying with Googles C++ style guide.
* - `Chromium` - A style complying with Chromiums style guide.
* - `Mozilla` - A style complying with Mozillas style guide.
* - `WebKit` - A style complying with WebKits style guide.
* - `Microsoft` - A style complying with Microsofts style guide.
* - `GNU` - A style complying with the GNU coding standards.
* - A string starting with `{`, for example: `{BasedOnStyle: Chromium, IndentWidth: 4, ...}`.
* - A string which represents `.clang-format` content.
*
* @returns {string} The formatted content.
* @throws {Error}
*
* @see {@link https://clang.llvm.org/docs/ClangFormatStyleOptions.html}
*/
export declare function format_byte_range(
content: string,
range: LineRange[],
filename?: Filename,
style?: Style,
): string;
/**
* @deprecated Use `format_byte_range` instead.
*/
export declare function formatByteRange(
content: string,
range: LineRange[],
filename?: Filename,
style?: Style,
): string;
export declare function version(): string;
export declare function set_fallback_style(style: Style): void;

View File

@@ -0,0 +1,748 @@
//===-- clang-format/ClangFormat.cpp - Clang format tool ------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file implements a clang-format tool that automatically formats
/// (fragments of) C++ code.
///
//===----------------------------------------------------------------------===//
#include "clang/../../lib/Format/MatchFilePath.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/DiagnosticOptions.h"
#include "clang/Basic/FileManager.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Basic/Version.h"
#include "clang/Format/Format.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "llvm/ADT/StringSwitch.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/Process.h"
#include <fstream>
#include "CustomFileSystem.h"
using namespace llvm;
using clang::tooling::Replacements;
static cl::opt<bool> Help("h", cl::desc("Alias for -help"), cl::Hidden);
// Mark all our options with this category, everything else (except for -version
// and -help) will be hidden.
static cl::OptionCategory ClangFormatCategory("Clang-format options");
static cl::list<unsigned>
Offsets("offset",
cl::desc("Format a range starting at this byte offset.\n"
"Multiple ranges can be formatted by specifying\n"
"several -offset and -length pairs.\n"
"Can only be used with one input file."),
cl::cat(ClangFormatCategory));
static cl::list<unsigned>
Lengths("length",
cl::desc("Format a range of this length (in bytes).\n"
"Multiple ranges can be formatted by specifying\n"
"several -offset and -length pairs.\n"
"When only a single -offset is specified without\n"
"-length, clang-format will format up to the end\n"
"of the file.\n"
"Can only be used with one input file."),
cl::cat(ClangFormatCategory));
static cl::list<std::string>
LineRanges("lines",
cl::desc("<start line>:<end line> - format a range of\n"
"lines (both 1-based).\n"
"Multiple ranges can be formatted by specifying\n"
"several -lines arguments.\n"
"Can't be used with -offset and -length.\n"
"Can only be used with one input file."),
cl::cat(ClangFormatCategory));
static cl::opt<std::string>
Style("style", cl::desc(clang::format::StyleOptionHelpDescription),
cl::init(clang::format::DefaultFormatStyle),
cl::cat(ClangFormatCategory));
static cl::opt<std::string>
FallbackStyle("fallback-style",
cl::desc("The name of the predefined style used as a\n"
"fallback in case clang-format is invoked with\n"
"-style=file, but can not find the .clang-format\n"
"file to use. Defaults to 'LLVM'.\n"
"Use -fallback-style=none to skip formatting."),
cl::init(clang::format::DefaultFallbackStyle),
cl::cat(ClangFormatCategory));
static cl::opt<std::string> AssumeFileName(
"assume-filename",
cl::desc("Set filename used to determine the language and to find\n"
".clang-format file.\n"
"Only used when reading from stdin.\n"
"If this is not passed, the .clang-format file is searched\n"
"relative to the current working directory when reading stdin.\n"
"Unrecognized filenames are treated as C++.\n"
"supported:\n"
" CSharp: .cs\n"
" Java: .java\n"
" JavaScript: .js .mjs .cjs .ts\n"
" Json: .json .ipynb\n"
" Objective-C: .m .mm\n"
" Proto: .proto .protodevel\n"
" TableGen: .td\n"
" TextProto: .txtpb .textpb .pb.txt .textproto .asciipb\n"
" Verilog: .sv .svh .v .vh"),
cl::init("<stdin>"), cl::cat(ClangFormatCategory));
static cl::opt<bool> Inplace("i",
cl::desc("Inplace edit <file>s, if specified."),
cl::cat(ClangFormatCategory));
static cl::opt<bool> OutputXML("output-replacements-xml",
cl::desc("Output replacements as XML."),
cl::cat(ClangFormatCategory));
static cl::opt<bool>
DumpConfig("dump-config",
cl::desc("Dump configuration options to stdout and exit.\n"
"Can be used with -style option."),
cl::cat(ClangFormatCategory));
static cl::opt<unsigned>
Cursor("cursor",
cl::desc("The position of the cursor when invoking\n"
"clang-format from an editor integration"),
cl::init(0), cl::cat(ClangFormatCategory));
static cl::opt<bool>
SortIncludes("sort-includes",
cl::desc("If set, overrides the include sorting behavior\n"
"determined by the SortIncludes style flag"),
cl::cat(ClangFormatCategory));
static cl::opt<std::string> QualifierAlignment(
"qualifier-alignment",
cl::desc("If set, overrides the qualifier alignment style\n"
"determined by the QualifierAlignment style flag"),
cl::init(""), cl::cat(ClangFormatCategory));
static cl::opt<std::string> Files(
"files",
cl::desc("A file containing a list of files to process, one per line."),
cl::value_desc("filename"), cl::init(""), cl::cat(ClangFormatCategory));
static cl::opt<bool>
Verbose("verbose", cl::desc("If set, shows the list of processed files"),
cl::cat(ClangFormatCategory));
// Use --dry-run to match other LLVM tools when you mean do it but don't
// actually do it
static cl::opt<bool>
DryRun("dry-run",
cl::desc("If set, do not actually make the formatting changes"),
cl::cat(ClangFormatCategory));
// Use -n as a common command as an alias for --dry-run. (git and make use -n)
static cl::alias DryRunShort("n", cl::desc("Alias for --dry-run"),
cl::cat(ClangFormatCategory), cl::aliasopt(DryRun),
cl::NotHidden);
// Emulate being able to turn on/off the warning.
static cl::opt<bool>
WarnFormat("Wclang-format-violations",
cl::desc("Warnings about individual formatting changes needed. "
"Used only with --dry-run or -n"),
cl::init(true), cl::cat(ClangFormatCategory), cl::Hidden);
static cl::opt<bool>
NoWarnFormat("Wno-clang-format-violations",
cl::desc("Do not warn about individual formatting changes "
"needed. Used only with --dry-run or -n"),
cl::init(false), cl::cat(ClangFormatCategory), cl::Hidden);
static cl::opt<unsigned> ErrorLimit(
"ferror-limit",
cl::desc("Set the maximum number of clang-format errors to emit\n"
"before stopping (0 = no limit).\n"
"Used only with --dry-run or -n"),
cl::init(0), cl::cat(ClangFormatCategory));
static cl::opt<bool>
WarningsAsErrors("Werror",
cl::desc("If set, changes formatting warnings to errors"),
cl::cat(ClangFormatCategory));
namespace {
enum class WNoError { Unknown };
}
static cl::bits<WNoError> WNoErrorList(
"Wno-error",
cl::desc("If set, don't error out on the specified warning type."),
cl::values(
clEnumValN(WNoError::Unknown, "unknown",
"If set, unknown format options are only warned about.\n"
"This can be used to enable formatting, even if the\n"
"configuration contains unknown (newer) options.\n"
"Use with caution, as this might lead to dramatically\n"
"differing format depending on an option being\n"
"supported or not.")),
cl::cat(ClangFormatCategory));
static cl::opt<bool>
ShowColors("fcolor-diagnostics",
cl::desc("If set, and on a color-capable terminal controls "
"whether or not to print diagnostics in color"),
cl::init(true), cl::cat(ClangFormatCategory), cl::Hidden);
static cl::opt<bool>
NoShowColors("fno-color-diagnostics",
cl::desc("If set, and on a color-capable terminal controls "
"whether or not to print diagnostics in color"),
cl::init(false), cl::cat(ClangFormatCategory), cl::Hidden);
static cl::list<std::string> FileNames(cl::Positional,
cl::desc("[@<file>] [<file> ...]"),
cl::cat(ClangFormatCategory));
static cl::opt<bool> FailOnIncompleteFormat(
"fail-on-incomplete-format",
cl::desc("If set, fail with exit code 1 on incomplete format."),
cl::init(false), cl::cat(ClangFormatCategory));
static cl::opt<bool> ListIgnored("list-ignored",
cl::desc("List ignored files."),
cl::cat(ClangFormatCategory), cl::Hidden);
namespace clang {
namespace format {
static FileID createInMemoryFile(StringRef FileName, MemoryBufferRef Source,
SourceManager &Sources, FileManager &Files,
llvm::vfs::InMemoryFileSystem *MemFS) {
MemFS->addFileNoOwn(FileName, 0, Source);
auto File = Files.getOptionalFileRef(FileName);
assert(File && "File not added to MemFS?");
return Sources.createFileID(*File, SourceLocation(), SrcMgr::C_User);
}
// Parses <start line>:<end line> input to a pair of line numbers.
// Returns true on error.
static bool parseLineRange(StringRef Input, unsigned &FromLine,
unsigned &ToLine) {
std::pair<StringRef, StringRef> LineRange = Input.split(':');
return LineRange.first.getAsInteger(0, FromLine) ||
LineRange.second.getAsInteger(0, ToLine);
}
static bool fillRanges(MemoryBuffer *Code,
std::vector<tooling::Range> &Ranges) {
IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem(
new llvm::vfs::InMemoryFileSystem);
FileManager Files(FileSystemOptions(), InMemoryFileSystem);
DiagnosticOptions DiagOpts;
DiagnosticsEngine Diagnostics(
IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), DiagOpts);
SourceManager Sources(Diagnostics, Files);
const auto ID = createInMemoryFile("<irrelevant>", *Code, Sources, Files,
InMemoryFileSystem.get());
if (!LineRanges.empty()) {
if (!Offsets.empty() || !Lengths.empty()) {
errs() << "error: cannot use -lines with -offset/-length\n";
return true;
}
for (const auto &LineRange : LineRanges) {
unsigned FromLine, ToLine;
if (parseLineRange(LineRange, FromLine, ToLine)) {
errs() << "error: invalid <start line>:<end line> pair\n";
return true;
}
if (FromLine < 1) {
errs() << "error: start line should be at least 1\n";
return true;
}
if (FromLine > ToLine) {
errs() << "error: start line should not exceed end line\n";
return true;
}
const auto Start = Sources.translateLineCol(ID, FromLine, 1);
const auto End = Sources.translateLineCol(ID, ToLine, UINT_MAX);
if (Start.isInvalid() || End.isInvalid())
return true;
const auto Offset = Sources.getFileOffset(Start);
const auto Length = Sources.getFileOffset(End) - Offset;
Ranges.push_back(tooling::Range(Offset, Length));
}
return false;
}
if (Offsets.empty())
Offsets.push_back(0);
const bool EmptyLengths = Lengths.empty();
unsigned Length = 0;
if (Offsets.size() == 1 && EmptyLengths) {
Length = Sources.getFileOffset(Sources.getLocForEndOfFile(ID)) - Offsets[0];
} else if (Offsets.size() != Lengths.size()) {
errs() << "error: number of -offset and -length arguments must match.\n";
return true;
}
for (unsigned I = 0, E = Offsets.size(), CodeSize = Code->getBufferSize();
I < E; ++I) {
const auto Offset = Offsets[I];
if (Offset >= CodeSize) {
errs() << "error: offset " << Offset << " is outside the file\n";
return true;
}
if (!EmptyLengths)
Length = Lengths[I];
if (Offset + Length > CodeSize) {
errs() << "error: invalid length " << Length << ", offset + length ("
<< Offset + Length << ") is outside the file.\n";
return true;
}
Ranges.push_back(tooling::Range(Offset, Length));
}
return false;
}
static void outputReplacementXML(StringRef Text) {
// FIXME: When we sort includes, we need to make sure the stream is correct
// utf-8.
size_t From = 0;
size_t Index;
while ((Index = Text.find_first_of("\n\r<&", From)) != StringRef::npos) {
outs() << Text.substr(From, Index - From);
switch (Text[Index]) {
case '\n':
outs() << "&#10;";
break;
case '\r':
outs() << "&#13;";
break;
case '<':
outs() << "&lt;";
break;
case '&':
outs() << "&amp;";
break;
default:
llvm_unreachable("Unexpected character encountered!");
}
From = Index + 1;
}
outs() << Text.substr(From);
}
static void outputReplacementsXML(const Replacements &Replaces) {
for (const auto &R : Replaces) {
outs() << "<replacement "
<< "offset='" << R.getOffset() << "' "
<< "length='" << R.getLength() << "'>";
outputReplacementXML(R.getReplacementText());
outs() << "</replacement>\n";
}
}
static bool
emitReplacementWarnings(const Replacements &Replaces, StringRef AssumedFileName,
const std::unique_ptr<llvm::MemoryBuffer> &Code) {
unsigned Errors = 0;
if (WarnFormat && !NoWarnFormat) {
SourceMgr Mgr;
const char *StartBuf = Code->getBufferStart();
Mgr.AddNewSourceBuffer(
MemoryBuffer::getMemBuffer(StartBuf, AssumedFileName), SMLoc());
for (const auto &R : Replaces) {
SMDiagnostic Diag = Mgr.GetMessage(
SMLoc::getFromPointer(StartBuf + R.getOffset()),
WarningsAsErrors ? SourceMgr::DiagKind::DK_Error
: SourceMgr::DiagKind::DK_Warning,
"code should be clang-formatted [-Wclang-format-violations]");
Diag.print(nullptr, llvm::errs(), ShowColors && !NoShowColors);
if (ErrorLimit && ++Errors >= ErrorLimit)
break;
}
}
return WarningsAsErrors;
}
static void outputXML(const Replacements &Replaces,
const Replacements &FormatChanges,
const FormattingAttemptStatus &Status,
const cl::opt<unsigned> &Cursor,
unsigned CursorPosition) {
outs() << "<?xml version='1.0'?>\n<replacements "
"xml:space='preserve' incomplete_format='"
<< (Status.FormatComplete ? "false" : "true") << "'";
if (!Status.FormatComplete)
outs() << " line='" << Status.Line << "'";
outs() << ">\n";
if (Cursor.getNumOccurrences() != 0) {
outs() << "<cursor>" << FormatChanges.getShiftedCodePosition(CursorPosition)
<< "</cursor>\n";
}
outputReplacementsXML(Replaces);
outs() << "</replacements>\n";
}
class ClangFormatDiagConsumer : public DiagnosticConsumer {
virtual void anchor() {}
void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
const Diagnostic &Info) override {
SmallVector<char, 16> vec;
Info.FormatDiagnostic(vec);
errs() << "clang-format error:" << vec << "\n";
}
};
// Returns true on error.
static bool format(StringRef FileName, bool ErrorOnIncompleteFormat = false) {
const bool IsSTDIN = FileName == "-";
if (!OutputXML && Inplace && IsSTDIN) {
errs() << "error: cannot use -i when reading from stdin.\n";
return true;
}
// On Windows, overwriting a file with an open file mapping doesn't work,
// so read the whole file into memory when formatting in-place.
ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
!OutputXML && Inplace
? MemoryBuffer::getFileAsStream(FileName)
: MemoryBuffer::getFileOrSTDIN(FileName, /*IsText=*/true);
if (std::error_code EC = CodeOrErr.getError()) {
errs() << FileName << ": " << EC.message() << "\n";
return true;
}
std::unique_ptr<llvm::MemoryBuffer> Code = std::move(CodeOrErr.get());
if (Code->getBufferSize() == 0)
return false; // Empty files are formatted correctly.
StringRef BufStr = Code->getBuffer();
const char *InvalidBOM = SrcMgr::ContentCache::getInvalidBOM(BufStr);
if (InvalidBOM) {
errs() << "error: encoding with unsupported byte order mark \""
<< InvalidBOM << "\" detected";
if (!IsSTDIN)
errs() << " in file '" << FileName << "'";
errs() << ".\n";
return true;
}
std::vector<tooling::Range> Ranges;
if (fillRanges(Code.get(), Ranges))
return true;
StringRef AssumedFileName = IsSTDIN ? AssumeFileName : FileName;
if (AssumedFileName.empty()) {
llvm::errs() << "error: empty filenames are not allowed\n";
return true;
}
auto RealFS = vfs::getRealFileSystem();
auto CustomFS = new vfs::CustomFileSystem(RealFS);
IntrusiveRefCntPtr<vfs::FileSystem> CustomFSPtr(CustomFS);
Expected<FormatStyle> FormatStyle =
getStyle(Style, AssumedFileName, FallbackStyle, Code->getBuffer(),
CustomFSPtr.get(), WNoErrorList.isSet(WNoError::Unknown));
if (!FormatStyle) {
llvm::errs() << toString(FormatStyle.takeError()) << "\n";
return true;
}
StringRef QualifierAlignmentOrder = QualifierAlignment;
FormatStyle->QualifierAlignment =
StringSwitch<FormatStyle::QualifierAlignmentStyle>(
QualifierAlignmentOrder.lower())
.Case("right", FormatStyle::QAS_Right)
.Case("left", FormatStyle::QAS_Left)
.Default(FormatStyle->QualifierAlignment);
if (FormatStyle->QualifierAlignment == FormatStyle::QAS_Left) {
FormatStyle->QualifierOrder = {"const", "volatile", "type"};
} else if (FormatStyle->QualifierAlignment == FormatStyle::QAS_Right) {
FormatStyle->QualifierOrder = {"type", "const", "volatile"};
} else if (QualifierAlignmentOrder.contains("type")) {
FormatStyle->QualifierAlignment = FormatStyle::QAS_Custom;
SmallVector<StringRef> Qualifiers;
QualifierAlignmentOrder.split(Qualifiers, " ", /*MaxSplit=*/-1,
/*KeepEmpty=*/false);
FormatStyle->QualifierOrder = {Qualifiers.begin(), Qualifiers.end()};
}
if (SortIncludes.getNumOccurrences() != 0) {
FormatStyle->SortIncludes = {};
if (SortIncludes)
FormatStyle->SortIncludes.Enabled = true;
}
unsigned CursorPosition = Cursor;
Replacements Replaces = sortIncludes(*FormatStyle, Code->getBuffer(), Ranges,
AssumedFileName, &CursorPosition);
const bool IsJson = FormatStyle->isJson();
// To format JSON insert a variable to trick the code into thinking its
// JavaScript.
if (IsJson && !FormatStyle->DisableFormat) {
auto Err =
Replaces.add(tooling::Replacement(AssumedFileName, 0, 0, "x = "));
if (Err)
llvm::errs() << "Bad Json variable insertion\n";
}
auto ChangedCode = tooling::applyAllReplacements(Code->getBuffer(), Replaces);
if (!ChangedCode) {
llvm::errs() << toString(ChangedCode.takeError()) << "\n";
return true;
}
// Get new affected ranges after sorting `#includes`.
Ranges = tooling::calculateRangesAfterReplacements(Replaces, Ranges);
FormattingAttemptStatus Status;
Replacements FormatChanges =
reformat(*FormatStyle, *ChangedCode, Ranges, AssumedFileName, &Status);
Replaces = Replaces.merge(FormatChanges);
if (DryRun) {
return Replaces.size() > (IsJson ? 1u : 0u) &&
emitReplacementWarnings(Replaces, AssumedFileName, Code);
}
if (OutputXML) {
outputXML(Replaces, FormatChanges, Status, Cursor, CursorPosition);
} else {
IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem(
new llvm::vfs::InMemoryFileSystem);
FileManager Files(FileSystemOptions(), InMemoryFileSystem);
DiagnosticOptions DiagOpts;
ClangFormatDiagConsumer IgnoreDiagnostics;
DiagnosticsEngine Diagnostics(
IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), DiagOpts,
&IgnoreDiagnostics, false);
SourceManager Sources(Diagnostics, Files);
FileID ID = createInMemoryFile(AssumedFileName, *Code, Sources, Files,
InMemoryFileSystem.get());
Rewriter Rewrite(Sources, LangOptions());
tooling::applyAllReplacements(Replaces, Rewrite);
if (Inplace) {
if (Rewrite.overwriteChangedFiles())
return true;
} else {
if (Cursor.getNumOccurrences() != 0) {
outs() << "{ \"Cursor\": "
<< FormatChanges.getShiftedCodePosition(CursorPosition)
<< ", \"IncompleteFormat\": "
<< (Status.FormatComplete ? "false" : "true");
if (!Status.FormatComplete)
outs() << ", \"Line\": " << Status.Line;
outs() << " }\n";
}
Rewrite.getEditBuffer(ID).write(outs());
}
}
return ErrorOnIncompleteFormat && !Status.FormatComplete;
}
} // namespace format
} // namespace clang
static void PrintVersion(raw_ostream &OS) {
OS << clang::getClangToolFullVersion("clang-format") << '\n';
}
// Dump the configuration.
static int dumpConfig() {
std::unique_ptr<llvm::MemoryBuffer> Code;
// We can't read the code to detect the language if there's no file name.
if (!FileNames.empty()) {
// Read in the code in case the filename alone isn't enough to detect the
// language.
ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
MemoryBuffer::getFileOrSTDIN(FileNames[0], /*IsText=*/true);
if (std::error_code EC = CodeOrErr.getError()) {
llvm::errs() << EC.message() << "\n";
return 1;
}
Code = std::move(CodeOrErr.get());
}
auto RealFS = vfs::getRealFileSystem();
auto CustomFS = new vfs::CustomFileSystem(RealFS);
IntrusiveRefCntPtr<vfs::FileSystem> CustomFSPtr(CustomFS);
Expected<clang::format::FormatStyle> FormatStyle = clang::format::getStyle(
Style,
FileNames.empty() || FileNames[0] == "-" ? AssumeFileName : FileNames[0],
FallbackStyle, Code ? Code->getBuffer() : "", CustomFSPtr.get());
if (!FormatStyle) {
llvm::errs() << toString(FormatStyle.takeError()) << "\n";
return 1;
}
std::string Config = clang::format::configurationAsText(*FormatStyle);
outs() << Config << "\n";
return 0;
}
using String = SmallString<128>;
static String IgnoreDir; // Directory of .clang-format-ignore file.
static String PrevDir; // Directory of previous `FilePath`.
static SmallVector<String> Patterns; // Patterns in .clang-format-ignore file.
// Check whether `FilePath` is ignored according to the nearest
// .clang-format-ignore file based on the rules below:
// - A blank line is skipped.
// - Leading and trailing spaces of a line are trimmed.
// - A line starting with a hash (`#`) is a comment.
// - A non-comment line is a single pattern.
// - The slash (`/`) is used as the directory separator.
// - A pattern is relative to the directory of the .clang-format-ignore file (or
// the root directory if the pattern starts with a slash).
// - A pattern is negated if it starts with a bang (`!`).
static bool isIgnored(StringRef FilePath) {
using namespace llvm::sys::fs;
if (!is_regular_file(FilePath))
return false;
String Path;
String AbsPath{FilePath};
auto PathStyle = vfs::getPathStyle();
using namespace llvm::sys::path;
vfs::make_absolute(AbsPath);
remove_dots(AbsPath, /*remove_dot_dot=*/true, PathStyle);
if (StringRef Dir{parent_path(AbsPath, PathStyle)}; PrevDir != Dir) {
PrevDir = Dir;
for (;;) {
Path = Dir;
append(Path, PathStyle, ".clang-format-ignore");
if (is_regular_file(Path))
break;
Dir = parent_path(Dir, PathStyle);
if (Dir.empty())
return false;
}
IgnoreDir = convert_to_slash(Dir, PathStyle);
std::ifstream IgnoreFile{Path.c_str()};
if (!IgnoreFile.good())
return false;
Patterns.clear();
for (std::string Line; std::getline(IgnoreFile, Line);) {
if (const auto Pattern{StringRef{Line}.trim()};
// Skip empty and comment lines.
!Pattern.empty() && Pattern[0] != '#') {
Patterns.push_back(Pattern);
}
}
}
if (IgnoreDir.empty())
return false;
const auto Pathname{convert_to_slash(AbsPath, PathStyle)};
for (const auto &Pat : Patterns) {
const bool IsNegated = Pat[0] == '!';
StringRef Pattern{Pat};
if (IsNegated)
Pattern = Pattern.drop_front();
if (Pattern.empty())
continue;
Pattern = Pattern.ltrim();
// `Pattern` is relative to `IgnoreDir` unless it starts with a slash.
// This doesn't support patterns containing drive names (e.g. `C:`).
if (Pattern[0] != '/') {
Path = IgnoreDir;
append(Path, Style::posix, Pattern);
remove_dots(Path, /*remove_dot_dot=*/true, Style::posix);
Pattern = Path;
}
if (clang::format::matchFilePath(Pattern, Pathname) == !IsNegated)
return true;
}
return false;
}
int main(int argc, const char **argv) {
InitLLVM X(argc, argv);
cl::HideUnrelatedOptions(ClangFormatCategory);
cl::SetVersionPrinter(PrintVersion);
cl::ParseCommandLineOptions(
argc, argv,
"A tool to format C/C++/Java/JavaScript/JSON/Objective-C/Protobuf/C# "
"code.\n\n"
"If no arguments are specified, it formats the code from standard input\n"
"and writes the result to the standard output.\n"
"If <file>s are given, it reformats the files. If -i is specified\n"
"together with <file>s, the files are edited in-place. Otherwise, the\n"
"result is written to the standard output.\n");
if (Help) {
cl::PrintHelpMessage();
return 0;
}
if (DumpConfig)
return dumpConfig();
if (!Files.empty()) {
std::ifstream ExternalFileOfFiles{std::string(Files)};
std::string Line;
unsigned LineNo = 1;
while (std::getline(ExternalFileOfFiles, Line)) {
FileNames.push_back(Line);
LineNo++;
}
errs() << "Clang-formatting " << LineNo << " files\n";
}
if (FileNames.empty()) {
if (isIgnored(AssumeFileName))
return 0;
return clang::format::format("-", FailOnIncompleteFormat);
}
if (FileNames.size() > 1 &&
(!Offsets.empty() || !Lengths.empty() || !LineRanges.empty())) {
errs() << "error: -offset, -length and -lines can only be used for "
"single file.\n";
return 1;
}
unsigned FileNo = 1;
bool Error = false;
for (const auto &FileName : FileNames) {
const bool Ignored = isIgnored(FileName);
if (ListIgnored) {
if (Ignored)
outs() << FileName << '\n';
continue;
}
if (Ignored)
continue;
if (Verbose) {
errs() << "Formatting [" << FileNo++ << "/" << FileNames.size() << "] "
<< FileName << "\n";
}
Error |= clang::format::format(FileName, FailOnIncompleteFormat);
}
return Error ? 1 : 0;
}

View File

@@ -0,0 +1,323 @@
//===-- clang-format/ClangFormat.cpp - Clang format tool ------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file implements a clang-format tool that automatically formats
/// (fragments of) C++ code.
///
//===----------------------------------------------------------------------===//
#include "lib.h"
#include "clang/Basic/FileManager.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Basic/Version.h"
#include "clang/Format/Format.h"
#include "clang/Rewrite/Core/Rewriter.h"
using namespace llvm;
using clang::tooling::Replacements;
static std::string FallbackStyle{clang::format::DefaultFallbackStyle};
static unsigned Cursor{0};
static bool SortIncludes{false};
static std::string QualifierAlignment{""};
static auto Ok(const std::string content) -> Result {
return {false, std::move(content)};
}
static auto Err(const std::string content) -> Result {
return {true, std::move(content)};
}
namespace clang {
namespace format {
static FileID createInMemoryFile(StringRef FileName, MemoryBufferRef Source,
SourceManager &Sources, FileManager &Files,
llvm::vfs::InMemoryFileSystem *MemFS) {
MemFS->addFileNoOwn(FileName, 0, Source);
auto File = Files.getOptionalFileRef(FileName);
assert(File && "File not added to MemFS?");
return Sources.createFileID(*File, SourceLocation(), SrcMgr::C_User);
}
static auto fillRanges(MemoryBuffer *Code, std::vector<tooling::Range> &Ranges)
-> void {
Ranges.push_back(tooling::Range(0, Code->getBuffer().size()));
}
static auto isPredefinedStyle(StringRef style) -> bool {
return StringSwitch<bool>(style.lower())
.Cases("llvm", "chromium", "mozilla", "google", "webkit", "gnu",
"microsoft", "none", "file", true)
.Default(false);
}
static auto format_range(const std::unique_ptr<llvm::MemoryBuffer> code,
const std::string assumedFileName,
const std::string style,
std::vector<tooling::Range> ranges) -> Result {
StringRef BufStr = code->getBuffer();
const char *InvalidBOM = SrcMgr::ContentCache::getInvalidBOM(BufStr);
if (InvalidBOM) {
std::stringstream err;
err << "encoding with unsupported byte order mark \"" << InvalidBOM
<< "\" detected.";
return Err(err.str());
}
StringRef AssumedFileName = assumedFileName;
if (AssumedFileName.empty())
AssumedFileName = "<stdin>";
IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem(
new llvm::vfs::InMemoryFileSystem);
FileManager Files(FileSystemOptions(), InMemoryFileSystem);
DiagnosticOptions DiagOpts;
DiagnosticsEngine Diagnostics(
IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), DiagOpts);
SourceManager Sources(Diagnostics, Files);
StringRef _style = style;
if (!_style.starts_with("{") && !isPredefinedStyle(_style)) {
std::unique_ptr<llvm::MemoryBuffer> DotClangFormat =
MemoryBuffer::getMemBuffer(style);
createInMemoryFile(".clang-format", *DotClangFormat.get(), Sources, Files,
InMemoryFileSystem.get());
_style = "file:.clang-format";
}
llvm::Expected<FormatStyle> FormatStyle =
getStyle(_style, AssumedFileName, FallbackStyle, code->getBuffer(),
InMemoryFileSystem.get(), false);
InMemoryFileSystem.reset();
if (!FormatStyle) {
std::string err = llvm::toString(FormatStyle.takeError());
return Err(err);
}
StringRef QualifierAlignmentOrder = QualifierAlignment;
FormatStyle->QualifierAlignment =
StringSwitch<FormatStyle::QualifierAlignmentStyle>(
QualifierAlignmentOrder.lower())
.Case("right", FormatStyle::QAS_Right)
.Case("left", FormatStyle::QAS_Left)
.Default(FormatStyle->QualifierAlignment);
if (FormatStyle->QualifierAlignment == FormatStyle::QAS_Left) {
FormatStyle->QualifierOrder = {"const", "volatile", "type"};
} else if (FormatStyle->QualifierAlignment == FormatStyle::QAS_Right) {
FormatStyle->QualifierOrder = {"type", "const", "volatile"};
} else if (QualifierAlignmentOrder.contains("type")) {
FormatStyle->QualifierAlignment = FormatStyle::QAS_Custom;
SmallVector<StringRef> Qualifiers;
QualifierAlignmentOrder.split(Qualifiers, " ", /*MaxSplit=*/-1,
/*KeepEmpty=*/false);
FormatStyle->QualifierOrder = {Qualifiers.begin(), Qualifiers.end()};
}
if (SortIncludes) {
FormatStyle->SortIncludes = {};
FormatStyle->SortIncludes.Enabled = true;
}
unsigned CursorPosition = Cursor;
Replacements Replaces = sortIncludes(*FormatStyle, code->getBuffer(), ranges,
AssumedFileName, &CursorPosition);
// To format JSON insert a variable to trick the code into thinking its
// JavaScript.
if (FormatStyle->isJson() && !FormatStyle->DisableFormat) {
auto err =
Replaces.add(tooling::Replacement(AssumedFileName, 0, 0, "x = "));
if (err)
return Err("Bad Json variable insertion");
}
auto ChangedCode =
cantFail(tooling::applyAllReplacements(code->getBuffer(), Replaces));
// Get new affected ranges after sorting `#includes`.
ranges = tooling::calculateRangesAfterReplacements(Replaces, ranges);
FormattingAttemptStatus Status;
Replacements FormatChanges =
reformat(*FormatStyle, ChangedCode, ranges, AssumedFileName, &Status);
Replaces = Replaces.merge(FormatChanges);
return Ok(
cantFail(tooling::applyAllReplacements(code->getBuffer(), Replaces)));
}
static auto format_range(const std::string str,
const std::string assumedFileName,
const std::string style, const bool is_line_range,
const std::vector<unsigned> ranges) -> Result {
ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
MemoryBuffer::getMemBuffer(str);
if (std::error_code EC = CodeOrErr.getError())
return Err(EC.message());
std::unique_ptr<llvm::MemoryBuffer> Code = std::move(CodeOrErr.get());
if (Code->getBufferSize() == 0)
return Ok(""); // Empty files are formatted correctly.
std::vector<tooling::Range> Ranges;
if (ranges.empty()) {
fillRanges(Code.get(), Ranges);
return format_range(std::move(Code), assumedFileName, style,
std::move(Ranges));
}
IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem(
new llvm::vfs::InMemoryFileSystem);
FileManager Files(FileSystemOptions(), InMemoryFileSystem);
DiagnosticOptions DiagOpts;
DiagnosticsEngine Diagnostics(
IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), DiagOpts);
SourceManager Sources(Diagnostics, Files);
FileID ID = createInMemoryFile("<irrelevant>", *Code, Sources, Files,
InMemoryFileSystem.get());
if (is_line_range) {
for (auto FromLine = begin(ranges); FromLine < end(ranges); FromLine += 2) {
auto ToLine = FromLine + 1;
SourceLocation Start = Sources.translateLineCol(ID, *FromLine, 1);
SourceLocation End = Sources.translateLineCol(ID, *ToLine, UINT_MAX);
if (Start.isInvalid() || End.isInvalid())
return Err("invalid line number");
unsigned Offset = Sources.getFileOffset(Start);
unsigned Length = Sources.getFileOffset(End) - Offset;
Ranges.push_back(tooling::Range(Offset, Length));
}
} else {
if (ranges.size() > 2 && ranges.size() % 2 != 0)
return Err("number of -offset and -length arguments must match");
if (ranges.size() == 1) {
auto offset = begin(ranges);
if (*offset >= Code->getBufferSize()) {
std::stringstream err;
err << "offset " << *offset << " is outside the file";
return Err(err.str());
}
SourceLocation Start =
Sources.getLocForStartOfFile(ID).getLocWithOffset(*offset);
SourceLocation End = Sources.getLocForEndOfFile(ID);
unsigned Offset = Sources.getFileOffset(Start);
unsigned Length = Sources.getFileOffset(End) - Offset;
Ranges.push_back(tooling::Range(Offset, Length));
} else {
for (auto offset = begin(ranges); offset < end(ranges); offset += 2) {
auto length = offset + 1;
if (*offset >= Code->getBufferSize()) {
std::stringstream err;
err << "offset " << *offset << " is outside the file";
return Err(err.str());
}
unsigned end = *offset + *length;
if (end > Code->getBufferSize()) {
std::stringstream err;
err << "invalid length " << *length << ", offset + length (" << end
<< ") is outside the file.";
return Err(err.str());
}
SourceLocation Start =
Sources.getLocForStartOfFile(ID).getLocWithOffset(*offset);
SourceLocation End = Start.getLocWithOffset(*length);
unsigned Offset = Sources.getFileOffset(Start);
unsigned Length = Sources.getFileOffset(End) - Offset;
Ranges.push_back(tooling::Range(Offset, Length));
}
}
}
return format_range(std::move(Code), assumedFileName, style,
std::move(Ranges));
}
static auto format(const std::string str, const std::string assumedFileName,
const std::string style) -> Result {
ErrorOr<std::unique_ptr<MemoryBuffer>> CodeOrErr =
MemoryBuffer::getMemBuffer(str);
if (std::error_code EC = CodeOrErr.getError())
return Err(EC.message());
std::unique_ptr<llvm::MemoryBuffer> Code = std::move(CodeOrErr.get());
if (Code->getBufferSize() == 0)
return Ok(""); // Empty files are formatted correctly.
std::vector<tooling::Range> Ranges;
fillRanges(Code.get(), Ranges);
return format_range(std::move(Code), assumedFileName, style,
std::move(Ranges));
}
} // namespace format
} // namespace clang
auto version() -> std::string {
return clang::getClangToolFullVersion("clang-format");
}
auto format(const std::string str, const std::string assumedFileName,
const std::string style) -> Result {
return clang::format::format(str, assumedFileName, style);
}
auto format_byte(const std::string str, const std::string assumedFileName,
const std::string style, const std::vector<unsigned> ranges)
-> Result {
return clang::format::format_range(str, assumedFileName, style, false,
std::move(ranges));
}
auto format_line(const std::string str, const std::string assumedFileName,
const std::string style, const std::vector<unsigned> ranges)
-> Result {
return clang::format::format_range(str, assumedFileName, style, true,
std::move(ranges));
}
auto set_fallback_style(const std::string style) -> void {
FallbackStyle = style;
}
auto set_sort_includes(const bool sort) -> void { SortIncludes = sort; }
auto dump_config(const std::string style, const std::string FileName,
const std::string code) -> Result {
llvm::Expected<clang::format::FormatStyle> FormatStyle =
clang::format::getStyle(style, FileName, FallbackStyle, code);
if (!FormatStyle)
return Err(llvm::toString(FormatStyle.takeError()));
std::string Config = clang::format::configurationAsText(*FormatStyle);
return Ok(Config);
}

View File

@@ -0,0 +1,24 @@
#ifndef CLANG_FORMAT_WASM_LIB_H_
#define CLANG_FORMAT_WASM_LIB_H_
#include <sstream>
struct Result {
bool error;
std::string content;
};
auto version() -> std::string;
auto format(const std::string str, const std::string assumedFileName, const std::string style) -> Result;
auto format_byte(const std::string str,
const std::string assumedFileName,
const std::string style,
const std::vector<unsigned> ranges) -> Result;
auto format_line(const std::string str,
const std::string assumedFileName,
const std::string style,
const std::vector<unsigned> ranges) -> Result;
auto set_fallback_style(const std::string style) -> void;
auto set_sort_includes(const bool sort) -> void;
auto dump_config(const std::string style, const std::string FileName, const std::string code) -> Result;
#endif

View File

@@ -0,0 +1,42 @@
set -Eeo pipefail
cd $(dirname $0)/..
project_root=$(pwd)
rm -rf pkg
mkdir -p pkg build
cd build
export CC=$(which clang)
export CXX=$(which clang++)
emcmake cmake -G Ninja ..
ninja clang-format-wasm
cd $project_root
if [[ ! -z "${WASM_OPT}" ]]; then
wasm-opt -Os build/clang-format-esm.wasm -o build/clang-format-esm-Os.wasm
wasm-opt -Oz build/clang-format-esm.wasm -o build/clang-format-esm-Oz.wasm
fi
SMALLEST_WASM=$(ls -Sr build/clang-format-e*.wasm | head -1)
cp $SMALLEST_WASM pkg/clang-format.wasm
cat src/template.js build/clang-format-esm.js >pkg/clang-format.js
# add shebang
echo '#!/usr/bin/env node' | cat - ./build/clang-format-cli.js >./pkg/clang-format-cli.cjs
cp ./build/clang-format-cli.wasm ./pkg/
cp ./src/clang-format.d.ts src/clang-format-*.js ./pkg/
cp ./package.json LICENSE README.md .npmignore ./pkg/
# copy git-clang-format and clang-format-diff.py
cp ./build/_deps/llvm_project-src/clang/tools/clang-format/git-clang-format ./pkg/
cp ./build/_deps/llvm_project-src/clang/tools/clang-format/clang-format-diff.py ./pkg/
ls -lh ./pkg
# make sure repo is clean
# git diff --exit-code

View File

@@ -0,0 +1,95 @@
diff --git a/src/cli.cc b/src/cli.cc
index 2861005..69ec009 100644
--- a/src/cli.cc
+++ b/src/cli.cc
@@ -12,7 +12,7 @@
///
//===----------------------------------------------------------------------===//
-#include "../../lib/Format/MatchFilePath.h"
+#include "clang/../../lib/Format/MatchFilePath.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/DiagnosticOptions.h"
#include "clang/Basic/FileManager.h"
@@ -27,6 +27,8 @@
#include "llvm/Support/Process.h"
#include <fstream>
+#include "CustomFileSystem.h"
+
using namespace llvm;
using clang::tooling::Replacements;
@@ -448,9 +450,12 @@ static bool format(StringRef FileName, bool ErrorOnIncompleteFormat = false) {
return true;
}
+ auto RealFS = vfs::getRealFileSystem();
+ auto CustomFS = new vfs::CustomFileSystem(RealFS);
+ IntrusiveRefCntPtr<vfs::FileSystem> CustomFSPtr(CustomFS);
Expected<FormatStyle> FormatStyle =
getStyle(Style, AssumedFileName, FallbackStyle, Code->getBuffer(),
- nullptr, WNoErrorList.isSet(WNoError::Unknown));
+ CustomFSPtr.get(), WNoErrorList.isSet(WNoError::Unknown));
if (!FormatStyle) {
llvm::errs() << toString(FormatStyle.takeError()) << "\n";
return true;
@@ -571,10 +576,15 @@ static int dumpConfig() {
}
Code = std::move(CodeOrErr.get());
}
+
+ auto RealFS = vfs::getRealFileSystem();
+ auto CustomFS = new vfs::CustomFileSystem(RealFS);
+ IntrusiveRefCntPtr<vfs::FileSystem> CustomFSPtr(CustomFS);
+
Expected<clang::format::FormatStyle> FormatStyle = clang::format::getStyle(
Style,
FileNames.empty() || FileNames[0] == "-" ? AssumeFileName : FileNames[0],
- FallbackStyle, Code ? Code->getBuffer() : "");
+ FallbackStyle, Code ? Code->getBuffer() : "", CustomFSPtr.get());
if (!FormatStyle) {
llvm::errs() << toString(FormatStyle.takeError()) << "\n";
return 1;
@@ -607,24 +617,26 @@ static bool isIgnored(StringRef FilePath) {
String Path;
String AbsPath{FilePath};
+ auto PathStyle = vfs::getPathStyle();
+
using namespace llvm::sys::path;
- make_absolute(AbsPath);
- remove_dots(AbsPath, /*remove_dot_dot=*/true);
+ vfs::make_absolute(AbsPath);
+ remove_dots(AbsPath, /*remove_dot_dot=*/true, PathStyle);
- if (StringRef Dir{parent_path(AbsPath)}; PrevDir != Dir) {
+ if (StringRef Dir{parent_path(AbsPath, PathStyle)}; PrevDir != Dir) {
PrevDir = Dir;
for (;;) {
Path = Dir;
- append(Path, ".clang-format-ignore");
+ append(Path, PathStyle, ".clang-format-ignore");
if (is_regular_file(Path))
break;
- Dir = parent_path(Dir);
+ Dir = parent_path(Dir, PathStyle);
if (Dir.empty())
return false;
}
- IgnoreDir = convert_to_slash(Dir);
+ IgnoreDir = convert_to_slash(Dir, PathStyle);
std::ifstream IgnoreFile{Path.c_str()};
if (!IgnoreFile.good())
@@ -644,7 +656,7 @@ static bool isIgnored(StringRef FilePath) {
if (IgnoreDir.empty())
return false;
- const auto Pathname{convert_to_slash(AbsPath)};
+ const auto Pathname{convert_to_slash(AbsPath, PathStyle)};
for (const auto &Pat : Patterns) {
const bool IsNegated = Pat[0] == '!';
StringRef Pattern{Pat};

View File

@@ -0,0 +1,26 @@
current_dir=$(pwd)
tmp_dir=$(mktemp -d)
cd $tmp_dir
git init
cp $current_dir/build/_deps/llvm_project-src/clang/tools/clang-format/ClangFormat.cpp ./cli.cc
git add -f .
git commit -m "init"
cp $current_dir/src/cli.cc ./cli.cc
git add -f .
git diff \
--cached \
--no-color \
--ignore-space-at-eol \
--no-ext-diff \
--src-prefix=a/src/ \
--dst-prefix=b/src/ \
>$current_dir/scripts/cli.patch || true
rm -rf $tmp_dir

View File

@@ -0,0 +1,146 @@
/* @ts-self-types="./clang-format.d.ts" */
async function load(module) {
if (typeof Response === "function" && module instanceof Response) {
if ("compileStreaming" in WebAssembly) {
try {
return await WebAssembly.compileStreaming(module);
} catch (e) {
if (module.headers.get("Content-Type") !== "application/wasm") {
console.warn(
"`WebAssembly.compileStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",
e,
);
} else {
throw e;
}
}
}
return module.arrayBuffer();
}
return module;
}
let wasm;
export default async function initAsync(input) {
if (wasm !== undefined) {
return wasm;
}
if (typeof input === "undefined") {
input = new URL("clang-format.wasm", import.meta.url);
}
if (
typeof input === "string" ||
(typeof Request === "function" && input instanceof Request) ||
(typeof URL === "function" && input instanceof URL)
) {
input = fetch(input);
}
wasm = await load(await input).then((wasm) => Module({ wasm }));
assert_init = () => {};
}
function assert_init() {
throw new Error("uninit");
}
export function version() {
assert_init();
return wasm.version();
}
export function set_fallback_style(style) {
assert_init();
wasm.set_fallback_style(style);
}
export function set_sort_includes(sort) {
assert_init();
wasm.set_sort_includes(sort);
}
function unwrap(result) {
const { error, content } = result;
if (error) {
throw Error(content);
}
return content;
}
export function format(content, filename = "<stdin>", style = "LLVM") {
assert_init();
const result = wasm.format(content, filename, style);
return unwrap(result);
}
export function format_line_range(
content,
range,
filename = "<stdin>",
style = "LLVM",
) {
assert_init();
const rangeList = new wasm.RangeList();
for (const [fromLine, toLine] of range) {
if (fromLine < 1) {
throw Error("start line should be at least 1");
}
if (fromLine > toLine) {
throw Error("start line should not exceed end line");
}
rangeList.push_back(fromLine);
rangeList.push_back(toLine);
}
const result = wasm.format_line(content, filename, style, rangeList);
rangeList.delete();
return unwrap(result);
}
export function format_byte_range(
content,
range,
filename = "<stdin>",
style = "LLVM",
) {
assert_init();
const rangeList = new wasm.RangeList();
if (range.length === 1 && range[0].length === 1) {
rangeList.push_back(range[0][0]);
} else {
for (const [offset, length] of range) {
if (offset < 0) {
throw Error("start offset should be at least 0");
}
if (length < 0) {
throw Error("length should be at least 0");
}
rangeList.push_back(offset);
rangeList.push_back(length);
}
}
const result = wasm.format_byte(content, filename, style, rangeList);
rangeList.delete();
return unwrap(result);
}
export function dump_config({
style = "file",
filename = "<stdin>",
code = "",
} = {}) {
assert_init();
const result = wasm.dump_config(style, filename, code);
return unwrap(result);
}
export {
format_byte_range as formatByteRange,
format_line_range as formatLineRange,
};

View File

@@ -0,0 +1,32 @@
export function format(input: string, filename: string, config?: LayoutConfig): string;
interface LayoutConfig {
line_width?: number;
line_ending?: "lf" | "crlf";
language_version?: string;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export type InitOutput = unknown;
// export type SyncInitInput = BufferSource | WebAssembly.Module;
// /**
// * Instantiates the given `module`, which can either be bytes or
// * a precompiled `WebAssembly.Module`.
// *
// * @param {SyncInitInput} module
// *
// * @returns {InitOutput}
// */
// export function initSync(module: SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
*
* @returns {Promise<InitOutput>}
*/
export default function init(module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;

View File

@@ -0,0 +1,84 @@
import { format as dart_fmt, instantiate, invoke } from "./dart_fmt.mjs";
let wasm;
function get_imports() {}
function init_memory() {}
function normalize(module) {
if (!(module instanceof WebAssembly.Module)) {
return new WebAssembly.Module(module);
}
return module;
}
export default async function (input) {
if (wasm !== undefined) return wasm;
if (typeof input === "undefined") {
input = new URL("dart_fmt.wasm", import.meta.url);
}
const imports = get_imports();
if (
typeof input === "string" ||
(typeof Request === "function" && input instanceof Request) ||
(typeof URL === "function" && input instanceof URL)
) {
input = fetch(input);
}
init_memory(imports);
wasm = await load(await input)
.then(normalize)
.then(instantiate);
invoke(wasm);
return wasm;
}
async function load(module) {
if (typeof Response === "function" && module instanceof Response) {
if ("compileStreaming" in WebAssembly) {
try {
return await WebAssembly.compileStreaming(module);
} catch (e) {
if (module.headers.get("Content-Type") != "application/wasm") {
console.warn(
"`WebAssembly.compileStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",
e,
);
} else {
throw e;
}
}
}
return module.arrayBuffer();
}
return module;
}
export function format(source, filename = "stdin.dart", config = {}) {
const options = { lineEnding: "\n" };
if (config.line_width) {
options.pageWidth = config.line_width;
}
if (options.line_ending === "crlf") {
options.lineEnding = "\r\n";
}
if(options.language_version) {
options.languageVersion = options.language_version;
}
const result = dart_fmt(source, filename, JSON.stringify(options));
const err = result[0] === "x";
const output = result.slice(1);
if (err) {
throw new Error(output);
}
return output;
}

View File

@@ -0,0 +1,350 @@
// `modulePromise` is a promise to the `WebAssembly.module` object to be
// instantiated.
// `importObjectPromise` is a promise to an object that contains any additional
// imports needed by the module that aren't provided by the standard runtime.
// The fields on this object will be merged into the importObject with which
// the module will be instantiated.
// This function returns a promise to the instantiated module.
export const instantiate = async (modulePromise, importObjectPromise) => {
let dartInstance;
// Prints to the console
function printToConsole(value) {
if (typeof dartPrint == "function") {
dartPrint(value);
return;
}
if (typeof console == "object" && typeof console.log != "undefined") {
console.log(value);
return;
}
if (typeof print == "function") {
print(value);
return;
}
throw "Unable to print message: " + js;
}
// Converts a Dart List to a JS array. Any Dart objects will be converted, but
// this will be cheap for JSValues.
function arrayFromDartList(constructor, list) {
const exports = dartInstance.exports;
const read = exports.$listRead;
const length = exports.$listLength(list);
const array = new constructor(length);
for (let i = 0; i < length; i++) {
array[i] = read(list, i);
}
return array;
}
// A special symbol attached to functions that wrap Dart functions.
const jsWrappedDartFunctionSymbol = Symbol("JSWrappedDartFunction");
function finalizeWrapper(dartFunction, wrapped) {
wrapped.dartFunction = dartFunction;
wrapped[jsWrappedDartFunctionSymbol] = true;
return wrapped;
}
// Imports
const dart2wasm = {
_49: v => v.toString(),
_64: s => {
if (!/^\s*[+-]?(?:Infinity|NaN|(?:\.\d+|\d+(?:\.\d*)?)(?:[eE][+-]?\d+)?)\s*$/.test(s)) {
return NaN;
}
return parseFloat(s);
},
_65: () => {
let stackString = new Error().stack.toString();
let frames = stackString.split('\n');
let drop = 2;
if (frames[0] === 'Error') {
drop += 1;
}
return frames.slice(drop).join('\n');
},
_69: () => {
// On browsers return `globalThis.location.href`
if (globalThis.location != null) {
return globalThis.location.href;
}
return null;
},
_70: () => {
return typeof process != "undefined" &&
Object.prototype.toString.call(process) == "[object process]" &&
process.platform == "win32"
},
_85: s => JSON.stringify(s),
_86: s => printToConsole(s),
_87: a => a.join(''),
_90: (s, t) => s.split(t),
_91: s => s.toLowerCase(),
_92: s => s.toUpperCase(),
_93: s => s.trim(),
_97: (s, p, i) => s.indexOf(p, i),
_98: (s, p, i) => s.lastIndexOf(p, i),
_100: Object.is,
_101: s => s.toUpperCase(),
_102: s => s.toLowerCase(),
_103: (a, i) => a.push(i),
_113: (a, b) => a == b ? 0 : (a > b ? 1 : -1),
_114: a => a.length,
_116: (a, i) => a[i],
_117: (a, i, v) => a[i] = v,
_120: (o, start, length) => new Uint8Array(o.buffer, o.byteOffset + start, length),
_121: (o, start, length) => new Int8Array(o.buffer, o.byteOffset + start, length),
_122: (o, start, length) => new Uint8ClampedArray(o.buffer, o.byteOffset + start, length),
_123: (o, start, length) => new Uint16Array(o.buffer, o.byteOffset + start, length),
_124: (o, start, length) => new Int16Array(o.buffer, o.byteOffset + start, length),
_125: (o, start, length) => new Uint32Array(o.buffer, o.byteOffset + start, length),
_126: (o, start, length) => new Int32Array(o.buffer, o.byteOffset + start, length),
_129: (o, start, length) => new Float32Array(o.buffer, o.byteOffset + start, length),
_130: (o, start, length) => new Float64Array(o.buffer, o.byteOffset + start, length),
_133: (o) => new DataView(o.buffer, o.byteOffset, o.byteLength),
_137: Function.prototype.call.bind(Object.getOwnPropertyDescriptor(DataView.prototype, 'byteLength').get),
_138: (b, o) => new DataView(b, o),
_140: Function.prototype.call.bind(DataView.prototype.getUint8),
_141: Function.prototype.call.bind(DataView.prototype.setUint8),
_142: Function.prototype.call.bind(DataView.prototype.getInt8),
_143: Function.prototype.call.bind(DataView.prototype.setInt8),
_144: Function.prototype.call.bind(DataView.prototype.getUint16),
_145: Function.prototype.call.bind(DataView.prototype.setUint16),
_146: Function.prototype.call.bind(DataView.prototype.getInt16),
_147: Function.prototype.call.bind(DataView.prototype.setInt16),
_148: Function.prototype.call.bind(DataView.prototype.getUint32),
_149: Function.prototype.call.bind(DataView.prototype.setUint32),
_150: Function.prototype.call.bind(DataView.prototype.getInt32),
_151: Function.prototype.call.bind(DataView.prototype.setInt32),
_156: Function.prototype.call.bind(DataView.prototype.getFloat32),
_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) }),
_184: (c) =>
queueMicrotask(() => dartInstance.exports.$invokeCallback(c)),
_187: (s, m) => {
try {
return new RegExp(s, m);
} catch (e) {
return String(e);
}
},
_188: (x0,x1) => x0.exec(x1),
_189: (x0,x1) => x0.test(x1),
_190: (x0,x1) => x0.exec(x1),
_191: (x0,x1) => x0.exec(x1),
_192: x0 => x0.pop(),
_198: o => o === undefined,
_199: o => typeof o === 'boolean',
_200: o => typeof o === 'number',
_202: o => typeof o === 'string',
_205: o => o instanceof Int8Array,
_206: o => o instanceof Uint8Array,
_207: o => o instanceof Uint8ClampedArray,
_208: o => o instanceof Int16Array,
_209: o => o instanceof Uint16Array,
_210: o => o instanceof Int32Array,
_211: o => o instanceof Uint32Array,
_212: o => o instanceof Float32Array,
_213: o => o instanceof Float64Array,
_214: o => o instanceof ArrayBuffer,
_215: o => o instanceof DataView,
_216: o => o instanceof Array,
_217: o => typeof o === 'function' && o[jsWrappedDartFunctionSymbol] === true,
_220: o => o instanceof RegExp,
_221: (l, r) => l === r,
_222: o => o,
_223: o => o,
_224: o => o,
_225: b => !!b,
_226: o => o.length,
_229: (o, i) => o[i],
_230: f => f.dartFunction,
_231: l => arrayFromDartList(Int8Array, l),
_232: (data, length) => {
const jsBytes = new Uint8Array(length);
const getByte = dartInstance.exports.$uint8ListGet;
for (let i = 0; i < length; i++) {
jsBytes[i] = getByte(data, i);
}
return jsBytes;
},
_233: l => arrayFromDartList(Uint8ClampedArray, l),
_234: l => arrayFromDartList(Int16Array, l),
_235: l => arrayFromDartList(Uint16Array, l),
_236: l => arrayFromDartList(Int32Array, l),
_237: l => arrayFromDartList(Uint32Array, l),
_238: l => arrayFromDartList(Float32Array, l),
_239: l => arrayFromDartList(Float64Array, l),
_240: (data, length) => {
const read = dartInstance.exports.$byteDataGetUint8;
const view = new DataView(new ArrayBuffer(length));
for (let i = 0; i < length; i++) {
view.setUint8(i, read(data, i));
}
return view;
},
_241: l => arrayFromDartList(Array, l),
_242: (s, length) => {
if (length == 0) return '';
const read = dartInstance.exports.$stringRead1;
let result = '';
let index = 0;
const chunkLength = Math.min(length - index, 500);
let array = new Array(chunkLength);
while (index < length) {
const newChunkLength = Math.min(length - index, 500);
for (let i = 0; i < newChunkLength; i++) {
array[i] = read(s, index++);
}
if (newChunkLength < chunkLength) {
array = array.slice(0, newChunkLength);
}
result += String.fromCharCode(...array);
}
return result;
}
,
_243: (s, length) => {
if (length == 0) return '';
const read = dartInstance.exports.$stringRead2;
let result = '';
let index = 0;
const chunkLength = Math.min(length - index, 500);
let array = new Array(chunkLength);
while (index < length) {
const newChunkLength = Math.min(length - index, 500);
for (let i = 0; i < newChunkLength; i++) {
array[i] = read(s, index++);
}
if (newChunkLength < chunkLength) {
array = array.slice(0, newChunkLength);
}
result += String.fromCharCode(...array);
}
return result;
}
,
_244: (s) => {
let length = s.length;
let range = 0;
for (let i = 0; i < length; i++) {
range |= s.codePointAt(i);
}
const exports = dartInstance.exports;
if (range < 256) {
if (length <= 10) {
if (length == 1) {
return exports.$stringAllocate1_1(s.codePointAt(0));
}
if (length == 2) {
return exports.$stringAllocate1_2(s.codePointAt(0), s.codePointAt(1));
}
if (length == 3) {
return exports.$stringAllocate1_3(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2));
}
if (length == 4) {
return exports.$stringAllocate1_4(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3));
}
if (length == 5) {
return exports.$stringAllocate1_5(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4));
}
if (length == 6) {
return exports.$stringAllocate1_6(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5));
}
if (length == 7) {
return exports.$stringAllocate1_7(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6));
}
if (length == 8) {
return exports.$stringAllocate1_8(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7));
}
if (length == 9) {
return exports.$stringAllocate1_9(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7), s.codePointAt(8));
}
if (length == 10) {
return exports.$stringAllocate1_10(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7), s.codePointAt(8), s.codePointAt(9));
}
}
const dartString = exports.$stringAllocate1(length);
const write = exports.$stringWrite1;
for (let i = 0; i < length; i++) {
write(dartString, i, s.codePointAt(i));
}
return dartString;
} else {
const dartString = exports.$stringAllocate2(length);
const write = exports.$stringWrite2;
for (let i = 0; i < length; i++) {
write(dartString, i, s.charCodeAt(i));
}
return dartString;
}
}
,
_247: l => new Array(l),
_251: (o, p) => o[p],
_255: o => String(o),
_260: x0 => x0.index,
_262: x0 => x0.length,
_264: (x0,x1) => x0[x1],
_265: (x0,x1) => x0.exec(x1),
_267: x0 => x0.flags,
_268: x0 => x0.multiline,
_269: x0 => x0.ignoreCase,
_270: x0 => x0.unicode,
_271: x0 => x0.dotAll,
_272: (x0,x1) => x0.lastIndex = x1
};
const baseImports = {
dart2wasm: dart2wasm,
Math: Math,
Date: Date,
Object: Object,
Array: Array,
Reflect: Reflect,
};
const jsStringPolyfill = {
"charCodeAt": (s, i) => s.charCodeAt(i),
"compare": (s1, s2) => {
if (s1 < s2) return -1;
if (s1 > s2) return 1;
return 0;
},
"concat": (s1, s2) => s1 + s2,
"equals": (s1, s2) => s1 === s2,
"fromCharCode": (i) => String.fromCharCode(i),
"length": (s) => s?.length||0,
"substring": (s, a, b) => s.substring(a, b),
};
dartInstance = await WebAssembly.instantiate(await modulePromise, {
...baseImports,
...(await importObjectPromise),
"wasm:js-string": jsStringPolyfill,
});
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;

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More