mirror of
https://github.com/purocean/yn.git
synced 2026-09-24 15:46:02 +08:00
Merge branch 'purocean:develop' into develop
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
@@ -88,13 +88,20 @@ For more information on how to use the following functions, please see [characte
|
||||
|
||||
[Latest release](https://github.com/purocean/yn/releases)
|
||||
|
||||
### [v3.11.6](https://github.com/purocean/yn/releases/tag/v3.11.6) 2021-11-17
|
||||
1. perf: optimize rendering large file performance
|
||||
2. feat: add export button for echarts and mermaid
|
||||
### [v3.11.7](https://github.com/purocean/yn/releases/tag/v3.11.7) 2021-11-19
|
||||
1. feat: add group container
|
||||
2. feat(macro): add $export method
|
||||
3. feat: add "New Folder" context menu
|
||||
4. feat: add insert menu
|
||||
5. upd: update shortcuts
|
||||
|
||||
<details>
|
||||
<summary>Expand to see more version records</summary>
|
||||
|
||||
### [v3.11.6](https://github.com/purocean/yn/releases/tag/v3.11.6) 2021-11-17
|
||||
1. perf: optimize rendering large file performance
|
||||
2. feat: add export button for echarts and mermaid
|
||||
|
||||
### [v3.11.5](https://github.com/purocean/yn/releases/tag/v3.11.5) 2021-11-15
|
||||
1. upd(mindmap): optimize the user experience
|
||||
2. fix(main): fix startup crash when config.json is not exist
|
||||
|
||||
+21
-2
@@ -377,7 +377,7 @@ xml 代码块 第一行注释需要有 `--drawio--` 文字
|
||||
|
||||
`type` 是必需的, `title` 和 `content` 是可选的。
|
||||
|
||||
支持的 `type` 有:`tip` `warning` `danger` `details`
|
||||
支持的 `type` 有:`tip` `warning` `danger` `details` `group` `group-item`
|
||||
|
||||
**示例**
|
||||
|
||||
@@ -405,6 +405,23 @@ xml 代码块 第一行注释需要有 `--drawio--` 文字
|
||||
这是一个 details 标签
|
||||
:::
|
||||
|
||||
:::: group 这是标签组
|
||||
|
||||
::: group-item Tab 1
|
||||
test 1
|
||||
:::
|
||||
|
||||
::: group-item *Tab 2
|
||||
test 2
|
||||
标题前带 * 表示默认激活此选项卡
|
||||
:::
|
||||
|
||||
::: group-item Tab 3
|
||||
test 3
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
## 元素属性书写
|
||||
|
||||
此功能使用 [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs) 实现
|
||||
@@ -450,7 +467,8 @@ Yank Note 运行你在页面中嵌入宏,用以动态的替换文档。
|
||||
### 一些示例
|
||||
|
||||
- 是否开启页面标题序号编号: <= headingNumber =>
|
||||
- 自定义变量: <= customVar =>
|
||||
- 定义变量: <= $export('testVar', 'Test') =><= testVar =>
|
||||
- 使用变量: <= customVar =>
|
||||
- 当前文档名: <= $doc.basename =>
|
||||
- 当前时间: <= $ctx.lib.dayjs().format('YYYY-MM-DD HH:mm') =>
|
||||
- 限定符转义: <= (1 <\= 3) ? 'result =\> true' : 'result =\> false' =>
|
||||
@@ -488,6 +506,7 @@ Yank Note 运行你在页面中嵌入宏,用以动态的替换文档。
|
||||
---- | ----- | ---
|
||||
`$ctx` | object | 编辑器 `ctx`,可参考[插件开发指南](PLUGIN.md) 和[Api 文档](https://yn-api-doc.vercel.app/modules/context.html)
|
||||
`$include` | (path: string, trim = false) => Result | 引入其他文档片段方法
|
||||
`$include` | (key: string, val: any) => Result | 定义一个本文档可以使用的变量
|
||||
`$doc` | object | 当前文档信息
|
||||
`$doc.basename` | string | 当前文档文件名(无后缀)
|
||||
`$doc.name` | string | 当前文档文件名
|
||||
|
||||
+5
-9
@@ -13,25 +13,22 @@
|
||||
为下一个匹配项添加光标 | Ctrl + D | Cmd + D
|
||||
查找 | Ctrl + F | Cmd + F
|
||||
打开文件快速跳转面板 | Ctrl + P | Cmd + P
|
||||
插入文档链接 | Ctrl + Alt + I | Cmd + Option + I
|
||||
保存文档 | Ctrl + S | Cmd + S
|
||||
连接行 | Ctrl + J | Cmd + J
|
||||
连接行 | Ctrl + J | Ctrl + J
|
||||
强制插入新行,忽略预置补全规则 | Alt + Enter | Option + Enter
|
||||
强制插入 Tab,忽略预置补全规则 | Shift + Enter | Shift + Enter
|
||||
当前行上移 | Ctrl + Shift + Up | Cmd + Shift + Up
|
||||
当前行下移 | Ctrl + Shift + Down | Cmd + Shift + Down
|
||||
重复当前行 | Ctrl + Shift + D | Cmd + Shift + D
|
||||
插入当前日期 | Shift + Alt + D | Shift + Option + D
|
||||
切换待办状态 | Alt + O | Alt + O
|
||||
插入当前日期 | Shift + Alt + D | Shift + Option + D
|
||||
插入当前时间 | Shift + Alt + T | Shift + Option + T
|
||||
插入文件附件 | Shift + Alt + F | Shift + Option + F
|
||||
插入文件附件 | Alt + F | Option + F
|
||||
插入图片 | Alt + I | Option + I
|
||||
插入文档链接 | Alt + D | Option + D
|
||||
在内置终端里面运行选中内容 | Shift + Alt + R | Shift + Option + R
|
||||
转换大写 | Ctrl + K, Ctrl + U | Cmd + K, Cmd + U
|
||||
转换小写 | Ctrl + K, Ctrl + L | Cmd + K, Cmd + K
|
||||
粘贴 HTML 富文本 | Ctrl + D + V | Cmd + D + V
|
||||
粘贴图片为 base64 链接 | Ctrl + B + V | Cmd + B + V
|
||||
转换单个外链图片到本地 | Ctrl + Shift + 单击图片 | Cmd + Shift + 单击图片
|
||||
转换所有外链图片到本地 | Ctrl + Shift + L | Cmd + Shift + L
|
||||
编辑表格单元格 | 双击单元格 | 双击单元格
|
||||
编辑表格单元格(弹出框) | Ctrl + 单击单元格 | Cmd + 单击单元格 或
|
||||
复制文档标题链接 | Ctrl + 单击标题 | Cmd + 单击标题
|
||||
@@ -41,4 +38,3 @@
|
||||
切换文档预览显示 | Alt + V | Option + V
|
||||
切换终端 | Alt + T | Option + T
|
||||
演示模式 | F5 | F5
|
||||
查看 Readme | Alt + H | Option + H
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yank.note",
|
||||
"version": "3.11.6",
|
||||
"version": "3.11.7",
|
||||
"description": "Yank Note 一款面向程序员的 Markdown 笔记应用",
|
||||
"main": "dist/main/app.js",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
+117
-88
@@ -26,7 +26,7 @@ interface TreeItem extends XFile {
|
||||
children?: XFile[];
|
||||
}
|
||||
|
||||
const withRepo = (repo = 'main', callback: (repoPath: string, ...targetPath: string[]) => any, ...target: string[]) => {
|
||||
function withRepo<T> (repo = 'main', callback: (repoPath: string, ...targetPath: string[]) => T, ...target: string[]): T {
|
||||
const repoPath = repository.getPath(repo)
|
||||
if (!repoPath) {
|
||||
throw new Error(`repo ${repo} not exists.`)
|
||||
@@ -43,110 +43,143 @@ const withRepo = (repo = 'main', callback: (repoPath: string, ...targetPath: str
|
||||
}))
|
||||
}
|
||||
|
||||
const read = (repo: string, p: string) => withRepo(repo, (_, targetPath) => fs.readFileSync(targetPath), p)
|
||||
export function read (repo: string, p: string): Promise<Buffer> {
|
||||
return withRepo(repo, (_, targetPath) => fs.readFile(targetPath), p)
|
||||
}
|
||||
|
||||
const write = (repo: string, p: string, content: any) => {
|
||||
export function write (repo: string, p: string, content: any): Promise<string> {
|
||||
if (readonly) throw new Error('Readonly')
|
||||
|
||||
return withRepo(repo, (_, filePath) => {
|
||||
fs.ensureFileSync(filePath)
|
||||
fs.writeFileSync(filePath, content)
|
||||
return withRepo(repo, async (_, filePath) => {
|
||||
// create dir.
|
||||
if (filePath.endsWith('/')) {
|
||||
await fs.ensureDir(filePath)
|
||||
return ''
|
||||
}
|
||||
|
||||
await fs.ensureFile(filePath)
|
||||
await fs.writeFile(filePath, content)
|
||||
|
||||
return crypto.createHash('md5').update(content).digest('hex')
|
||||
}, p)
|
||||
}
|
||||
|
||||
const rm = (repo: string, p: string) => {
|
||||
export async function rm (repo: string, p: string) {
|
||||
if (readonly) throw new Error('Readonly')
|
||||
|
||||
withRepo(repo, (repoPath, targetPath) => {
|
||||
await withRepo(repo, async (repoPath, targetPath) => {
|
||||
if (targetPath !== repoPath) {
|
||||
const newPath = path.join(repository.getTrashPath(repo), p.replace(/\.\./g, '')) + '.' + (new Date()).getTime()
|
||||
fs.moveSync(targetPath, newPath)
|
||||
await fs.move(targetPath, newPath)
|
||||
}
|
||||
}, p)
|
||||
}
|
||||
|
||||
const mv = (repo: string, oldPath: string, newPath: string) => {
|
||||
export async function mv (repo: string, oldPath: string, newPath: string) {
|
||||
if (readonly) throw new Error('Readonly')
|
||||
|
||||
withRepo(repo, (_, oldP, newP) => {
|
||||
await withRepo(repo, async (_, oldP, newP) => {
|
||||
if (oldPath !== newP) {
|
||||
fs.moveSync(oldP, newP)
|
||||
await fs.move(oldP, newP)
|
||||
}
|
||||
}, oldPath, newPath)
|
||||
}
|
||||
|
||||
const exists = (repo: string, p: string) => withRepo(repo, (_, targetPath) => fs.existsSync(targetPath), p)
|
||||
export function exists (repo: string, p: string) {
|
||||
return withRepo(repo, (_, targetPath) => fs.existsSync(targetPath), p)
|
||||
}
|
||||
|
||||
const hash = (repo: string, p: string) => {
|
||||
const content = read(repo, p)
|
||||
export async function hash (repo: string, p: string) {
|
||||
const content = await read(repo, p)
|
||||
return crypto.createHash('md5').update(content).digest('hex')
|
||||
}
|
||||
|
||||
const checkHash = (repo: string, p: string, oldHash: string) => {
|
||||
return oldHash === hash(repo, p)
|
||||
export async function checkHash (repo: string, p: string, oldHash: string) {
|
||||
return oldHash === await hash(repo, p)
|
||||
}
|
||||
|
||||
const upload = (repo: string, buffer: Buffer, path: string) => {
|
||||
export async function upload (repo: string, buffer: Buffer, path: string) {
|
||||
if (readonly) throw new Error('Readonly')
|
||||
write(repo, path, buffer)
|
||||
await write(repo, path, buffer)
|
||||
}
|
||||
|
||||
const getRelativePath = (from: string, to: string) =>
|
||||
'/' + path.relative(from, to).replace(/\\/g, '/')
|
||||
function getRelativePath (from: string, to: string) {
|
||||
return '/' + path.relative(from, to).replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
const travels = (location: string, repo: string, basePath: string, markedFiles: MarkedFile[] | null = null): any => {
|
||||
if (!fs.statSync(location).isDirectory()) {
|
||||
return []
|
||||
async function travels (
|
||||
location: string,
|
||||
repo: string,
|
||||
basePath: string,
|
||||
markedFiles: MarkedFile[],
|
||||
data: TreeItem
|
||||
): Promise<void> {
|
||||
if (!(await fs.stat(location)).isDirectory()) {
|
||||
return
|
||||
}
|
||||
|
||||
const list = fs.readdirSync(location).filter(x => !x.startsWith('.') && !ignorePath.test(x))
|
||||
const list = await fs.readdir(location)
|
||||
|
||||
const sortOptions = [(v: string) => v && v.charCodeAt(0) > 255 ? 1 : 0, (v: string) => v]
|
||||
const dirs: TreeItem[] = []
|
||||
const files: TreeItem[] = []
|
||||
|
||||
const dirs = NaturalOrderby.orderBy(list.filter(x => fs.statSync(path.join(location, x)).isDirectory()), sortOptions)
|
||||
const files = NaturalOrderby.orderBy(list.filter(x => !fs.statSync(path.join(location, x)).isDirectory()), sortOptions)
|
||||
await Promise.all(list.map(async x => {
|
||||
if (x.startsWith('.') || ignorePath.test(x)) {
|
||||
return
|
||||
}
|
||||
|
||||
markedFiles = markedFiles || mark.list()
|
||||
|
||||
return dirs.map(x => {
|
||||
const p = path.join(location, x)
|
||||
return {
|
||||
name: x,
|
||||
path: getRelativePath(basePath, p),
|
||||
type: 'dir',
|
||||
repo: repo,
|
||||
children: travels(p, repo, basePath, markedFiles)
|
||||
} as TreeItem
|
||||
}).concat(files.map(x => {
|
||||
const p = path.join(location, x)
|
||||
const xpath = getRelativePath(basePath, p)
|
||||
const stat = fs.statSync(p)
|
||||
const stat = await fs.stat(p)
|
||||
|
||||
return {
|
||||
name: x,
|
||||
path: xpath,
|
||||
type: 'file',
|
||||
repo: repo,
|
||||
marked: (markedFiles || []).findIndex(f => f.path === xpath && f.repo === repo) > -1,
|
||||
birthtime: stat.birthtimeMs,
|
||||
mtime: stat.mtimeMs
|
||||
} as TreeItem
|
||||
if (stat.isFile()) {
|
||||
const xpath = getRelativePath(basePath, p)
|
||||
|
||||
files.push({
|
||||
name: x,
|
||||
path: xpath,
|
||||
type: 'file',
|
||||
repo: repo,
|
||||
marked: (markedFiles || []).findIndex(f => f.path === xpath && f.repo === repo) > -1,
|
||||
birthtime: stat.birthtimeMs,
|
||||
mtime: stat.mtimeMs
|
||||
})
|
||||
} else if (stat.isDirectory()) {
|
||||
const dir: TreeItem = {
|
||||
name: x,
|
||||
path: getRelativePath(basePath, p),
|
||||
type: 'dir',
|
||||
repo: repo,
|
||||
children: []
|
||||
}
|
||||
|
||||
dirs.push(dir)
|
||||
await travels(p, repo, basePath, markedFiles, dir)
|
||||
}
|
||||
}))
|
||||
|
||||
const sortOptions = [(v: TreeItem) => v && v.name.charCodeAt(0) > 255 ? 1 : 0, (v: TreeItem) => v.name]
|
||||
|
||||
data.children = NaturalOrderby.orderBy(dirs, sortOptions)
|
||||
.concat(NaturalOrderby.orderBy(files, sortOptions))
|
||||
}
|
||||
|
||||
const tree = (repo: string) => {
|
||||
return withRepo(repo, repoPath => [{
|
||||
export async function tree (repo: string): Promise<TreeItem[]> {
|
||||
const data: TreeItem[] = [{
|
||||
name: '/',
|
||||
type: 'dir',
|
||||
path: '/',
|
||||
repo: repo,
|
||||
children: travels(repoPath, repo, repoPath)
|
||||
}])
|
||||
children: []
|
||||
}]
|
||||
|
||||
const markedFiles = mark.list()
|
||||
|
||||
await withRepo(repo, async repoPath => travels(repoPath, repo, repoPath, markedFiles, data[0]))
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const open = (repo: string, p: string) => {
|
||||
export function open (repo: string, p: string) {
|
||||
withRepo(repo, (_, targetPath) => {
|
||||
if (isWsl) {
|
||||
targetPath = wsl.toWinPath(targetPath)
|
||||
@@ -156,60 +189,56 @@ const open = (repo: string, p: string) => {
|
||||
}, p)
|
||||
}
|
||||
|
||||
const search = (repo: string, str: string) => {
|
||||
export async function search (repo: string, str: string) {
|
||||
str = str.trim()
|
||||
if (!str) {
|
||||
return []
|
||||
}
|
||||
|
||||
const files = [] as any
|
||||
const files: TreeItem[] = []
|
||||
|
||||
const match = (p: string, str: string) => {
|
||||
return p.endsWith('.md') && !p.endsWith('.c.md') && new RegExp(str, 'i').test(fs.readFileSync(p, 'utf-8'))
|
||||
const match = async (p: string, str: string) => {
|
||||
return p.endsWith('.md') &&
|
||||
!p.endsWith('.c.md') &&
|
||||
new RegExp(str, 'i')
|
||||
.test(await fs.readFile(p, 'utf-8'))
|
||||
}
|
||||
|
||||
const travelFiles = (location: string, basePath: string) => {
|
||||
if (!fs.statSync(location).isDirectory()) {
|
||||
const travelFiles = async (location: string, basePath: string) => {
|
||||
// limit results
|
||||
if (files.length >= 70) {
|
||||
return
|
||||
}
|
||||
|
||||
const list = fs.readdirSync(location).filter(x => !x.startsWith('.') && !ignorePath.test(x))
|
||||
if (!(await fs.stat(location)).isDirectory()) {
|
||||
return
|
||||
}
|
||||
|
||||
list.forEach(x => {
|
||||
const p = path.join(location, x)
|
||||
const list = await fs.readdir(location, { withFileTypes: true })
|
||||
|
||||
if (fs.statSync(p).isDirectory()) {
|
||||
travelFiles(p, basePath)
|
||||
} else if (fs.statSync(p).isFile()) {
|
||||
if (match(p, str)) {
|
||||
await Promise.all(list.map(async x => {
|
||||
if (x.name.startsWith('.') || ignorePath.test(x.name)) {
|
||||
return
|
||||
}
|
||||
|
||||
const p = path.join(location, x.name)
|
||||
|
||||
if (x.isDirectory()) {
|
||||
await travelFiles(p, basePath)
|
||||
} else if (x.isFile()) {
|
||||
if (await match(p, str)) {
|
||||
files.push({
|
||||
repo,
|
||||
name: x,
|
||||
name: x.name,
|
||||
path: getRelativePath(basePath, p),
|
||||
type: 'file',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
withRepo(repo, repoPath => {
|
||||
travelFiles(repoPath, repoPath)
|
||||
})
|
||||
await withRepo(repo, repoPath => travelFiles(repoPath, repoPath))
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
export default {
|
||||
read,
|
||||
write,
|
||||
rm,
|
||||
mv,
|
||||
exists,
|
||||
hash,
|
||||
checkHash,
|
||||
upload,
|
||||
tree,
|
||||
open,
|
||||
search,
|
||||
}
|
||||
|
||||
+23
-22
@@ -1,5 +1,5 @@
|
||||
import * as os from 'os'
|
||||
import * as fs from 'fs'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import Koa from 'koa'
|
||||
import bodyParser from 'koa-body'
|
||||
@@ -7,7 +7,7 @@ import * as mime from 'mime'
|
||||
import request from 'request'
|
||||
import { promisify } from 'util'
|
||||
import { STATIC_DIR, HOME_DIR, HELP_DIR, USER_PLUGIN_DIR, FLAG_DISABLE_SERVER, APP_NAME } from '../constant'
|
||||
import file from './file'
|
||||
import * as file from './file'
|
||||
import dataRepository from './repository'
|
||||
import run from './run'
|
||||
import convert from './convert'
|
||||
@@ -25,8 +25,8 @@ const fileContent = async (ctx: any, next: any) => {
|
||||
if (ctx.path === '/api/file') {
|
||||
if (ctx.method === 'GET') {
|
||||
ctx.body = result('ok', 'success', {
|
||||
content: file.read(ctx.query.repo, ctx.query.path).toString(),
|
||||
hash: file.hash(ctx.query.repo, ctx.query.path)
|
||||
content: (await file.read(ctx.query.repo, ctx.query.path)).toString(),
|
||||
hash: await file.hash(ctx.query.repo, ctx.query.path)
|
||||
})
|
||||
} else if (ctx.method === 'POST') {
|
||||
const oldHash = ctx.request.body.old_hash
|
||||
@@ -35,25 +35,25 @@ const fileContent = async (ctx: any, next: any) => {
|
||||
throw new Error('No hash.')
|
||||
} else if (oldHash === 'new' && file.exists(ctx.request.body.repo, ctx.request.body.path)) {
|
||||
throw new Error('File already exists.')
|
||||
} else if (oldHash !== 'new' && !file.checkHash(ctx.request.body.repo, ctx.request.body.path, oldHash)) {
|
||||
} else if (oldHash !== 'new' && !(await file.checkHash(ctx.request.body.repo, ctx.request.body.path, oldHash))) {
|
||||
throw new Error('File is stale. Please refresh.')
|
||||
}
|
||||
|
||||
const hash = file.write(ctx.request.body.repo, ctx.request.body.path, ctx.request.body.content)
|
||||
const hash: string = await file.write(ctx.request.body.repo, ctx.request.body.path, ctx.request.body.content)
|
||||
ctx.body = result('ok', 'success', hash)
|
||||
} else if (ctx.method === 'DELETE') {
|
||||
file.rm(ctx.query.repo, ctx.query.path)
|
||||
await file.rm(ctx.query.repo, ctx.query.path)
|
||||
ctx.body = result()
|
||||
} else if (ctx.method === 'PATCH') {
|
||||
if (file.exists(ctx.request.body.repo, ctx.request.body.newPath)) {
|
||||
throw new Error('File already exists.')
|
||||
}
|
||||
|
||||
file.mv(ctx.request.body.repo, ctx.request.body.oldPath, ctx.request.body.newPath)
|
||||
await file.mv(ctx.request.body.repo, ctx.request.body.oldPath, ctx.request.body.newPath)
|
||||
ctx.body = result()
|
||||
}
|
||||
} else if (ctx.path === '/api/tree') {
|
||||
ctx.body = result('ok', 'success', file.tree(ctx.query.repo))
|
||||
ctx.body = result('ok', 'success', (await file.tree(ctx.query.repo)))
|
||||
} else {
|
||||
await next()
|
||||
}
|
||||
@@ -66,11 +66,11 @@ const attachment = async (ctx: any, next: any) => {
|
||||
const repo = ctx.request.body.repo
|
||||
const attachment = ctx.request.body.attachment
|
||||
const buffer = Buffer.from(attachment.substring(attachment.indexOf(',') + 1), 'base64')
|
||||
file.upload(repo, buffer, path)
|
||||
await file.upload(repo, buffer, path)
|
||||
ctx.body = result('ok', 'success', path)
|
||||
} else if (ctx.method === 'GET') {
|
||||
ctx.type = mime.getType(ctx.query.path)
|
||||
ctx.body = file.read(ctx.query.repo, ctx.query.path)
|
||||
ctx.body = await file.read(ctx.query.repo, ctx.query.path)
|
||||
}
|
||||
} else {
|
||||
await next()
|
||||
@@ -109,7 +109,7 @@ const searchFile = async (ctx: any, next: any) => {
|
||||
const search = ctx.query.search
|
||||
const repo = ctx.query.repo
|
||||
|
||||
ctx.body = result('ok', 'success', file.search(repo, search))
|
||||
ctx.body = result('ok', 'success', await file.search(repo, search))
|
||||
} else {
|
||||
await next()
|
||||
}
|
||||
@@ -163,7 +163,7 @@ const tmpFile = async (ctx: any, next: any) => {
|
||||
if (ctx.path.startsWith('/api/tmp-file')) {
|
||||
const absPath = path.join(os.tmpdir(), APP_NAME + '_' + ctx.query.name.replace(/\//g, '_'))
|
||||
if (ctx.method === 'GET') {
|
||||
ctx.body = fs.readFileSync(absPath)
|
||||
ctx.body = await fs.readFile(absPath)
|
||||
} else if (ctx.method === 'POST') {
|
||||
let body: any = ctx.request.body.toString()
|
||||
|
||||
@@ -174,10 +174,10 @@ const tmpFile = async (ctx: any, next: any) => {
|
||||
)
|
||||
}
|
||||
|
||||
fs.writeFileSync(absPath, body)
|
||||
await fs.writeFile(absPath, body)
|
||||
ctx.body = result('ok', 'success', { path: absPath })
|
||||
} else if (ctx.method === 'DELETE') {
|
||||
fs.unlinkSync(absPath)
|
||||
await fs.unlink(absPath)
|
||||
ctx.body = result('ok', 'success')
|
||||
}
|
||||
} else {
|
||||
@@ -211,10 +211,10 @@ const readme = async (ctx: any, next: any) => {
|
||||
if (ctx.path.startsWith('/api/help')) {
|
||||
if (ctx.query.path) {
|
||||
ctx.type = mime.getType(ctx.query.path)
|
||||
ctx.body = fs.readFileSync(path.join(HELP_DIR, ctx.query.path.replace('../', '')))
|
||||
ctx.body = await fs.readFile(path.join(HELP_DIR, ctx.query.path.replace('../', '')))
|
||||
} else {
|
||||
ctx.body = result('ok', 'success', {
|
||||
content: fs.readFileSync(path.join(HELP_DIR, ctx.query.doc.replace('../', ''))).toString()
|
||||
content: await fs.readFile(path.join(HELP_DIR, ctx.query.doc.replace('../', '')), 'utf-8')
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -227,13 +227,14 @@ const userPlugin = async (ctx: any, next: any) => {
|
||||
ctx.type = 'application/javascript; charset=utf-8'
|
||||
|
||||
let code = ''
|
||||
fs.readdirSync(USER_PLUGIN_DIR, { withFileTypes: true })
|
||||
.filter(x => x.isFile() && x.name.endsWith('.js'))
|
||||
.forEach(x => {
|
||||
for (const x of await fs.readdir(USER_PLUGIN_DIR, { withFileTypes: true })) {
|
||||
if (x.isFile() && x.name.endsWith('.js')) {
|
||||
code += `// ===== ${x.name} =====\n` +
|
||||
fs.readFileSync(path.join(USER_PLUGIN_DIR, x.name)) +
|
||||
(await fs.readFile(path.join(USER_PLUGIN_DIR, x.name))) +
|
||||
'\n// ===== end =====\n\n'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = code
|
||||
} else {
|
||||
await next()
|
||||
|
||||
@@ -105,7 +105,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function handleDocCreated ({ doc }: { doc: Doc | null }) {
|
||||
switchFile(doc)
|
||||
if (!doc || doc.type === 'file') {
|
||||
switchFile(doc)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDocDeleted ({ doc }: { doc: Doc | null }) {
|
||||
|
||||
@@ -142,6 +142,8 @@ export default defineComponent({
|
||||
triggerHook('VIEW_RENDERED', { getViewDom })
|
||||
}
|
||||
|
||||
let updateRender = debounce(render, 100)
|
||||
|
||||
function render () {
|
||||
logger.debug('render')
|
||||
// not markdown file, show code block.
|
||||
@@ -149,7 +151,13 @@ export default defineComponent({
|
||||
? currentContent.value
|
||||
: '```' + extname(fileName.value || '').replace(/^\./, '') + '\n' + currentContent.value + '\n```'
|
||||
|
||||
const startTime = performance.now()
|
||||
renderContent.value = markdown.render(content, { source: content, file: currentFile.value })
|
||||
const renderTime = performance.now() - startTime
|
||||
|
||||
logger.debug('rendered', 'cost', renderTime)
|
||||
|
||||
updateRender = debounce(render, Math.max(25, renderTime * (renderTime < 100 ? 1.2 : 1.8)))
|
||||
}
|
||||
|
||||
const renderDebonce = debounce(render, 100, { leading: true })
|
||||
@@ -239,7 +247,6 @@ export default defineComponent({
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
})
|
||||
|
||||
const updateRender = debounce(renderDebonce, 20)
|
||||
watch(
|
||||
() => currentContent.value + filePath.value,
|
||||
() => autoPreview.value && updateRender()
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ item.name === '/' ? currentRepoName : item.name }} <span class="count">({{item.children ? item.children.length : 0}})</span>
|
||||
</div>
|
||||
<div class="item-action">
|
||||
<svg-icon class="icon" name="folder-plus-solid" @click.exact.stop.prevent="createFile()" :title="$t('tree.context-menu.create')"></svg-icon>
|
||||
<svg-icon class="icon" name="folder-plus-solid" @click.exact.stop.prevent="createFile()" :title="$t('tree.context-menu.create-doc')"></svg-icon>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
@@ -156,17 +156,27 @@ export function runCommand (command: Command) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a command shortcuts label.
|
||||
* @param id
|
||||
* Get shortcuts label.
|
||||
* @param idOrKeys command id or keys
|
||||
* @returns
|
||||
*/
|
||||
export function getKeysLabel (id: string): string {
|
||||
const command = getCommand(id)
|
||||
if (!command || !command.keys) {
|
||||
return ''
|
||||
export function getKeysLabel (id: string): string
|
||||
export function getKeysLabel (keys: (string | number)[]): string
|
||||
export function getKeysLabel (idOrKeys: (string | number)[] | string): string {
|
||||
let keys = []
|
||||
|
||||
if (typeof idOrKeys === 'string') {
|
||||
const command = getCommand(idOrKeys)
|
||||
if (!command || !command.keys) {
|
||||
return ''
|
||||
}
|
||||
|
||||
keys = command.keys
|
||||
} else {
|
||||
keys = idOrKeys
|
||||
}
|
||||
|
||||
return command.keys.map(getKeyLabel).join(isMacOS ? ' ' : '+')
|
||||
return keys.map(getKeyLabel).join(isMacOS ? ' ' : '+')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import statusBarSetting from '@fe/plugins/status-bar-setting'
|
||||
import statusBarRepositorySwitch from '@fe/plugins/status-bar-repository-switch'
|
||||
import statusBarView from '@fe/plugins/status-bar-view'
|
||||
import statusBarNavigation from '@fe/plugins/status-bar-navigation'
|
||||
import statusBarInsert from './plugins/status-bar-insert'
|
||||
import statusBarTool from '@fe/plugins/status-bar-tool'
|
||||
import statusBarHelp from '@fe/plugins/status-bar-help'
|
||||
import statusBarRefresh from '@fe/plugins/status-bar-refresh'
|
||||
@@ -52,6 +53,7 @@ export default [
|
||||
statusBarRepositorySwitch,
|
||||
statusBarView,
|
||||
statusBarNavigation,
|
||||
statusBarInsert,
|
||||
statusBarTool,
|
||||
statusBarHelp,
|
||||
statusBarTerminal,
|
||||
|
||||
@@ -62,14 +62,14 @@ function addDocument (doc: Doc) {
|
||||
|
||||
export default {
|
||||
name: 'editor-attachment',
|
||||
register: () => {
|
||||
register: (ctx) => {
|
||||
whenEditorReady().then(({ editor, monaco }) => {
|
||||
editor.addAction({
|
||||
id: 'plugin.editor.add-image',
|
||||
contextMenuGroupId: 'modification',
|
||||
label: t('add-image'),
|
||||
keybindings: [
|
||||
monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.KEY_I
|
||||
monaco.KeyMod.Alt | monaco.KeyCode.KEY_I
|
||||
],
|
||||
run: () => addAttachment(true),
|
||||
})
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
contextMenuGroupId: 'modification',
|
||||
label: t('editor.context-menu.add-attachment'),
|
||||
keybindings: [
|
||||
monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.KEY_F
|
||||
monaco.KeyMod.Alt | monaco.KeyCode.KEY_F
|
||||
],
|
||||
run: () => addAttachment(false),
|
||||
})
|
||||
@@ -87,12 +87,36 @@ export default {
|
||||
contextMenuGroupId: 'modification',
|
||||
label: t('editor.context-menu.add-doc'),
|
||||
keybindings: [
|
||||
monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.KEY_I
|
||||
monaco.KeyMod.Alt | monaco.KeyCode.KEY_D
|
||||
],
|
||||
run: () => {
|
||||
getActionHandler('filter.choose-document')().then(addDocument)
|
||||
},
|
||||
run: () => getActionHandler('filter.choose-document')().then(addDocument),
|
||||
})
|
||||
})
|
||||
|
||||
ctx.statusBar.tapMenus(menus => {
|
||||
menus['status-bar-insert']?.list?.push(
|
||||
{
|
||||
id: 'plugin.editor.add-image',
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('add-image'),
|
||||
subTitle: ctx.command.getKeysLabel([ctx.command.Alt, 'i']),
|
||||
onClick: () => addAttachment(true),
|
||||
},
|
||||
{
|
||||
id: 'plugin.editor.add-file',
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('editor.context-menu.add-attachment'),
|
||||
subTitle: ctx.command.getKeysLabel([ctx.command.Alt, 'f']),
|
||||
onClick: () => addAttachment(false),
|
||||
},
|
||||
{
|
||||
id: 'plugin.editor.add-document',
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('editor.context-menu.add-doc'),
|
||||
subTitle: ctx.command.getKeysLabel([ctx.command.Alt, 'd']),
|
||||
onClick: () => getActionHandler('filter.choose-document')().then(addDocument),
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
} as Plugin
|
||||
|
||||
@@ -51,7 +51,15 @@ function createDependencyProposals (range: any) {
|
||||
|
||||
export default {
|
||||
name: 'editor-markdown',
|
||||
register: () => {
|
||||
register: (ctx) => {
|
||||
function insertDate () {
|
||||
insert(dayjs().format('YYYY-MM-DD'))
|
||||
}
|
||||
|
||||
function insertTime () {
|
||||
insert(dayjs().format('HH:mm:ss'))
|
||||
}
|
||||
|
||||
whenEditorReady().then(({ editor, monaco }) => {
|
||||
const KM = monaco.KeyMod
|
||||
const KC = monaco.KeyCode
|
||||
@@ -63,9 +71,7 @@ export default {
|
||||
keybindings: [
|
||||
KM.Shift | KM.Alt | KC.KEY_D
|
||||
],
|
||||
run: () => {
|
||||
insert(dayjs().format('YYYY-MM-DD'))
|
||||
}
|
||||
run: insertDate
|
||||
})
|
||||
|
||||
editor.addAction({
|
||||
@@ -75,9 +81,7 @@ export default {
|
||||
keybindings: [
|
||||
KM.Shift | KM.Alt | KC.KEY_T
|
||||
],
|
||||
run: () => {
|
||||
insert(dayjs().format('HH:mm:ss'))
|
||||
}
|
||||
run: insertTime
|
||||
})
|
||||
|
||||
editor.addCommand(KM.Alt | KC.Enter, () => {
|
||||
@@ -94,22 +98,6 @@ export default {
|
||||
insert(getOneIndent())
|
||||
})
|
||||
|
||||
editor.addCommand(KM.CtrlCmd | KM.Shift | KC.UpArrow, () => {
|
||||
editor.getAction('editor.action.moveLinesUpAction').run()
|
||||
})
|
||||
|
||||
editor.addCommand(KM.CtrlCmd | KM.Shift | KC.DownArrow, () => {
|
||||
editor.getAction('editor.action.moveLinesDownAction').run()
|
||||
})
|
||||
|
||||
editor.addCommand(KM.CtrlCmd | KM.Shift | KC.KEY_D, () => {
|
||||
editor.getAction('editor.action.copyLinesDownAction').run()
|
||||
})
|
||||
|
||||
editor.addCommand(KM.CtrlCmd | KC.KEY_J, () => {
|
||||
editor.getAction('editor.action.joinLines').run()
|
||||
})
|
||||
|
||||
editor.addCommand(KM.chord(KM.CtrlCmd | KC.KEY_K, KM.CtrlCmd | KC.KEY_U), () => {
|
||||
editor.getAction('editor.action.transformToUppercase').run()
|
||||
})
|
||||
@@ -161,5 +149,24 @@ export default {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ctx.statusBar.tapMenus(menus => {
|
||||
menus['status-bar-insert']?.list?.push(
|
||||
{
|
||||
id: 'plugin.editor.insert-time',
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('editor.context-menu.insert-time'),
|
||||
subTitle: ctx.command.getKeysLabel([ctx.command.Shift, ctx.command.Alt, 't']),
|
||||
onClick: insertTime,
|
||||
},
|
||||
{
|
||||
id: 'plugin.editor.insert-date',
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('editor.context-menu.insert-date'),
|
||||
subTitle: ctx.command.getKeysLabel([ctx.command.Shift, ctx.command.Alt, 'd']),
|
||||
onClick: insertDate,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
} as Plugin
|
||||
|
||||
@@ -119,14 +119,7 @@ export default {
|
||||
}
|
||||
|
||||
ctx.statusBar.tapMenus(menus => {
|
||||
menus['status-bar-tool']?.list?.push(
|
||||
{
|
||||
id: pasteRtfActionId,
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('status-bar.tool.paste-rft'),
|
||||
subTitle: 'Markdown',
|
||||
onClick: pasteRtf
|
||||
},
|
||||
menus['status-bar-insert']?.list?.push(
|
||||
{
|
||||
id: pasteImageAsBase64ActionId,
|
||||
type: 'normal',
|
||||
@@ -136,6 +129,13 @@ export default {
|
||||
pasteImageFromClipboard(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: pasteRtfActionId,
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('status-bar.tool.paste-rft'),
|
||||
subTitle: 'Markdown',
|
||||
onClick: pasteRtf
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ export default {
|
||||
{ id: 'duplicate', label: t('tree.context-menu.duplicate'), onClick: () => ctx.doc.duplicateDoc(node) },
|
||||
] : []),
|
||||
...(node.type === 'dir' ? [
|
||||
{ id: 'create', label: t('tree.context-menu.create'), onClick: () => ctx.doc.createDoc({ repo: node.repo }, node) }
|
||||
{ id: 'create-doc', label: t('tree.context-menu.create-doc'), onClick: () => ctx.doc.createDoc({ repo: node.repo }, node) },
|
||||
{ id: 'create-dir', label: t('tree.context-menu.create-dir'), onClick: () => ctx.doc.createDir({ repo: node.repo }, node) }
|
||||
] : []),
|
||||
...(node.path !== '/' ? [
|
||||
{ id: 'rename', label: t('tree.context-menu.rename'), onClick: () => ctx.doc.moveDoc(node) },
|
||||
|
||||
@@ -121,7 +121,7 @@ export default {
|
||||
})
|
||||
|
||||
ctx.statusBar.tapMenus(menus => {
|
||||
menus['status-bar-tool']?.list?.push({
|
||||
menus['status-bar-insert']?.list?.unshift({
|
||||
id: addImageActionId,
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('add-image'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import MarkdownItContainer from 'markdown-it-container'
|
||||
import { h } from 'vue'
|
||||
import { Fragment, h } from 'vue'
|
||||
import { Plugin } from '@fe/context'
|
||||
import type Token from 'markdown-it/lib/token'
|
||||
|
||||
@@ -50,6 +50,70 @@ export default {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
background: var(--g-color-80);
|
||||
border-radius: var(--g-border-radius);
|
||||
border: 1px solid var(--g-color-80);
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group p {
|
||||
order: 100;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group .custom-container-title {
|
||||
order: 1;
|
||||
padding-left: 8px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group .group-item-radio {
|
||||
position: absolute;
|
||||
right: -99999999px;
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group .group-item-label {
|
||||
order: 1;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 2;
|
||||
padding: 0 1em;
|
||||
color: var(--g-color-20);
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group .group-item-label:hover {
|
||||
background: var(--g-color-85);
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group .group-item-content {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
display: none;
|
||||
border-radius: var(--g-border-radius);
|
||||
border-top-left-radius: 0;
|
||||
padding: 12px;
|
||||
padding-bottom: 0;
|
||||
background: var(--g-color-100);
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group .group-item-radio:checked + .group-item-label {
|
||||
background: var(--g-color-100);
|
||||
border-top-left-radius: var(--g-border-radius);
|
||||
border-top-right-radius: var(--g-border-radius);
|
||||
color: var(--g-color-0);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.markdown-view .markdown-body .custom-container.group .group-item-radio:checked + .group-item-label + .group-item-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[app-theme=dark] .markdown-view .markdown-body .custom-container.danger {
|
||||
background-color: #503f3f;
|
||||
color: #d9bebe;
|
||||
@@ -73,8 +137,11 @@ export default {
|
||||
}
|
||||
`)
|
||||
|
||||
let groupItemIdx = 0
|
||||
let groupItemName = 0
|
||||
|
||||
ctx.markdown.registerPlugin(md => {
|
||||
['tip', 'warning', 'danger', 'details'].forEach(name => {
|
||||
['tip', 'warning', 'danger', 'details', 'group-item', 'group'].forEach(name => {
|
||||
const reg = new RegExp(`^${name}\\s*(.*)$`)
|
||||
|
||||
md.use(MarkdownItContainer, name, {
|
||||
@@ -87,8 +154,28 @@ export default {
|
||||
|
||||
if (tokens[idx].nesting === 1) {
|
||||
const title = md.utils.escapeHtml(match![1])
|
||||
const containerTag = name === 'details' ? 'details' : 'div'
|
||||
const containerClass = `custom-container ${name}`
|
||||
|
||||
if (name === 'group-item') {
|
||||
const parent = h('div', { class: 'group-item-content' }, [])
|
||||
const radioName = `group-item-${groupItemName}`
|
||||
const id = `group-item-${groupItemName}-${groupItemIdx++}`
|
||||
const checked = groupItemIdx === 1 || title.startsWith('*')
|
||||
|
||||
return {
|
||||
node: h(Fragment, [
|
||||
h('input', { key: id, class: 'group-item-radio', id, name: radioName, type: 'radio', checked }),
|
||||
h('label', { class: 'group-item-label', for: id }, title.replace('*', '').trim() || 'Group Item'),
|
||||
parent
|
||||
]),
|
||||
parent
|
||||
}
|
||||
} else if (name === 'group') {
|
||||
groupItemIdx = 0
|
||||
groupItemName++
|
||||
}
|
||||
|
||||
const containerTag = name === 'details' ? 'details' : 'div'
|
||||
const titleTag = name === 'details' ? 'summary' : 'p'
|
||||
const titleClass = name === 'details' ? '' : 'custom-container-title'
|
||||
|
||||
|
||||
@@ -76,11 +76,19 @@ function lineCount (str: string) {
|
||||
return s
|
||||
}
|
||||
|
||||
function exportVar (key: string, val: any): Result {
|
||||
return { __macroResult: true, vars: { [key]: val }, toString: () => '' }
|
||||
}
|
||||
|
||||
async function include (belongDoc: Doc | undefined | null, path: string, trim = false): Promise<Result> {
|
||||
if (!belongDoc) {
|
||||
throw new Error('Current document is null')
|
||||
}
|
||||
|
||||
if (!path.endsWith('.md')) {
|
||||
return { __macroResult: true, toString: () => 'Error: $include markdown file only' }
|
||||
}
|
||||
|
||||
try {
|
||||
const absolutePath = resolve(dirname(belongDoc.path), path)
|
||||
const { content } = await readFile({ repo: belongDoc.repo, path: absolutePath })
|
||||
@@ -120,6 +128,7 @@ export default {
|
||||
}
|
||||
|
||||
const vars: Record<string, any> = {
|
||||
$export: exportVar,
|
||||
$include: include.bind(null, file),
|
||||
$ctx: ctx,
|
||||
$doc: {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { showHelp } from '@fe/services/document'
|
||||
import { Plugin } from '@fe/context'
|
||||
import { Alt, getKeysLabel } from '@fe/core/command'
|
||||
|
||||
export default {
|
||||
name: 'status-bar-help',
|
||||
register: ctx => {
|
||||
const showHelpAction = ctx.action.registerAction({
|
||||
name: 'plugin.status-bar-help.show-readme',
|
||||
keys: [Alt, 'h'],
|
||||
keys: null,
|
||||
handler: () => showHelp('README.md')
|
||||
})
|
||||
|
||||
@@ -39,7 +38,6 @@ export default {
|
||||
id: 'toggle-readme',
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('status-bar.help.readme'),
|
||||
tips: getKeysLabel(showHelpAction.name),
|
||||
onClick: () => ctx.action.getActionHandler(showHelpAction.name)()
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Plugin } from '@fe/context'
|
||||
|
||||
export default {
|
||||
name: 'status-bar-tool',
|
||||
register: ctx => {
|
||||
ctx.statusBar.tapMenus(menus => {
|
||||
menus['status-bar-insert'] = {
|
||||
id: 'status-bar-insert',
|
||||
position: 'left',
|
||||
title: ctx.i18n.t('status-bar.insert.insert'),
|
||||
list: []
|
||||
}
|
||||
})
|
||||
}
|
||||
} as Plugin
|
||||
@@ -3,7 +3,7 @@ import * as api from '@fe/support/api'
|
||||
import { encodeMarkdownLink } from '@fe/utils'
|
||||
import { useToast } from '@fe/support/ui/toast'
|
||||
import store from '@fe/support/store'
|
||||
import { CtrlCmd, getKeysLabel, isCommand, LeftClick, Shift } from '@fe/core/command'
|
||||
import { CtrlCmd, isCommand, LeftClick, Shift } from '@fe/core/command'
|
||||
import { replaceValue } from '@fe/services/editor'
|
||||
import { refreshTree } from '@fe/services/tree'
|
||||
import { upload } from '@fe/services/base'
|
||||
@@ -108,7 +108,6 @@ export default {
|
||||
register: (ctx) => {
|
||||
ctx.action.registerAction({
|
||||
name: actionKeydown,
|
||||
keys: [CtrlCmd, Shift, 'l'],
|
||||
handler: transformAll
|
||||
})
|
||||
|
||||
@@ -128,7 +127,6 @@ export default {
|
||||
id: actionKeydown,
|
||||
type: 'normal',
|
||||
title: ctx.i18n.t('status-bar.tool.convert-img-link'),
|
||||
subTitle: getKeysLabel(actionKeydown),
|
||||
onClick: ctx.action.getActionHandler(actionKeydown)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -152,6 +152,55 @@ export async function createDoc (doc: Optional<Pick<Doc, 'repo' | 'path' | 'cont
|
||||
return file
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dir.
|
||||
* @param doc
|
||||
* @param baseDoc
|
||||
* @returns
|
||||
*/
|
||||
export async function createDir (doc: Pick<Doc, 'repo' | 'path' | 'content'>, baseDoc: Doc): Promise<Doc>
|
||||
export async function createDir (doc: Optional<Pick<Doc, 'repo' | 'path' | 'content'>, 'path'>, baseDoc?: Doc): Promise<Doc>
|
||||
export async function createDir (doc: Optional<Pick<Doc, 'repo' | 'path' | 'content'>, 'path'>, baseDoc?: Doc) {
|
||||
if (!doc.path) {
|
||||
if (baseDoc) {
|
||||
const currentPath = baseDoc.type === 'dir' ? baseDoc.path : dirname(baseDoc.path)
|
||||
|
||||
const name = await useModal().input({
|
||||
title: t('document.create-dir-dialog.title'),
|
||||
hint: t('document.create-dir-dialog.hint'),
|
||||
content: t('document.current-path', currentPath),
|
||||
value: 'new-folder',
|
||||
select: true
|
||||
})
|
||||
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
|
||||
doc.path = join(currentPath, name, '/')
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc.path) {
|
||||
throw new Error('Need path')
|
||||
}
|
||||
|
||||
const name = basename(doc.path)
|
||||
|
||||
const dir: Doc = { ...doc, path: doc.path, type: 'dir', name, contentHash: 'new' }
|
||||
|
||||
try {
|
||||
await api.writeFile(dir)
|
||||
|
||||
triggerHook('DOC_CREATED', { doc: dir })
|
||||
} catch (error: any) {
|
||||
useToast().show('warning', error.message)
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate a document.
|
||||
* @param originDoc
|
||||
|
||||
@@ -76,6 +76,7 @@ export function insert (text: string) {
|
||||
forceMoveMarkers: true
|
||||
}
|
||||
])
|
||||
getEditor().focus()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -83,7 +83,7 @@ export async function readFile (file: PathItem) {
|
||||
* @param content
|
||||
* @returns
|
||||
*/
|
||||
export async function writeFile (file: Doc, content: string) {
|
||||
export async function writeFile (file: Doc, content = '') {
|
||||
const { repo, path, contentHash } = file
|
||||
const result = await fetchHttp('/api/file', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -15,7 +15,7 @@ const data = {
|
||||
'copy-code': 'Copy code',
|
||||
'loading': 'Loading',
|
||||
'add-image': 'Add Image',
|
||||
'exit-presentation-msg': 'Press the Esc key to exit the demo mode',
|
||||
'exit-presentation-msg': 'Press the Esc key to exit',
|
||||
'reload': 'Reload',
|
||||
'open-in-new-window': 'New Window',
|
||||
'view-figure': 'View Figure',
|
||||
@@ -115,7 +115,11 @@ const data = {
|
||||
'file-transform-error': 'Encrypted and unencrypted files cannot be converted to each other',
|
||||
'create-dialog': {
|
||||
'title': 'Create a file (encrypted file ends with .c.md)',
|
||||
'hint': 'File path',
|
||||
'hint': 'File name',
|
||||
},
|
||||
'create-dir-dialog': {
|
||||
'title': 'Create a Folder',
|
||||
'hint': 'Folder name',
|
||||
},
|
||||
'duplicate-dialog': {
|
||||
'title': 'Duplicate a File',
|
||||
@@ -156,6 +160,9 @@ const data = {
|
||||
'forward': 'Forward',
|
||||
'back': 'Back',
|
||||
},
|
||||
'insert': {
|
||||
'insert': 'Insert',
|
||||
},
|
||||
'tool': {
|
||||
'tool': 'Tool',
|
||||
'paste-rft': 'Paste RTF',
|
||||
@@ -200,7 +207,8 @@ const data = {
|
||||
'mark': 'Mark File',
|
||||
'unmark': 'Unmark File',
|
||||
'duplicate': 'Duplicate',
|
||||
'create': 'New File',
|
||||
'create-doc': 'New File',
|
||||
'create-dir': 'New Folder',
|
||||
'rename': 'Rename / Move',
|
||||
'delete': 'Delete',
|
||||
'open-in-os': 'Open in OS',
|
||||
|
||||
@@ -115,8 +115,12 @@ const data: BaseLanguage = {
|
||||
'wrong-password': '密码错误',
|
||||
'file-transform-error': '加密文件和非加密文件不能互相转换',
|
||||
'create-dialog': {
|
||||
'title': '创建文件(加密文件以 .c.md 结尾)',
|
||||
'hint': '文件路径',
|
||||
'title': '创建文件(加密文件以 .c.md 结尾)',
|
||||
'hint': '文件名',
|
||||
},
|
||||
'create-dir-dialog': {
|
||||
'title': '创建文件夹',
|
||||
'hint': '文件夹名',
|
||||
},
|
||||
'duplicate-dialog': {
|
||||
'title': '重复文件',
|
||||
@@ -157,6 +161,9 @@ const data: BaseLanguage = {
|
||||
'forward': '前进',
|
||||
'back': '后退',
|
||||
},
|
||||
'insert': {
|
||||
'insert': '插入',
|
||||
},
|
||||
'tool': {
|
||||
'tool': '工具',
|
||||
'paste-rft': '粘贴富文本',
|
||||
@@ -201,7 +208,8 @@ const data: BaseLanguage = {
|
||||
'mark': '标记文件',
|
||||
'unmark': '取消标记',
|
||||
'duplicate': '重复文件',
|
||||
'create': '创建文件',
|
||||
'create-doc': '创建文件',
|
||||
'create-dir': '创建文件夹',
|
||||
'rename': '重命名 / 移动',
|
||||
'delete': '删除',
|
||||
'open-in-os': '在系统中打开',
|
||||
|
||||
Reference in New Issue
Block a user