├── .gitignore ├── .npmrc ├── Doc ├── AutoFormatting.md ├── AutoFormatting_ZH.md ├── CustomRules.md ├── CustomRules_ZH.md ├── EditEnhancements.md ├── EditEnhancements_ZH.md ├── UserDefinedRegExp.md └── UserDefinedRegExp_ZH.md ├── README.md ├── README_ZH.md ├── assets ├── donate.png ├── enhance-paste.gif └── multi-cursor.gif ├── changelog.md ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── src ├── core.ts ├── lang │ └── locale │ │ ├── en-US.ts │ │ ├── index.ts │ │ ├── ru-RU.ts │ │ ├── zh-CN.ts │ │ └── zh-TW.ts ├── main.ts ├── settings.ts ├── syntax.ts ├── tabstop.ts ├── tabstops_state_field.ts └── utils.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | 13 | # obsidian 14 | data.json 15 | 16 | .DS_Store 17 | .vscode 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /Doc/AutoFormatting.md: -------------------------------------------------------------------------------- 1 | # Text Auto-formatting 2 | 3 | Easy Typing plugin provides powerful text auto-formatting features that automatically format text during input based on user-defined rules. 4 | 5 | ## Main Features 6 | 7 | ### 1. Auto Capitalize 8 | 9 | - Automatically capitalizes the first letter of each sentence in English input mode 10 | - Can be set to work only when typing or globally 11 | - In typing-only mode, auto-capitalization can be undone 12 | 13 | ### 2. Auto Space between Chinese and English 14 | 15 | - Automatically adds spaces between Chinese and English text 16 | - Can be set to add spaces between Chinese characters and numbers 17 | 18 | ### 3. Auto Space between Punctuation and Text 19 | 20 | - Intelligently adds spaces between text and English punctuation 21 | 22 | ### 4. Space Strategies for Different Blocks 23 | 24 | The plugin divides text lines into several types of blocks: 25 | - Text block 26 | - Inline formula block 27 | - Inline code block 28 | - Link block 29 | - User-defined regular expression block 30 | 31 | Each type of block can have one of three space strategies: 32 | 1. No requirement: No space requirement between this block and others 33 | 2. Soft space: This block can be separated from others by soft spaces (e.g., punctuation) 34 | 3. Strict space: This block must be separated from others by a space 35 | 36 | ### 5. Custom Regular Expression Blocks 37 | 38 | Users can define special blocks using custom regular expressions and set specific space strategies for these blocks. This is very useful for handling text with special formats. 39 | 40 | For more details on this feature, please refer to the [User-defined Regular Expression Blocks](./UserDefinedRegExp.md) document. 41 | 42 | ## Usage 43 | 44 | 1. Enable auto-formatting in the plugin settings 45 | 2. Adjust various settings as needed 46 | 3. The plugin will automatically apply formatting rules during editing 47 | 48 | ## Notes 49 | 50 | - Auto-formatting is enabled by default and works in real-time during editing 51 | - You can use plugin commands to format the entire current article, the current line, or the currently selected area 52 | - Certain special areas (such as code blocks, math formulas) will not be auto-formatted 53 | -------------------------------------------------------------------------------- /Doc/AutoFormatting_ZH.md: -------------------------------------------------------------------------------- 1 | # 文本自动格式化 2 | 3 | Easy Typing 插件提供了强大的文本自动格式化功能,可以根据用户设置的规则,在输入过程中自动格式化文本。 4 | 5 | ## 主要功能 6 | 7 | ### 1. 首字母大写 8 | 9 | - 在英文输入模式下,自动将每个句子的首字母大写 10 | - 可选择仅在输入时生效或全局生效 11 | - 输入时生效模式下,首字母大写操作可撤销 12 | 13 | ### 2. 中英文间自动添加空格 14 | 15 | - 自动在中文和英文之间添加空格 16 | - 可设置是否在中文和数字之间添加空格 17 | 18 | ### 3. 标点与文本间自动添加空格 19 | 20 | - 智能地在文本和英文标点之间添加空格 21 | 22 | ### 4. 不同区块间的空格策略 23 | 24 | 插件将文本行分为几种区块: 25 | - 文本块 26 | - 行内公式块 27 | - 行内代码块 28 | - 链接块 29 | - 用户自定义正则匹配块 30 | 31 | 每种区块可设置三种空格策略: 32 | 1. 无要求: 该区块与其他区块间没有空格要求 33 | 2. 软空格: 该区块与其他区块可以由软空格(如标点符号)分割 34 | 3. 严格空格: 该区块与其他区块间必须有空格分割 35 | 36 | ### 5. 自定义正则区块 37 | 38 | 用户可以通过自定义正则表达式来定义特殊区块,并为这些区块设��特定的空格策略。这对于处理特殊格式的文本非常有用。 39 | 40 | 关于此功能的更多详细信息,请参阅[用户自定义正则表达式区块](./UserDefinedRegExp_ZH.md)文档。 41 | 42 | ## 使用方法 43 | 44 | 1. 在插件设置中开启自动格式化功能 45 | 2. 根据需要调整各项设置 46 | 3. 在编辑过程中,插件会自动应用格式化规则 47 | 48 | ## 注意事项 49 | 50 | - 自动格式化功能默认在编辑过程中即时生效 51 | - 可以通过插件命令来格式化当前文章全文、当前行或当前选中区域 52 | - 某些特殊区域(如代码块、数学公式)不会被自动格式化 53 | -------------------------------------------------------------------------------- /Doc/CustomRules.md: -------------------------------------------------------------------------------- 1 | # Customizable Conversion Rules 2 | 3 | Easy Typing plugin supports user-defined conversion rules to meet personalized editing needs. 4 | 5 | ## Rule Types 6 | 7 | ### 1. Conversion Rules for Selected Text 8 | 9 | When text is selected and a specific trigger symbol is entered, the text will be converted to a specified format. 10 | 11 | Format: `trigger symbol,left string,right string` 12 | 13 | Example: `-,~~,~~` means that when `-` is entered after selecting text, it will convert the text to `~~selected text~~` 14 | 15 | ### 2. Conversion Rules for Deletion 16 | 17 | When using the backspace key to delete text, specific text formats will be converted to other formats. 18 | 19 | Format for matching text before deletion: `text before cursor|text after cursor` 20 | Format for matching text after deletion: `text before cursor|text after cursor` 21 | 22 | Example: Before deletion: `<|>`, After deletion: `|` means that when the cursor is between `<>`, pressing the backspace key will delete the entire `<>` 23 | 24 | More examples: 25 | 26 | - Before deletion: `- [ ] |`, After deletion: `- |` means that when the cursor is after `- [ ]`, pressing the backspace key will delete the entire `[ ]`, effectively turning an empty task item into a list item. 27 | 28 | ### 3. Conversion Rules for Input 29 | 30 | When inputting specific character sequences, they will be automatically converted to other formats. 31 | 32 | Format for matching text before conversion: `text before cursor|text after cursor` 33 | Format for matching text after conversion: `text before cursor|text after cursor` 34 | 35 | Example: Before conversion: `:)|`, After conversion: `😀|` means that inputting `:)` will be automatically converted to the smiling face emoji 😀, with the cursor positioned after it 36 | 37 | ## Syntax for Custom Conversion and Deletion Rules 38 | 39 | - Use `|` to represent the cursor position 40 | - Deletion rules and input rules support regular expression matching, in the format `r/regex before cursor/|r/regex after cursor/` 41 | - In the converted text, use `[[n]]` to reference the content captured by regular expression matching groups, where n is a zero-based index. `[[0]]` refers to the first matching group, `[[1]]` to the second, and so on. 42 | - Note: `[[n]]` is only valid in the text after conversion 43 | - For the mth group matched by the regex after the cursor, use `[[m+n-1]]`, where n is the number of groups matched by the regex before the cursor. 44 | - Use `$n` (where n is a number) to represent multiple cursor positions. `$0` represents the first cursor position, `$1` the second, and so on. 45 | - You can use `${n: text}` to indicate that the text is selected 46 | - In multi-cursor scenarios, after the rule takes effect, you can switch between cursor positions using the Tab key. 47 | - Note: `$n` is only valid in the text after conversion 48 | 49 | ## Advanced Usage 50 | 51 | ### Example of Multiple Cursors and Regular Expressions 52 | 53 | Example 1: Custom conversion rule: Match `r/(?<=^|\n)([\w-]+)-call/|`, Convert to `> [![[0]]] $0\n> $1`, which will convert `note-call` at the beginning of a line to: 54 | ``` 55 | > [!note] $0 56 | > $1 57 | ``` 58 | Here, $0 and $1 represent different cursor positions, which can be switched between using the Tab key. 59 | ![](/assets/multi-cursor.gif) 60 | 61 | Example 2: Match `r/t_(\d|i)/|`, Convert to `$t_{[[0]]}$`, which will convert `t_1` inline to `$t_{1}$` 62 | 63 | Example 3: Custom deletion rule, Before: `r/ ?!?\[\[[^\n\[\]]*\]\]/|`, After: `|`, which allows one-click deletion of the wikilink `[[link content]]` before the cursor. 64 | -------------------------------------------------------------------------------- /Doc/CustomRules_ZH.md: -------------------------------------------------------------------------------- 1 | # 自定义转换规则 2 | 3 | Easy Typing 插件支持用户自定义转换规则,以满足个性化的编辑需求。 4 | 5 | ## 使用方法 6 | 7 | 1. 在插件设置中找到自定义转换规则部分 8 | 2. 根据需要添加相应类型的规则 9 | 3. 规则会在编辑过程中自动生效 10 | 11 | ## 规则类型 12 | 13 | ### 1. 选中文本时的转换规则 14 | 15 | 当选中文本并输入特定触发符号时,将文本转换为指定格式。 16 | 17 | 格式:`触发符号,左侧字符串,右侧字符串` 18 | 19 | 例如:`-,~~,~~` 表示选中文本后输入 `-`,会将文本转换为 `~~选中的文本~~` 20 | 21 | ### 2. 删除时的转换规则 22 | 23 | 在使用退格键删除文本时,将特定格式的文本转换为其他格式。 24 | 25 | 匹配删除前文本格式:`光标前文本|光标后文本` 26 | 匹配删除后文本格式:`光标前文本|光标后文本` 27 | 28 | 例如:删除前:`<|>`,删除后:`|` 表示当光标在 `<>` 之间时,按退格键会删除整个 `<>` 29 | 30 | 更多例子: 31 | 32 | - 删除前:`- [ ] |`,删除后:`- |` 表示当光标在 `- [ ]` 之后时,按退格键会删除整个 `[ ]`,实际效果是删除空task项时,会变成列表项。 33 | 34 | ### 3. 输入时的转换规则 35 | 36 | 在输入特定字符序列时,自动将其转换为其他格式。 37 | 38 | 匹配转换前文本模式:`光标前文本|光标后文本` 39 | 匹配转换后文本模式:`光标前文本|光标后文本` 40 | 例如:转换前:`:)|`,转换后:`😀|`, 表示输入 `:)` 时会自动转换为笑脸表情 😀,光标位于表情符号后面 41 | 42 | 43 | ## 自定义转换、删除规则的语法 44 | 45 | - 使用 `|` 来表示光标位置 46 | - 删除规则和输入规则支持正则表达式匹配,格式为 `r/光标前正则表达式/|r/光标后正则表达式/` 47 | - 转换后的文本中可以使用 `[[n]]` 来引用正则表达式捕获的匹配组内容,以0开始的数字表示匹配组,`[[0]]` 表示第一个匹配组,`[[1]]` 表示第二个匹配组,以此类推。 48 | - 注意:`[[n]]` 仅在转换后文本中有效 49 | - 对于 光标后正则表达式 匹配的第 m 组内容,设置为 `[[m+n-1]]`,n 为光标前正则表达式匹配的组的数量。 50 | - 可以使用 `$n` (n 为数字)来表示多光标位置,`$0` 表示第一个光标位置,`$1` 表示第二个光标位置,以此类推。 51 | - 可以使用 `${n: 文本}` 来表示文本被选中 52 | - 多光标的情况下,在规则生效后,按 Tab 键可以在多个光标位置之间切换。 53 | - 注意:`$n` 仅在转换后文本中有效 54 | 55 | ## 高级用法 56 | 57 | ### 多光标和正则表达式转换规则示例 58 | 59 | 例子1:自定义转换规则: 匹配 `r/(?<=^|\n)([\w-]+)-call/|`, 转换成 `> [![[0]]] $0\n> $1`,即可实现在行首输入 `note-call`,即可转换成 60 | ``` 61 | > [!note] $0 62 | > $1 63 | ``` 64 | 其中, $0, $1 表示不同的两个光标,可以通过 Tab 键从 $0 跳转到 $1 65 | ![](/assets/multi-cursor.gif) 66 | 67 | 例子2: 匹配 `r/t_(\d|i)/|`,转换成 `$t_{[[0]]}$`,即可实现在行内输入 `t_1` 时,转换成 `$t_{1}$` 68 | 69 | 例子3:自定义删除规则,转换前:`r/ ?!?\[\[[^\n\[\]]*\]\]/|`,转换后:`|`,即可实现一键删除光标前的双链 `[[双链内容]]`。 70 | -------------------------------------------------------------------------------- /Doc/EditEnhancements.md: -------------------------------------------------------------------------------- 1 | # Edit Enhancements 2 | 3 | Easy Typing plugin provides various editing enhancements to optimize the user's editing experience. 4 | 5 | ## Main Features 6 | 7 | ### 1. Symbol Auto-pairing/Deletion 8 | 9 | - Automatically completes the right half of a symbol pair when the left half is entered 10 | - Supported symbol pairs include: 【】, (), 《》, "", '', 「」, 『』 11 | - When the cursor is between paired symbols, pressing the delete key will delete the entire symbol pair 12 | 13 | ### 2. Symbol Editing Enhancement for Selected Text 14 | 15 | When text is selected: 16 | - Entering 【 will convert the text to [[text]] 17 | - Entering ¥ will convert the text to $text$ 18 | - Entering · will convert the text to `text` 19 | - Entering other paired symbols (such as 《》, "", '', etc.) will add the corresponding symbols on both sides of the text 20 | 21 | ### 3. Continuous Full-width Symbol to Half-width Conversion 22 | 23 | Continuously entering two full-width symbols will automatically convert them to the corresponding half-width symbols, for example: 24 | - 。。 converts to . 25 | - !! converts to ! 26 | - ;; converts to ; 27 | - ,, converts to , 28 | - :: converts to : 29 | - ?? converts to ? 30 | - 、、 converts to / 31 | - (( converts to () 32 | 33 | ### 4. Obsidian Syntax-related Editing Enhancements 34 | 35 | - Continuously entering two ¥ will become $$, with the cursor positioned in the middle 36 | - Continuously entering two 【 will become [[]] 37 | - Continuously entering · three times will become ``` 38 | 39 | ## Usage 40 | 41 | 1. Enable the corresponding editing enhancement features in the plugin settings 42 | 2. These features will automatically take effect during editing 43 | 44 | ## Notes 45 | 46 | - These features are designed to improve the editing efficiency of Chinese users in Obsidian 47 | - Some features may overlap with other plugins or Obsidian's built-in functions, please adjust according to personal needs 48 | -------------------------------------------------------------------------------- /Doc/EditEnhancements_ZH.md: -------------------------------------------------------------------------------- 1 | # 编辑增强功能 2 | 3 | Easy Typing 插件提供了多种编辑增强功能,以优化用户的编辑体验。 4 | 5 | ## 主要功能 6 | 7 | ### 1. 符号自动配对/删除 8 | 9 | - 输入左半边符号时,自动补全右半边 10 | - 支持的符号对包括: 【】, (), 《》, "", '', 「」, 『』 11 | - 当光标在配对符号之间时,按删除键会删除整个配对符号 12 | 13 | ### 2. 选中文本的符号编辑增强 14 | 15 | 在选中文本的情况下: 16 | - 输入【 将文本转换为 [[文本]] 17 | - 输入¥ 将文本转换为 $文本$ 18 | - 输入· 将文本转换为 `文本` 19 | - 输入其他配对符号(如《》,"",''等)会在文本两侧添加相应符号 20 | 21 | ### 3. 连续全角符号转半角符号 22 | 23 | 连续输入两个全角符号会自动转换为对应的半角符号,例如: 24 | - 。。 转换为 . 25 | - !! 转换为 ! 26 | - ;; 转换为 ; 27 | - ,, 转换为 , 28 | - :: 转换为 : 29 | - ?? 转换为 ? 30 | - 、、 转换为 / 31 | - (( 转换为 () 32 | 33 | ### 4. Obsidian 语法相关的编辑增强 34 | 35 | - 连续输入两个 ¥ 会变成 $$,并将光标定位到中间 36 | - 连续输入两个 【 会变成 [[]] 37 | - 连续输入三次 · 会变成 ``` 38 | 39 | 40 | ## 使用方法 41 | 42 | 1. 在插件设置中开启相应的编辑增强功能 43 | 2. 在编辑过程中,这些功能会自动生效 44 | 45 | ## 注意事项 46 | 47 | - 这些功能旨在提高中文用户在 Obsidian 中的编辑效率 48 | - 部分功能可能与其他插件或 Obsidian 的内置功能有重叠,请根据个人需求进行调整 49 | -------------------------------------------------------------------------------- /Doc/UserDefinedRegExp.md: -------------------------------------------------------------------------------- 1 | # User-defined Regular Expression Blocks 2 | 3 | As part of the text auto-formatting feature, Easy Typing plugin allows users to define special blocks through custom regular expressions and set specific space strategies for these blocks. 4 | 5 | ## Purpose 6 | 7 | 1. Prevent specific content from being formatted 8 | 2. Set special space rules for content in specific formats 9 | 3. Recognize and protect special syntax structures 10 | 11 | ## Syntax 12 | 13 | In the text editing area for custom regular expressions, each line of string is a regular rule, in the following format: 14 | 15 | ``` 16 | | 17 | ``` 18 | 19 | ## Space Strategies 20 | 21 | Three space strategies are represented by three symbols: 22 | - No space requirement (-) 23 | - Soft space (=) 24 | - Strict space (+) 25 | 26 | These strategies align with the space strategies for different blocks in the main auto-formatting feature. 27 | 28 | ## Examples 29 | 30 | 1. Recognizing Obsidian tags: 31 | ``` 32 | #[\u4e00-\u9fa5\w\/]+|++ 33 | ``` 34 | This rule will recognize content starting with `#` followed by Chinese characters, letters, numbers, underscores, or slashes, and require strict spaces on both sides. 35 | 36 | 2. Recognizing network links: 37 | ``` 38 | (file:///|https?://|ftp://|obsidian://|zotero://|www.)[^\s()《》。,,!?;;:""''\)\(\[\]\{\}']+|++ 39 | ``` 40 | This rule will recognize various types of links and require strict spaces on both sides. 41 | 42 | 3. Recognizing Obsidian callout syntax: 43 | ``` 44 | \[\!.*?\][-+]{0,1}|-+ 45 | ``` 46 | This rule will recognize the header of callout syntax to prevent incorrect formatting. 47 | 48 | 4. Recognizing double angle brackets: 49 | ``` 50 | <.*?>|-- 51 | ``` 52 | This rule will recognize double angle bracket blocks to ensure their internal text is not affected by auto-formatting. 53 | 54 | 5. Recognizing numeric time: 55 | ``` 56 | \d{2}:\d{1,2}|++ 57 | ``` 58 | This rule will recognize time text like 12:16, preventing auto-formatting from mistakenly adding spaces. 59 | 60 | ## Usage 61 | 62 | 1. Navigate to the custom regular expression section in the plugin settings 63 | 2. Add your regular expressions following the syntax described above 64 | 3. The plugin will apply these rules during the auto-formatting process 65 | 66 | ## Notes 67 | 68 | - Writing regular expressions requires some basic knowledge 69 | - Overly complex or inefficient regular expressions may affect plugin performance 70 | - From version v5.5.0, comment lines (starting with //) are supported in the custom regular expression area 71 | 72 | For more information on text auto-formatting, please refer to the [Text Auto-formatting](./AutoFormatting.md) document. 73 | -------------------------------------------------------------------------------- /Doc/UserDefinedRegExp_ZH.md: -------------------------------------------------------------------------------- 1 | # 用户自定义正则表达式区块 2 | 3 | 作为文本自动格式化功能的一部分,Easy Typing 插件允许用户通过自定义正则表达式来定义特殊区块,并为这些区块设置特定的空格策略。 4 | 5 | ## 目的 6 | 7 | 1. 防止特定内容被格式化 8 | 2. 为特定格式的内容设置特殊的空格规则 9 | 3. 识别并保护特殊语法结构 10 | 11 | ## 语法 12 | 13 | 在自定义正则表达式的文本编辑区域,每一行字符串都为一个正则规则,其格式如下: 14 | 15 | ``` 16 | <正则表达式>|<左空格策略><右空格策略> 17 | ``` 18 | 19 | ## 空格策略 20 | 21 | 三种空格策略分别用三个符号来表示: 22 | - 不要求空格 (-) 23 | - 软空格 (=) 24 | - 严格空格 (+) 25 | 26 | 这些策略与主要自动格式化功能中不同区块的空格策略相一致。 27 | 28 | ## 示例 29 | 30 | 1. 识别 Obsidian 标签: 31 | ``` 32 | #[\u4e00-\u9fa5\w\/]+|++ 33 | ``` 34 | 这个规则会识别以 `#` 开头,后面跟着中文、字母、数字、下划线或斜杠的内容,并在其左右两侧要求严格空格。 35 | 36 | 2. 识别网络链接: 37 | ``` 38 | (file:///|https?://|ftp://|obsidian://|zotero://|www.)[^\s()《》。,,!?;;:""''\)\(\[\]\{\}']+|++ 39 | ``` 40 | 这个规则会识别各种类型的链接,并在其左右两侧要求严格空格。 41 | 42 | 3. 识别 Obsidian callout 语法: 43 | ``` 44 | \[\!.*?\][-+]{0,1}|-+ 45 | ``` 46 | 这个规则会识别 callout 语法的头部,防止误格式化。 47 | 48 | 4. 识别双尖括号: 49 | ``` 50 | <.*?>|-- 51 | ``` 52 | 这个规则会识别双尖括号块,保证其内部文本不被自动格式化影响。 53 | 54 | 5. 识别数字时间: 55 | ``` 56 | \d{2}:\d{1,2}|++ 57 | ``` 58 | 这个规则会识别如 12:16 这样的时间文本,防止自动格式化导致空格的误添加。 59 | 60 | ## 使用方法 61 | 62 | 1. 在插件设置中找到自定义正则表达式部分 63 | 2. 按照上述语法添加你的正则表达式 64 | 3. 插件会在自动格式化过程中应用这些规则 65 | 66 | ## 注意事项 67 | 68 | - 正则表达式的编写需要一定的基础知识 69 | - 过于复杂或低效的正则表达式可能会影响插件性能 70 | - 从 v5.5.0 版本开始,支持在自定义正则表达式区域使用注释行(以 // 开头) 71 | 72 | 关于文本自动格式化的更多信息,请参阅[文本自动格式化](./AutoFormatting_ZH.md)文档。 73 | 74 | 想要了解正则表达式的使用,可以查看 [《阮一峰:正则表达式简明教程》](https://javascript.ruanyifeng.com/stdlib/regexp.html#) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Obsidian Easy Typing

2 |
3 | 4 | ![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22easy-typing-obsidian%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) ![latest download](https://img.shields.io/github/downloads/Yaozhuwa/easy-typing-obsidian/latest/total?style=plastic) 5 | 6 | [[中文](https://github.com/Yaozhuwa/easy-typing-obsidian/blob/master/README_ZH.md) | English] 7 |
8 | 9 | Easy Typing is an enhancement plugin for [Obsidian](https://obsidian.md) that improves the writing experience. It includes automatic text formatting and symbol editing enhancements during editing. 10 | 11 | ## Core Features 12 | 13 | 1. **Text Auto-formatting**: Capitalizes the first letter and automatically adds spaces between specific parts of each line according to user-defined rules. This feature also includes user-defined regular expression blocks for handling special text formats. 14 | 15 | 2. **Edit Enhancements**: Includes symbol auto-pairing/deletion, symbol editing enhancement for selected text, continuous full-width symbol to half-width conversion, Obsidian syntax-related editing enhancements, Tabout, code block editing enhancements, and Backspace editing enhancements. 16 | 17 | 3. **Customizable Conversion Rules**: Supports user-defined conversion rules for various editing scenarios. 18 | 19 | 4. **Plugin Built-in Commands**: Includes the deletion of extra blank lines, formatting the entire document, formatting the current line/selected area, creating a new line after the current line and jumping (Ctrl+Enter), and toggling comments (supports comments within code blocks, Mod+/). 20 | 21 | 5. **Experimental Features**: Includes some experimental features, including strict line breaks where Enter inputs two line breaks, automatic punctuation correction (only during input, English punctuation between Chinese is converted to Chinese punctuation), etc. 22 | 23 | ## Detailed Documentation 24 | 25 | For detailed information on each feature, please refer to the following documents: 26 | 27 | - [Text Auto-formatting](./Doc/AutoFormatting.md) 28 | - [Edit Enhancements](./Doc/EditEnhancements.md) 29 | - [Customizable Conversion Rules](./Doc/CustomRules.md) 30 | 31 | ## Changelog 32 | 33 | For a full changelog, see [changelog.md](./changelog.md) 34 | 35 | ## Acknowledgements 36 | 37 | - https://github.com/artisticat1/obsidian-latex-suite 38 | - https://github.com/aptend/typing-transformer-obsidian 39 | - https://marcus.se.net/obsidian-plugin-docs/ 40 | 41 | ## Support 42 | 43 | If you like this plugin and want to say thanks, you can buy me a coffee here! 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 |

Obsidian Easy Typing

2 |
3 | 4 | ![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=插件下载量&query=%24%5B%22easy-typing-obsidian%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) ![latest download](https://img.shields.io/github/downloads/Yaozhuwa/easy-typing-obsidian/latest/total?style=plastic) 5 | 6 | [中文 | [English](https://github.com/Yaozhuwa/easy-typing-obsidian)] 7 | 8 |
9 | 10 | Easy Typing 是一个 [Obsidian](https://obsidian.md/) 的书写体验增强插件,功能包含编辑时自动格式化文本和符号编辑增强。自动格式化文本对文档的格式进行规范化,并且美化文档的观感。编辑增强优化用户的编辑体验。 11 | 12 | ## 插件核心功能 13 | 14 | 1. **文本自动格式化**:提供了首字母大写功能,并根据用户设置的规则,在输入过程中对每一行的特定部分自动添加空格。这个功能还包括用户自定义正则表达式区块,用于处理特殊的文本格式。 15 | 16 | 2. **文本编辑增强功能**:包括符号自动配对/删除、选中文本的符号编辑增强、连续全角符号转半角符号、Obsidian 语法相关的编辑增强、Tabout、代码块的编辑增强,删除功能的编辑增强。 17 | 18 | 3. **自定义转换规则**:支持用户自定义转换规则,具有很高的可玩性。 19 | 20 | 4. **插件内置的命令**:包含多余空白行的删除,格式化全文,格式化当前行/选中区域,在当前行后创建新行并跳转(`Ctrl+Enter`),切换注释(支持代码块内的注释, `Mod+/`)。 21 | 22 | 5. **实验性功能**:包含一些实验性功能,包含严格换行时回车将输入两个换行符,标点自动矫正(仅在输入过程中,中文间的英文标点转化成中文的标点)等 23 | 24 | ## 详细文档 25 | 26 | 关于每个功能的详细信息,请参阅以下文档: 27 | 28 | - [文本自动格式化](./Doc/AutoFormatting_ZH.md) 29 | - [编辑增强功能](./Doc/EditEnhancements_ZH.md) 30 | - [自定义转换规则](./Doc/CustomRules_ZH.md) 31 | 32 | ## 更新记录 33 | 34 | 完整的更新记录见 [changelog.md](./changelog.md) 35 | 36 | ## 致谢 37 | 38 | - https://github.com/artisticat1/obsidian-latex-suite 39 | - https://github.com/aptend/typing-transformer-obsidian 40 | - https://marcus.se.net/obsidian-plugin-docs/ 41 | 42 | ## 赞助 43 | 44 | 如果你喜欢这个插件,并对我表示感谢,你可以在这里请我喝一杯奶茶! 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /assets/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yaozhuwa/easy-typing-obsidian/52cdaa97459a1daaf916f9a5637691808b2ec7ac/assets/donate.png -------------------------------------------------------------------------------- /assets/enhance-paste.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yaozhuwa/easy-typing-obsidian/52cdaa97459a1daaf916f9a5637691808b2ec7ac/assets/enhance-paste.gif -------------------------------------------------------------------------------- /assets/multi-cursor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yaozhuwa/easy-typing-obsidian/52cdaa97459a1daaf916f9a5637691808b2ec7ac/assets/multi-cursor.gif -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 Changelog 2 | - V5.5.14 2025-05-07 3 | - 删除控制台输出 4 | - V5.5.13 2025-05-05 5 | - 修复 #184 6 | - CMD+A 增强功能在列表项中也有效了,第一次选中当前列表项,第二次选中当前列表项及其子项,第三次选中整个列表,最后选中全文。 7 | - Bug Fix: Resolved issue #184 8 | - Enhanced functionality for CMD+A now works in list items: the first press selects the current list item, the second press selects the current list item and its sub-items, the third press selects the entire list, and finally selects the entire document. 9 | - V5.5.12 2025-04-09 10 | - 新增功能:标题折叠回车保序:折叠标题后回车不触发展开,直接添加同级标题行。默认关闭,需要在设置中打开。#274 11 | - 修复 #267,#275 12 | - New Feature: Pressing Enter on collapsed headers now adds a same-level header line without expanding (disabled by default, enable in settings) #274 13 | - Bug Fixes: Resolved folding interaction issues #267 and #275 14 | - V5.5.11 2024-12-20 15 | - 基础编辑增强:支持粘贴多行内容到列表或者引用块时,自动添加列表或者引用前缀 #262, #263 16 | - 基础编辑增强:支持粘贴多级列表项内容到列表 17 | - 修复:修复 CMD+Enter 新建多级引用行时错误添加空格的问题 18 | - Enhanced basic editing: Automatically add list or quote prefixes when pasting multiple lines of content into lists or quote blocks #262, #263 19 | - Enhanced basic editing: Support pasting multi-level list items into lists 20 | - Fix: Fixed the issue of incorrectly adding spaces when creating multi-level quote lines with CMD+Enter 21 | 22 | ![](/assets/enhance-paste.gif) 23 | 24 | - V5.5.10 2024-12-01 25 | - CMD/Ctrl+A 增强功能支持quote/callout,第一次选中当前引用行内容,第二次选中当前的整个引用 #260 26 | - 切换代码块注释命令 27 | - 修复对 CSS 代码块注释错误的问题 #259 28 | - 增加了对 HTML、Markdown、Matlab 代码块注释的支持 29 | - 优化该命令在空的代码行的表现 30 | - 修复 MD 链接内部有小括号时会被错误格式化的问题 #178 31 | - Enhanced functionality for CMD/Ctrl+A to support quote/callout, the first press selects the current quote line content, the second press selects the entire current quote #260 32 | - Toggle code block comment command 33 | - Fixed the issue of incorrect comments in CSS code blocks #259 34 | - Added support for comments in HTML, Markdown, and Matlab code blocks 35 | - Optimized the performance of this command on empty code lines 36 | - Fixed the issue where MD links with parentheses inside would be incorrectly formatted #178 37 | - V5.5.9 2024-11-23 38 | - 严格换行模式支持三种模式(两次换行,两次空格+换行,混合模式)#193 39 | - 双空格模式:回车会变成两次空格+换行 40 | - 混合模式是在引用块中使用两次空格+换行,在其他地方使用两次换行 41 | - 修复了列表中代码块结尾回车时,触发两次换行的问题 42 | - 增加了选中当前文本块的快捷键命令 #256 43 | - 修改了选中文本块的逻辑,目前不需要严格换行模式也把相邻的文本行作为同一文本块的内容 #255。 44 | - V5.5.8 2024-11-19 45 | - 紧急修复上次更新导致的一个输入问题 46 | - Urgently fixed an input issue caused by the last update 47 | - V5.5.7 2024-11-19 48 | - 严格换行两次支持 quote 块 #254 49 | - 增加了引用符号 > 与文本之间自动空格的功能,默认开启,可以在设置中关闭 50 | - 优化了在基础输入增强中,在句首输入 》或者 > 的行为 51 | - Added support for strict line breaks twice in quote blocks #254 52 | - Added the function of automatically inserting a space between the quote symbol > and the text, enabled by default, can be turned off in the settings 53 | - Optimized the behavior of inputting 》or > at the beginning of a sentence in basic input enhancement 54 | - V5.5.6 2024-11-14 55 | - 增加了 Ctrl/Cmd+A 增强功能,第一次选中当前行,第二次选中当前文本块,第三次选中全文 #255 56 | - (目前仅在纯文本块中生效,不包括引用和列表) 57 | - 需要再设置-实验性功能中打开该功能选项后生效 58 | - Added enhanced functionality for Ctrl/Cmd+A: the first press selects the current line, the second press selects the current text block, and the third press selects the entire text #255 59 | - (Currently only effective in plain text blocks, excluding quotes and lists) 60 | - Need to open the option in the Experimental Features settings to take effect 61 | - V5.5.5 2024-11-03 62 | - 优化了列表下代码块内空行时 Enter 的处理 63 | - 跳转到新建行功能支持 Task 64 | - 优化 Ctrl/Cmd+A 选中代码块的功能,第一次会选中代码内容,第二次会选中整个代码块(包含 \`\`\`) 65 | - 修复了 Debug 信息的输出 66 | - Optimized the handling of Enter in code blocks under lists when there are empty lines 67 | - The function of jumping to a new line supports Task 68 | - Optimized the function of selecting code blocks with Ctrl/Cmd+A, the first time will select the code content, the second time will select the entire code block (including \`\`\`) 69 | - Fixed the output of Debug information 70 | - V5.5.4 2024年10月27日 71 | - 重构文档,使文档更简洁清晰 72 | - 增强删除功能添加功能开关,默认开启,可以到设置中关闭。 73 | - 增强删除功能:删除有序列表时,更新后续列表序号 74 | - 严格换行两次:支持代码块后回车换行两次。 75 | - V5.5.3 2024年10月26日 76 | - 修复了代码块内删除空行时,删除列表代码块的 Bug 77 | - V5.5.2 2024-10-26 增加了几个新的提升编辑体验的功能 78 | - 优化了空的列表项和引用项的删除功能,可以自行体验。 79 | - #252 添加了快捷键命令:跳转到新建行,默认快捷方式是 CMD/Ctrl+Enter,会与 Obsidian快捷键冲突,需要自己设置 80 | - #243 优化了代码块的粘贴功能,去除了冗余的智能缩进 81 | - #222 增加了切换注释命令(CMD/Ctrl+/),可以切换代码块和普通文本的注释(目前支持部分常用语言的代码块的注释),与Obsidian内置的注释功能快捷键冲突,需要自己设置快捷键。 82 | - 目前支持如下语言: 83 | - 'js': '//', 84 | - 'javascript': '//', 85 | - 'ts': '//', 86 | - 'typescript': '//', 87 | - 'py': '#', 88 | - 'python': '#', 89 | - 'rb': '#', 90 | - 'ruby': '#', 91 | - 'java': '//', 92 | - 'c': '//', 93 | - 'cpp': '//', 94 | - 'cs': '//', 95 | - 'go': '//', 96 | - 'rust': '//', 97 | - 'swift': '//', 98 | - 'kotlin': '//', 99 | - 'php': '//', 100 | - 'css': '//', 101 | - 'scss': '//', 102 | - 'sql': '--', 103 | - 'shell': '#', 104 | - 'bash': '#', 105 | - 'powershell': '#', 106 | - V5.5.1 2024-09-15 Tabout 可以跳出行内成对符号包裹的文本 107 | - 增强 Tabout 功能,支持跳出行内成对符号包裹的文本,支持的符号对有: 108 | - ["【|】", "(|)", "《|》", "“|”", "‘|’", 109 | "「|」", "『|』", "'|'", "\"|\"", "$$|$$", '$|$', '__|__', '_|_', 110 | "==|==", "~~|~~", "**|**", '*|*', "[[|]]", '[|]',"{|}", "(|)", "<|>"]; 111 | - 选中文本替换增强添加了对 `「『` 符号的支持 112 | - V5.5.0 2024-07-28 自定义规则支持正则表达式匹配及多光标跳转,多语言支持 113 | - 重大更新(破坏性更新) 114 | - 自定义规则支持多光标跳转(参考了 Latex-Suite 代码),结合v5.4.0版本的更新,目前的自定义删除/转化规则支持正则表达式匹配及多光标跳转 115 | - 转化后字符串 116 | - 多光标用 `$n` 表示,n 为从 0 开始的整数,也可以使用 `${n: 文本}` 来表示文本被选中,Tab 键可以跳转到下一个光标 117 | - 自定义规则的正则匹配语法: `r/正则表达式1/|r/正则表达式2/`,这是光标左右都是正则表达式匹配的情况。 118 | - 也可以只有一边是正则表达式匹配,另一边是之前的匹配如: `!|r/\[\[.+?\]\]/`,可以匹配在 wiki 链接前输入全角叹号 `!` 的情况。 119 | - !! 不同于 V5.4.0 版本,现在不再用 `$n` 来引用正则表达式捕获的匹配组的内容,而是使用 `[[n]]` 来引用 120 | - `[[n]]`,匹配成功的第 `n` 组内容,`n` 是从 0 开始的自然数。 121 | - 对于 正则表达式 2 匹配的第 m 组内容,设置为 `[[m+n]]`,n 为第一个正则表达式匹配的组的数量。 122 | 例子:自定义转换规则: 匹配 `r/(?<=^|\n)([\w-]+)-call/|`, 转换成 `> [![[0]]] $0\n> $1`,即可实现在行首输入 `note-call`,即可转换成 123 | ``` 124 | > [!note] $0 125 | > $1 126 | ``` 127 | 其中, $0, $1 表示不同的两个光标,可以通过 Tab 键从 $0 跳转到 $1 128 | ![](/assets/multi-cursor.gif) 129 | 130 | - 其他更新 131 | - 增加多语言支持,目前支持了简体中文\繁体中文\英文\俄文, #234, Thanks to [niazlv](https://github.com/niazlv) 132 | - 修复自定义删除/转换规则 中的转义符无法转义 #240 133 | - 优化链接根据别名智能空格的效果 #233 134 | - 自定义正则表达式区域支持注释行, 以 // 开头的行被视为注释 #173 135 | 136 | - V5.4.1 2024-06-30 小 Bug 修复与严格换行时回车的优化 137 | - 优化严格换行回车产生两个换行符的功能 138 | - 光标在行首且上一行与当前行非空的情况回车不处理。 139 | - 如下一行非空白行,不做处理 140 | - 修复 getDefaultIndentChar 在默认 Ob 设置下的会返回错误结果的问题, #229 #230 #231 141 | - V5.4.0 2024-06-05 自定义规则支持正则表达式! 142 | - 转换前的匹配语法: `r/正则表达式1/|r/正则表达式2/`,这是光标左右都是正则表达式匹配的情况。 143 | - 也可以只有一边是正则表达式匹配,另一边是之前的匹配如: `!|r/\[\[.+?\]\]/`,可以匹配在 wiki 链接前输入全角叹号 `!` 的情况。 144 | - 对于转换后的字符串,在以前的功能基础上,还可以引用正则表达式匹配内的匹配组的内容(一般是正则表达式内小括号部分匹配成功的内容) 145 | - $n:匹配成功的第 `n` 组内容,`n` 是从 0 开始的自然数。 146 | - 对于 正则表达式 2 匹配的第 m 组内容,设置为 $(m+n),n 为第一个正则表达式匹配的组的数量。 147 | 148 | 例如:自定义转换规则,匹配 `r/(?<=^|\n)(\w+)-call/|`,转换成 `> [!$0]\n> |`,可以实现在文章行首输入 `note-call`,即可转换成 149 | 150 | ``` 151 | > [!note] 152 | > | 153 | ``` 154 | 155 | 而在行首输入 `tip-call` 时,自动转换成 156 | 157 | ``` 158 | > [!tip] 159 | > | 160 | ``` 161 | 162 | 自定义删除规则匹配 `r/> \[![\w\d]+\].*?\n> /|`,即可实现快速删除空的 callout 块,如在如下情况下按删除会将整个 callout 全部删除 163 | 164 | ``` 165 | > [!note] 166 | > | 167 | ``` 168 | 169 | - V5.3.4 2024-06-04 自定义删除/转换规则支持转义字符 #225 170 | - 可以在自定义删除/转换规则中使用转义字符 `\`,如 `\n` 是换行符。(支持`\n \r \t`) 171 | - 输入 `\|` 时不会被当作光标,转换后也只输出 `|` 的结果 172 | - V5.3.3 2024-05-05 一些功能修复 173 | - 修复代码块粘贴某些情况智能缩进错误的问题 174 | - 修复中文输入法 IME 下,用户自定义规则在代码块中失效的问题 175 | - 优化代码块识别的功能,修复 CMD+A 某些情况的不精确选择问题 176 | - 修复 triggerPuncRectify 误触发导致的错误 177 | - 不再在时间戳左边添加软空格 #223 178 | - 使用 [obsidian-typings - npm package](https://www.npmjs.com/package/obsidian-typings) 来使用未公开的 Obsidian API。 179 | - V5.3.2 2024-04-24 Small Fix 180 | - 修复:中文输入法下某些情况输入文字时转换规则不生效的问题 #221 181 | - V5.3.1 2024-04-24 增加代码块编辑体验增强功能 182 | - 新功能:代码块编辑增强(可以在设置中切换关闭/打开) 183 | - 在代码块内,Cmd/Ctrl+A 会选中代码块 184 | - 增强代码块内的粘贴,会智能地缩进与删除多余空白符号 185 | - 增强列表下代码块内的删除按键,使光标始终在代码块有效区域 186 | - 增强代码块内Tab键的效果,Tab缩进的效果与编辑器设置中的使用制表符对应 187 | - 列表下代码块的创建和删除(此项始终开启,与设置开关无关) 188 | - 修复:中文输入法下如果回车或者数字键输入文字时转换规则不生效的问题 #221 189 | - V5.3.0 2024-04-16 新的输入法检测方式 190 | - 采用新的输入法输入检测方式,可能能适配更多输入法 191 | - 修复 normal-paste #218 192 | - 修复代码块快速删除 193 | - V5.2.3 2024-03-29 代码块编辑的一系列优化 194 | - 优化列表中空代码块的快速删除 195 | - 优化列表中代码块的快速创建(连按三次\`) 196 | - 优化代码块中 `Tab` 键的功能,会根据Obsidian编辑器设置中的使用制表符设置,插入制表符或者空格 197 | - 优化代码块中粘贴代码的表现,会智能判断缩进 198 | - 优化表格内编辑的格式化处理代码,更简洁 199 | - 解决有时候文档末尾创建新行时报错的问题 200 | - v5.2.2 2024-03-17 201 | - 中英文间自动空格支持撤销(Ctrl+Z), 撤销后继续输入不会影响原内容。 202 | - 以回车开头的自定义转换规则也会在文档首行生效 203 | - 解决删除多余空白行和格式化全文时解析语法树可能不完整的问题。 204 | - 修复格式化当前行的功能有时候只格式化行的前面部分内容的问题。 205 | - 修复标点与中文间空格有时候失效的问题 206 | - v5.2.1 2024-03-10 207 | - 由于支持旧版输入法可能会导致其他输入法冲突,故为支持旧版输入法的功能添加了开关,默认为关闭。#202 208 | - 修复文字后不加空格输入`'`时会错误自动配对的问题。 #201 209 | - v5.2.0 2024-02-28 210 | - 支持微软输入法 211 | - 解决表格编辑时有时变为非聚焦状态的问题 212 | - 标点配对功能默认增加了对英文标点的自动配对(相比与Obsidian内置的标点配对,本插件的配对输入和删除可以在表格编辑时生效) 213 | - v5.1.16 2024-02-16 214 | - 支持latex公式`$\qquad$` 以及 `
`,他们前后不会添加空格 (#162,#195) 215 | - 调整自定义正则空格策略(-)的优先级到最高 216 | - 优化列表下的代码块创建和删除 217 | - 修改了表格内包含
时格式化的一个小bug 218 | - v5.1.15 2024-01-27 219 | - 支持新版本 Obsidian1.5.3 表格内编辑的自动格式化,#189 220 | - 自定义选中文本增强增加了对……和——触发的支持。#191 221 | - v5.1.14 2023-12-28 222 | - 新增实验性功能,可以修复 Obsidian 在 MacOS 中右键呼出右键菜单时光标会自动跳转到下一行的问题。修改该设置需要重启 Obsidian 生效 223 | - v5.1.13 2023-12-28 224 | - fix #183, callout 内的代码块和非代码块区域能正确识别了 225 | - v5.1.12 2023-12-11 226 | - Tabout 功能增加了选中文本被成对符号包裹时,按Tab会使光标直接跳出到符号的右边,具体成对符号为 `["【|】", "(|)", "《|》", "“|”", "‘|’", "「|」", "『|』", "'|'", "\"|\"", "$$|$$", '$|$', '__|__', '_|_', "==|==", "~~|~~", "**|**", '*|*', "[[|]]", '[|]',"{|}", "(|)", "<|>"]` 227 | - 支持 callout 中的代码块的识别,现在在 callout 代码块中输入不会导致错误的格式化了。 #151,感谢 [HoBeedzc](https://github.com/HoBeedzc) 提供的思路 228 | - 修复了对于单引号连续全角转半角功能失效的问题 #169 229 | - 修复了在某些输入法下,自定义规则对输入;的情况失效的问题 #169 230 | - v5.1.11 2023-11-26 231 | - 连续全角转半角的功能:增加了连续输入两个全角竖线转半角竖线的规则 (#145, #113) 232 | - 基础输入增强:增加`!【【|`转`![[|]]`,修复了某些`¥¥|`不能转`$|$`的问题(win和Mac符号不同)。(#172,#170) 233 | - 现在以更好的方式读取obsidian的`strict line`设置,应该可以在移动端也可以识别~ (#133) 234 | - 修复了句子中某些分段后面的半角标点与后面的文本不空格的问题 235 | - 修复无别名当前页面链接的智能空格bug(#142) 236 | - merge pull request to fix formatArticle method fail because of hooking viewUpdate (#175, thanks to [eventlOwOp](https://github.com/eventlOwOp)) 237 | - v5.1.10 2023-07-09 238 | - 增加了命令:无格式化粘贴,默认快捷键 Ctrl(CMD)+Shift+V,在粘贴的时候不会触发自动格式化文本。 239 | - 优化了粘贴文本时自动格式化的功能 240 | - 修复了在白板中,粘贴多行文本会重复粘贴的 Bug. 241 | - 修复了粘贴时格式化数字和中文间空格的设置不生效的 Bug。 242 | - Added command: "Paste without formatting" with the default shortcut Ctrl(CMD)+Shift+V, which allows pasting text without triggering automatic text formatting. 243 | - Optimized automatic text formatting when pasting: 244 | - Fixed a bug where pasting multiple lines of text in the whiteboard would result in duplicate pasting. 245 | - Fixed a bug where the setting to format spaces between numbers and Chinese characters was not applied when pasting. 246 | - v5.1.9 2023-02-23 247 | - 增加了实验性功能 248 | - 仅在输入过程中,中文间的英文标点(,.?!)自动转换为全角(可撤销)。该功能默认关闭,需要到设置中打开。 249 | - 一些改动 250 | - 将连续顿号转`/`的设定从默认的连续全角转半角功能中删除,需要该功能可通过设置中的自定义转换规则实现。 #127 251 | - 适配了前面带两个!的MD链接和 wiki 链接,为了适配Make.MD插件的 flow editor. 252 | - v5.1.8 2023-02-11 253 | - Bug fix 254 | - fix cursor move when use command to format article. #126 255 | - fix english number space settings not work. 256 | - fix english number space and chinese number space sometimes not work. 257 | - v5.1.7 2023-02-09 258 | - New Feature 259 | - Add Experimental Feature: Strict Line breaks Mode Enter Twice (when you turn on the strict line breaks in editor settings, one enter will produce two `\n`) 增加了实验性功能:在编辑器设置了严格换行的情况下,在普通文本行使用回车键会产生两个换行符。 260 | - Add Experimental Feature: Enhance Chinese Input Method (Which is designed to fix the issue #125)增加了实验性功能:中文输入法下输入英文,回车让英文上屏时自动格式化。 261 | - The experimental functionality needs to be updated and turned on in the plug-in settings, with the default off. 实验性功能需要更新后在插件设置中打开,默认为关闭。 262 | - v5.1.6 2023-02-08 263 | - 插件命令正式支持中文,会根据数字OB设置的语言变化。 #116 264 | - 数字间的逗号和点号不再触发添加空格。 #121 265 | - ios端粘贴文本不会触发自动格式化了, #124 266 | - 设置中添加了数字和中文,数字和英文的空格选项 #117 267 | - 格式化全文、格式化选中文本产生的变化被整合到一个transaction,可以一次撤销。 268 | - v5.1.5 2023-01-27 269 | - Bug fix 270 | - fix #119。修复选中文本后输入两次$时的结果与期望不同 271 | - Improvement 272 | - add chinese command name. 插件命令添加中文描述 273 | - improve delete blank line command for blockid 删除空白行不会删除blockid后的空白行了。 274 | - changed 275 | - no longger add soft space between timestamp and text. 自动格式化不再要求文本和识别的时间戳之间有软空格。 276 | - v5.1.4 2023-01-18 277 | - Bug fix 278 | - Fix #114 279 | - Improve 280 | - 重新安装插件后无需重启软件即可生效。After reinstalling the plug-in, it will take effect without restarting the software. 281 | - Changed 282 | - 空格后面及句首的符号`.`不再被认为是句子的结束。(相当于不会错误地将文本中的拓展名如 `.txt` 自动格式化成 `. txt`,需要`.`前面有空格)The sign `.` after a space or at the beginning of a sentence is no longer considered the end of a sentence. (That is to say this plugin will not accidentally format the extension name in the text such as `.txt` to `. Txt` automatically, requiring `.` to be preceded by a space) 283 | - v5.1.3 2023-01-10 284 | - Bug fix 285 | - Fix #107 286 | - v5.1.2 2022-12-29 287 | - Bug Fix 288 | - Fix dummy+soft link space strategy not work as expected 289 | - Improve timestamp recognize, now support 12:12:12, 1:22 ... 290 | - Changes 291 | - bad regexp notice now only appear when debug switch is on. 292 | - v5.1.1 2022-12-12 293 | - Bug Fix 294 | - 修复了当前文件路径不更新导致的检查是否为排除文件总是返回真的问题。#105 295 | - v5.1.0 296 | - New Feature 297 | - 增加了Tabout功能,可以在光标在行内代码中时按Tab跳出行内代码。Tabout inline code. 298 | - Improve 299 | - 修复了公式块在不规范书写的时候可能会导致后续部分自动格式化失效的问题。Fixed an issue where the automatic formatting could fail when the formula blocks were not properly written. 300 | - 用 syntaxTree 模块完全代替了自己的 ArticleParser 文章内容解析器。应该会带来性能的提升。The syntaxTree module completely replaces my own ArticleParser(article content parser). There should be a performance boost. 301 | - Other 302 | - 增加了 buymeacoffee 赞助链接. Add a fundingUrl of buymeacoffee. 303 | - v5.0.12 2022.12.07 304 | - Improvement 305 | - 增强了一键去除空格命令(Easy Typing: Delete blank lines of the selected area or whole article)的逻辑,不再去除列表、callout后面的第一个空白行,以及水平线前的第一个空白行。(enhance command:delete blank line. no longer delete first blank line after list, task, callout and blank line exact before hr) 306 | - v5.0.10 2022.11.07 307 | - bug fix 308 | - 修复转换规则在某些输入法下对中文符号 —— 不生效的 Bug。#96 309 | - enhancement 310 | - 增加行开头为有引号 “ 的文本的首字母大写。#97 311 | - v5.0.9 2022.10.29 312 | - Bug Fix 313 | - 修复回车换行时的自动格式化在该行是列表或Task时失效的问题 314 | - 修复链接右边的智能空格不正确的问题 315 | - v5.0.8 2022.10.29 316 | - Bug fix 317 | - 修复在关闭自动格式化时粘贴文本还会触发自动格式化的 Bug。Fix paste auto format when autoformat switch off 318 | - 尝试修复在某些输入法下,转换规则在输入全角字符时不生效的 Bug。Try to fix convert rule not work when input fullwidth symbol. 319 | - Inprovement 320 | - 现在选中替换规则的左右字符串可以为空。Now Selection Replace Rule's left and right char can be left blank. 321 | - V5.0.7 2022.10.16 322 | - 结合 @codemirror/language SyntaxTree 来解析行,行解析更准确。 #57 #84。 parser line with help of @codemirror/language SyntaxTree, make it more robust. 323 | - V5.0.6 2022.10.15 324 | - 修复上个版本的小更新导致的和 Latex suite 冲突的问题。 325 | - 修改一些设置面板的描述,和文档更新。 326 | - V5.0.5 2022.10.14 327 | - 自动格式化会忽略 frontmatter 区域。 autoformat ignore frontmatter 328 | - 设置中增加自动格式化忽略文件、文件夹。support exclude folder/files for autoformat 329 | - 增加粘贴内容时的自动格式化。support autoformat when paste. 330 | - 增加删除多余空白行的命令(选中文本情况下只删除选中区域的空白行,未选中文本情况下对全文删除空白行)。add command to delete blank lines 331 | - <> 符号不再自动配对,如有需要可以在设置自定义规则实现。 332 | - V5.0.4 2022.10.10 333 | - fix bug which cause conflict with obsidian-latex-suite 334 | - V5.0.3 2022.10.05 335 | - 增加缩写的识别,缩写部分不被格式化(#48, #5)。 缩写指的是符合正则表达式 `/([a-zA-Z]\.)+/` 的部分,如 i.e. 336 | - 修复代码区块和公式区块类型改变时却没触发文档重解析的 bug。 337 | - V5.0.1~V5.0.2 2022.10.03 338 | - 现在 * ~ 都会被认为是软空格. fix #41, now ~ * are considered as soft space 339 | - 自定义转换规则支持换行. User defined conversion rules now support line breaking. 340 | - V5.0.0 2022.09.26 341 | - 重构了代码框架,使用新的接口重新实现了之前的所有功能,大大提升了本插件的性能及可拓展性,并且添加了一些新的功能。 342 | - 新功能与改进 343 | - **增加了对移动端的支持**! 344 | - **取消了行模式**,得益于新接口的使用,现在本插件能更好地识别出中文输入法的结束,无需行模式也不再会有之前的输入错乱的 Bug。现在,插件在每次中文输入结束和英文字符输入的时候进行文本的格式化。 345 | - **增强了符号自动配对及增加了配对符号的快速删除功能**,光标在配对的符号之间时,按删除键会将整个配对符号全部删除,如在《|》时按删除键,会直接将两个书名号全部删除。支持更多符号对如:`“”`、`$$`、`()` 等。 346 | - 细化了对符号输入增强的功能分类并分别设置了开关:1. 符号自动配对/删除;2. 选中文本的符号编辑增强;3. 连续全角符号转半角符号;4. Obsidian 语法相关的编辑增强。详见 readme 文档。 347 | - **增加了自定义编辑转换规则**,支持自定义选中文本、退格键删除文本以及打字时三种情况下的文本转换规则。(参考了 [aptend/typing-transformer-obsidian](https://github.com/aptend/typing-transformer-obsidian)) 348 | - **增加了不同区块空格策略的设定**,三种空格策略:1. 无要求;2. 软空格;3. 严格空格。软空格指当前区块可以与其他区块以标点符号分割(如 `$formula-block$,文本区块` 中公式区块和文本区块以逗号分割,该逗号就算软空格),严格空格指该区块和其他区块之间必须有空格符号分割。 349 | - 现在,**每个自定义区块左右两边的空格策略都可以单独设置**,大大增强了正则区块的实用性和可玩性。详见 readme 文档。 350 | - 增加了一个插入代码块的命令: "insert code block w/wo selection"。可以在选中和未选中文本的情况下自适应地插入代码块语法(为了我自己方便) 351 | - **提升了性能**。 352 | - 功能变化 353 | - 由于使用 CodeMirror 6 API,**不再支持 Legacy Editor**。 354 | - 致谢 355 | - 感谢插件 [aptend/typing-transformer-obsidian](https://github.com/aptend/typing-transformer-obsidian) ,通过该插件了解了CodeMirror6 的相关 API 的使用。以及该插件通用转换规则的思路也对本插件有所启发。 356 | - V4.0.8 2022.09.05 357 | - Bug Fix 358 | - Auto-capitalization now work for cyrillic symbols #65 359 | - 调整了中文引号的自动补全 #64 360 | - 调整引号和其他区块的自动空格的特性。 #63 361 | - V4.0.7 2022.08.23 362 | - Bug Fix 363 | - Fix Smart Space between text and link. #60 364 | - V4.0.6 2022.04.27 365 | - Improvement 366 | - improve action when press 3 contiguous `·`。连续三次`·`的处理得到增强 367 | - Bug fix 368 | - fix press `{` when something selected will cause duplication. 修复在选中文本时按`{`会产生重复文本的问题。 #40 369 | - V4.0.5 2022.04.06 370 | - Improvement 371 | - 全角字符增强功能:增加了中文括号、书名号、引号的自动配对输入。 372 | - 优化了插件设置面板 373 | - V4.0.4 2022.03.27 374 | - Bug fix 375 | - 修复在 Ubuntu 下,全角增强开启下两次`:`不会变成`:`的 bug。 376 | - V4.0.3 2022.03.27 377 | - New Feature 378 | - 新增了行模式:只在一行输入结束,回车创建新行的时候,对该行进行格式化。可以在插件设置中打开(Add LineMode: Only formatting when line end. need to be activated in setting pane) 379 | - Improvement 380 | - 增加了格式化全文的命令(add command to foramt the whole article) 381 | - format-selection 命令在没有选中文本的情况下,格式化当前行。(format-selection command will format current line when there is no selection) 382 | - Bug fix 383 | - 修复了链接后面错误添加空格的 bug。(fix bug: sometime it mistakenly add space after link) 384 | - 修复全角增强功能在 linux 下两次 `;`不会转换成 `;` 的 bug 385 | - Others 386 | - 建议在插件设置中的正则表达式设置中添加两行正则: 387 | - `\[\!.*?\][+-]{0,1}` 用于排除对 Obsidian 0.14 版本后新增的 callout 类型的格式化 388 | - `<.*?>` 用于排除 Templater 插件特定语法的格式化。 389 | - V4.0.1 2022.02.21 390 | - Improvement 391 | - 全角字符增强,增加了对Mac系统下两次中文竖线变英文竖线的功能 #24 392 | - 全角字符增强,调整增强了对 》 以及 $ 的操作 393 | - V4.0.0 2022.02.09 394 | - Improvement 395 | - **同时支持 live preview 模式 (Support Live preview) 和 legacy editor** 396 | - 优化文本解析的时机和解析的范围,提升性能;在切换文档的时候重新解析文档。 397 | - 支持 Admonition 代码块内部文本自动格式化 398 | - 增加命令:格式化选中的文本 399 | - 增强了 `链接与文本智能空格` 的功能 400 | - 大大增强了 `全角字符输入增强/辅助` 功能 401 | - 回车后,对上一行文本进行格式化 402 | - Bug fix 403 | - 解决了某些情况下,格式化行时最终光标计算错误的 Bug。 404 | - 修复了解析全文时,在某些情况下在全文最后会多计算一行 undefined 的 Bug 405 | 406 | --- 407 | - v3.4.3 2021.11.18 408 | - Bug fix 409 | - fix that two successive `:` will be converted to `:` not working in ubuntu; 410 | - v3.4.2 2021.11.18 411 | - New feature 412 | - Add new feature to full-width symbol enhancement. Two successive `:` will be converted to `:`; two successive `》` will be converted to `>`; two successive `。` will be converted to `.`; two successive `、` will be converted to `/`; two successive `(` will be converted to `()`, and the cursor will be in the middle. It only works when you input, and it won't affect the existing context. 为全角符号增强添加新功能。 两个连续的`:`将被转换为`:`; 两个连续的`》`将被转换为`>`; 两个连续的`。`将被转换为`.`; 两个连续的`、`将被转换为`/`; 两个连续的`(`会被转换成`()`,光标会在中间。该功能只在你输入的时候触发,不会影响已经存在的文本。 413 | - v3.4.1 2021.11.17 414 | - Improvement 415 | - Be compatible with plugins: Obsidian Emoji Shortcodes. 与插件 Obsidian Emoji Shortcodes 兼容。 416 | - v3.4.0 2021.10.23 417 | - New feature 418 | - 全角字符增强功能增加了行首的`》`自动转换成`>`,行首的`、`自动转换成`/`(为了配合核心插件slash commands)。The full-width character enhancement has new feature: The `》` at the beginning of the line is automatically converted to `>`, and the `and` at the beginning of the line are automatically converted to `/` (in order to cooperate with the core plug-in slash commands) #17 419 | - Bug fix 420 | - 修复了鼠标点击到新的空白行首再输入时,句首字母大写失效的问题。Fixed an issue where capitalizing the first letter of a sentence would not work when clicking on the beginning of a blank line 421 | - v3.3.5 2021.10.20 422 | - Bug fix 423 | - Try to fix #17 which happened on MacOS: 2 `¥` can't convert to `$$`。 424 | - v3.3.4 2021.9.29 425 | - Improvement 426 | - 提升了对链接的识别,不带`https://` 的 www.开头的链接也能识别。Enhance the recognization for link, link not begin with `https://` but begin with `www.` can be recognized. 427 | - Changes 428 | - 全角字符增强的功能不再将中文省略号(中文输入法下`shift+6`)自动转化成`^`。The enhanced function of full-width characters no longer automatically converts Chinese ellipsis (`shift+6` under Chinese input method) into `^`. 429 | - v3.3.3 2021.6.22 430 | - Change 431 | - 移除了分号后面的首字母大写,英文用法中分号后面不用大写。Remove capitalization after ";", for I had midsunderstood the use of ";" in English. 432 | - v3.3.2 2021.6.20 433 | - bug fix 434 | - 修复了首字母大写在某些情况失效的bug. Fixed a bug where initial capitalization failed in some cases. 435 | - v3.3.0 2021.6.19 436 | - Shinny new things 437 | - 首字母大写可撤销 Feature of capitalizing the first letter of every sentence is revocable now. 438 | - 增加了[[wikilink]]的智能空格选项,可以根据其上下文决定是否加空格。 Add setting for smart spacing for [[wikilink]], which decide whether to add spaces from the context. 439 | - Bug fix 440 | - 解决了之前格式化一行文本的命令不生效的bug。Fixed a bug where formatting a line of text doesn't work 441 | - v3.2.2 2021.6.16 442 | - Improvement 443 | - 增加了对错误的自定义正则的处理,修复了空字符串的正则导致软件卡死的bug, 正则表达式功能应该没问题了。 444 | - 修改了formatLine的方式,只对一行中需要格式化的部分进行replaceRange,解决了一行中如果有%`-时每次格式化都需要重新解析全文的问题,提升了性能。 445 | - 全角字符增强的功能增加了输入……(shift+6)转换为^的功能。issue #10. 446 | - 修改了很多if-else,改成switch,略微提升性能 447 | - v3.2.1 2021.6.10 448 | - Improvement 449 | - 增加了连续三次键入·,会变成\`\`\` 450 | - v3.2.0 2021.6.10 451 | - Shiny new things 452 | - 增加了用户自定义正则表达式的功能,对正则表达式选取的部分不进行格式化。如`:\w*:`将识别emoji,并不会格式化其内部。`{{.*?}}`将识别双花括号部分,并对其内部不格式化。 453 | - 还可以选择自定义区块和其他文本区块之间空格。 454 | - Change 455 | - 合并了之前的 wikilink, mdlink, barelink,统称为link,相关的自动空格开关也合并为1个。 456 | - Others 457 | - 部分代码重新命名/注释,更加清晰 458 | - v3.1.9 2021.6.9 459 | - Bug fix 460 | - 修复 v3.1.8 全角输入增强对 Mac 版本 obsidian无效的问题 461 | - v3.1.8 2021.6.9 462 | - Improvement 463 | - 增加了全角符号输入增强,现在连续两个¥¥会变成$$,并将光标定位到中间,输入两个【会变成`[[cursor]]`,同理输入两个`·`会变成\`cursor\` 464 | - 重写了splitLine函数,改善逻辑,增加可维护性,并支持行内`$$block formula$$`的识别。 465 | - readme 增加了对插件设置面板的说明 466 | - v3.1.7 2021.6.7 467 | - Improvement 468 | - selectFormat 增加了选中文本时按【则在文本两边增加`[]`的效果. 469 | - 现在选中文本再【或者¥后,文本还保持选中 470 | - list或者task内部开头的整行公式(如`$$x+y=z$$`)现在也能被识别,不会导致错误的格式化。 471 | - v3.1.6 2021.5.28 472 | - Improvement 473 | - 在设置项增加了Debug,可以在控制台输出调试信息,方便调试。 474 | - Bug fix 475 | - 修复光标定位新行时,prevCursor没更新导致的bug。 476 | - v3.1.5 2021.5.28 477 | - Bug fix 478 | - 修复了在text类型的行编辑后,光标定位到codeblock内编辑情况下,format会对codeblock起作用的bug。 479 | - v3.1.4 2021.5.26 480 | - Improvement 481 | - Reduce computatio: 不再在每次按键输入时解析全文,分析段的类型,而是在文本行数变化,或者在本文增减 `$- 这几个符号的时候重新解析全文的行类型。 482 | - Effect change: 修改了数字空格选项的逻辑,不再在数字与字母之间空格。 483 | - Bugs fix 484 | - set `manifest.json`: `"isDesktopOnly": true`, Since I'm using the CodeMirror 5 API 485 | - remove unused imports 486 | - unhook events in `onunload()` 487 | - v3.1.3 2021.5.16 488 | - bug fix 489 | - [x] 修复了标题首字母大写不生效的bug 490 | - [x] 修复了`[https://xxx]()` mdlink中方括号内部两边会自动生成空格的问题。 491 | - v3.1.2 2021.5.16 492 | - bug fix 493 | - [x] 文本和inline元素之间打空格回多添加一个空格, that won't happen again. 494 | - v3.1.1 2021.5.15 495 | - Improve 496 | - [x] 选中文本情况下,按中文的¥键,将自动替换成$,变成行内公式 497 | - [x] 选中文本情况下,按中文的·,将自动替换成`,变成行内代码块 498 | - v3.1.0 2021.5.14 499 | - Improve 500 | - **增加了全文的内容识别,在全文语境下的代码块和公式中不自动格式化** 501 | - 增加了快捷键:全文格式化 502 | - 增加了快捷键:AutoFormat 开关切换 503 | - Md 链接将不论小括号内容 504 | - v3.0.1 505 | - bug fix 506 | - 修复了inline code 等元素和前后的标点符号空格的bug。 507 | - v3.0.0 508 | - Improvement 509 | - **重构代码逻辑**,提升可维护性 510 | - 增强了行内公式和行内代码块的识别逻辑 511 | - 增加了多种链接的识别:wiki link, markdown link and bare link. 512 | - 增加了多种链接的自动空格功能开关 513 | - 开发了**光标位置计算算法**,大大提升了行内编辑的体验 514 | - v2.3.1 515 | - bug fix 516 | - [x] 修复数字和后面的冒号自动空格的 bug 517 | - v2.3.0 518 | - improvement 519 | - 增加了对obsidian 和 zotero 链接的识别(`obsidian://`, `zotero://`),链接内部不自动 format 520 | - Bug fix 521 | - 修复了对部分链接内部字符无法识别的bug 522 | - v2.2.0 523 | - improvement 524 | - 去除部分冗余代码 525 | - 对main.ts中类重新命名 526 | - v2.1.0 527 | - bugs fix 528 | - 修复上个版本中链接在某些情况下还是会被格式化的bug 529 | - v2.0.0 530 | - Improvement 531 | - 独立设置数字和英文文本,标点的空格,数字和`.`不空格 532 | - list,checkbox 中支持英文行首字母大写 533 | - 自动识别网址链接,不格式化网址链接部分内容 534 | - 识别 WikiLink 和 MarkDown link,不格式化其内容 535 | - 设置面板分类更加清晰 536 | - Bug fix 537 | - inline 元素的范围识别逻辑 538 | - v1.0.0 539 | - 基本功能完成 -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules' 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === 'production'); 13 | 14 | esbuild.build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ['src/main.ts'], 19 | bundle: true, 20 | external: [ 21 | 'obsidian', 22 | 'electron', 23 | '@codemirror/autocomplete', 24 | '@codemirror/collab', 25 | '@codemirror/commands', 26 | '@codemirror/language', 27 | '@codemirror/lint', 28 | '@codemirror/search', 29 | '@codemirror/state', 30 | '@codemirror/view', 31 | '@lezer/common', 32 | '@lezer/highlight', 33 | '@lezer/lr', 34 | ...builtins], 35 | format: 'cjs', 36 | watch: !prod, 37 | target: 'es2018', 38 | logLevel: "info", 39 | sourcemap: prod ? false : 'inline', 40 | treeShaking: true, 41 | outfile: 'main.js', 42 | }).catch(() => process.exit(1)); 43 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "easy-typing-obsidian", 3 | "name": "Easy Typing", 4 | "version": "5.5.14", 5 | "minAppVersion": "0.15.0", 6 | "description": "This plugin aims to enhance and optimize the editing experience in Obsidian", 7 | "author": "yaozhuwa", 8 | "authorUrl": "https://github.com/Yaozhuwa", 9 | "isDesktopOnly": false, 10 | "fundingUrl": "https://www.buymeacoffee.com/yaozhuwa" 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-typing-obsidian", 3 | "version": "5.5.14", 4 | "description": "Autoformat your note as typing.(Auto captalize, autospace and more...)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@codemirror/language": "^6.2.1", 16 | "@types/node": "^20.10.7", 17 | "@types/obsidian-typings": "npm:obsidian-typings@^1.1.1", 18 | "@types/sprintf-js": "^1.1.4", 19 | "@typescript-eslint/eslint-plugin": "5.29.0", 20 | "@typescript-eslint/parser": "5.29.0", 21 | "builtin-modules": "3.3.0", 22 | "esbuild": "0.14.47", 23 | "obsidian": "latest", 24 | "tslib": "2.4.0", 25 | "typescript": "4.7.4" 26 | }, 27 | "dependencies": { 28 | "sprintf-js": "^1.1.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import { Notice} from "obsidian" 2 | import { EasyTypingSettings, WorkMode } from './settings' 3 | import { Annotation, EditorState, Extension, StateField, Transaction, TransactionSpec, Text, Line } from '@codemirror/state'; 4 | import { offsetToPos, posToOffset, stringDeleteAt, stringInsertAt, isParamDefined} from './utils' 5 | import { ensureSyntaxTree, syntaxTree } from "@codemirror/language"; 6 | import { print } from "./utils" 7 | 8 | export enum LineType { text = 'text', codeblock = 'codeblock', formula = 'formula', 9 | code_start = 'code_block_start', code_end = 'code_block_end', 10 | none = 'none', frontmatter="frontmatter", 11 | quote='quote', callout_title='callout_title', list='list', table= 'table' } 12 | 13 | export enum SpaceState { 14 | none, 15 | soft, 16 | strict 17 | } 18 | 19 | export enum InlineType { 20 | text = 'text', code = 'code', formula = 'formula', 21 | wikilink = 'wikilink', mdlink = "mdlink", 22 | user = 'user-defined', none = 'none' 23 | } 24 | 25 | export interface InlineChange { 26 | text: string, 27 | begin: number, 28 | end: number, 29 | origin: string 30 | } 31 | 32 | export interface ArticlePart { 33 | type: LineType; 34 | begin: number; 35 | end: number 36 | } 37 | 38 | export interface InlinePart { 39 | content: string; 40 | type: InlineType; 41 | begin: number; 42 | end: number; 43 | leftSpaceRequire: SpaceState; 44 | rightSpaceRequire: SpaceState; 45 | } 46 | 47 | export class LineFormater { 48 | constructor() { } 49 | syntaxTreeNodeNameType(name:string):InlineType{ 50 | if(name.contains('code') && !name.contains("link")){ 51 | return InlineType.code; 52 | } 53 | else if(name.contains('math')){ 54 | return InlineType.formula; 55 | } 56 | else{ 57 | return InlineType.text; 58 | } 59 | } 60 | 61 | // param lineNum: 1-based line number 62 | parseLineWithSyntaxTree(state: EditorState, lineNum:number, regRegExp?: string){ 63 | let linePartsOfTxtCodeFormula: InlinePart[] = []; 64 | let line = state.doc.line(lineNum); 65 | const tree = syntaxTree(state); 66 | let pos = line.from; 67 | let prevNodeType:InlineType = InlineType.none; 68 | let prevBeginIdx = 0; 69 | while(pos { 124 | item.begin += linePartsOfTxtCodeFormula[i].begin; 125 | item.end += linePartsOfTxtCodeFormula[i].begin; 126 | retArray.push(item); 127 | }); 128 | } 129 | } 130 | // console.log(retArray) 131 | return retArray; 132 | } 133 | 134 | formatLineOfDoc(state: EditorState, settings: EasyTypingSettings, fromB: number, toB: number, insertedStr: string): [TransactionSpec[], TransactionSpec] | null { 135 | let doc = state.doc; 136 | let line = doc.lineAt(fromB).text; 137 | let res = null 138 | if (insertedStr.contains("\n")) 139 | { 140 | // console.log('FromB, ToB', fromB, toB) 141 | res = this.formatLine(state, doc.lineAt(fromB).number, settings, offsetToPos(doc, fromB).ch, offsetToPos(doc, fromB).ch); 142 | } 143 | else 144 | { 145 | res = this.formatLine(state, doc.lineAt(fromB).number, settings, offsetToPos(doc, toB).ch, offsetToPos(doc, fromB).ch); 146 | } 147 | if (res ===null || res[2].length==0) return null; 148 | 149 | let newline = stringInsertAt(res[0], res[1], "|"); 150 | // if (settings.debug) console.log("EasyTyping: New Line String:", newline) 151 | 152 | let changes: TransactionSpec[] = []; 153 | let offset = doc.lineAt(fromB).from; 154 | 155 | for(let changeItem of res[2]) 156 | { 157 | changes.push({ 158 | changes:{from: offset+changeItem.begin, to:offset+changeItem.end, insert:changeItem.text}, userEvent:"EasyTyping.change" 159 | }) 160 | } 161 | if (insertedStr.contains("\n")){ 162 | console.log("insertStr", insertedStr) 163 | res[1]+= insertedStr.length; 164 | } 165 | return [changes, {selection:{anchor:offset+res[1]}, userEvent:"EasyTyping.change"}]; 166 | } 167 | 168 | // 返回值: [最终的行,最终光标位置,内容改变] 169 | // param lineNum: 1-based line number 170 | // curCh: 光标在当前行的位置 171 | // prevCh: 光标在前一时刻在当前行的位置 172 | formatLine(state: EditorState, lineNum:number, settings: EasyTypingSettings, curCh: number, prevCh?: number): [string, number, InlineChange[]] | null { 173 | // new Notice("format-now"); 174 | // print("formatLine", lineNum, curCh, prevCh) 175 | let line = state.doc.line(lineNum).text; 176 | let regNull = /^\s*$/g; 177 | if (regNull.test(line)) return [line, curCh, []]; 178 | // 1. 划分一行文字的内部不同模块区域 179 | 180 | let lineParts = settings.UserDefinedRegSwitch 181 | ? this.parseLineWithSyntaxTree(state, lineNum, settings.UserDefinedRegExp) 182 | : this.parseLineWithSyntaxTree(state, lineNum); 183 | 184 | if (settings.debug) console.log("line parts\n", lineParts); 185 | 186 | // 备份原来的lineParts, 深拷贝 187 | let linePartsOrigin = JSON.parse(JSON.stringify(lineParts)); 188 | let inlineChangeList: InlineChange[] = []; 189 | 190 | let cursorLinePartIndex = -1; 191 | let cursorRelativeIndex = -1; 192 | let resultCursorCh = 0; // 输出的光标位置 193 | 194 | // 2. 找到光标所在的部分,如果是 InlinePart.text,则在光标处插入'\0'来标记光标位置 195 | for (let i = 0; i < lineParts.length; i++) { 196 | if (curCh > lineParts[i].begin && curCh <= lineParts[i].end) { 197 | cursorLinePartIndex = i; 198 | cursorRelativeIndex = curCh - lineParts[i].begin; 199 | if (lineParts[i].type === InlineType.text) { 200 | lineParts[i].content = stringInsertAt(lineParts[i].content, cursorRelativeIndex, '\0'); 201 | } 202 | break; 203 | } 204 | } 205 | let resultLine = ''; 206 | let offset = 0; 207 | // 保存前一部分的区块类型,InlineType.none 代表一行的开始 208 | let prevPartType: string = InlineType.none; 209 | let prevTextEndSpaceState = SpaceState.none; 210 | 211 | // 3. 遍历每个行部分,进行格式化处理 212 | for (let i = 0; i < lineParts.length; i++) { 213 | // 3.1 如果行内第一部分为文本,则处理句首字母大写的部分 214 | if (i === 0 && lineParts[i].type === InlineType.text && settings.AutoCapital) { 215 | // 3.1.1 如果 prevCursor 且光标不在此部分,则跳过 216 | if (isParamDefined(prevCh) && cursorLinePartIndex != 0) { } 217 | else { 218 | let regFirstSentence = /^\s*(\- (\[[x ]\] )?)?“?[a-z\u0401\u0451\u0410-\u044f]/g; 219 | let regHeaderSentence = /^(#+ |>+ ?|“)[a-z\u0401\u0451\u0410-\u044f]/g; 220 | let textcopy = lineParts[0].content; 221 | let match = regFirstSentence.exec(textcopy); 222 | let matchHeader = regHeaderSentence.exec(textcopy); 223 | let dstCharIndex = -1; 224 | if (match) { 225 | dstCharIndex = regFirstSentence.lastIndex - 1; 226 | } 227 | else if (matchHeader) { 228 | dstCharIndex = regHeaderSentence.lastIndex - 1; 229 | } 230 | 231 | if (settings.AutoCapitalMode == WorkMode.Globally || (isParamDefined(prevCh) && dstCharIndex >= prevCh && dstCharIndex < curCh)) { } 232 | else { 233 | dstCharIndex = -1; 234 | } 235 | 236 | if (dstCharIndex != -1) { 237 | 238 | lineParts[0].content = textcopy.substring(0, dstCharIndex) + textcopy.charAt(dstCharIndex).toUpperCase() + textcopy.substring(dstCharIndex + 1); 239 | } 240 | } 241 | } 242 | 243 | switch (lineParts[i].type) { 244 | // 3.2.1 处理文本区块 245 | case InlineType.text: 246 | let content = lineParts[i].content; 247 | // Text.4 处理句首字母大写 248 | if (settings.AutoCapital) { 249 | var reg = /[\.\?\!。!?]([\s]*)[a-z\u0401\u0451\u0410-\u044f]/g; 250 | while (true) { 251 | let match = reg.exec(content); 252 | if (!match) break; 253 | let tempIndex = reg.lastIndex - 1; 254 | // console.log("prevCh, curCh, offset, tempIndex") 255 | // console.log(prevCh, curCh, offset, tempIndex) 256 | let isSpaceDot = tempIndex-2<0 || content.substring(tempIndex-2, tempIndex)==' .'; 257 | if (settings.AutoCapitalMode == WorkMode.Globally && !isSpaceDot) { 258 | lineParts[i].content = content.substring(0, tempIndex) + content.charAt(tempIndex).toUpperCase() + content.substring(reg.lastIndex); 259 | content = lineParts[i].content; 260 | } 261 | else if (isParamDefined(prevCh) && tempIndex >= prevCh - offset && tempIndex < curCh - offset && !isSpaceDot) { 262 | lineParts[i].content = content.substring(0, tempIndex) + content.charAt(tempIndex).toUpperCase() + content.substring(reg.lastIndex); 263 | content = lineParts[i].content; 264 | } 265 | } 266 | } 267 | 268 | function insertSpace(content: string, reg: RegExp, prevCh: number, curCh: number, offset: number): [string, number] { 269 | while (true) { 270 | let match = reg.exec(content); 271 | if (!match) break; 272 | let tempIndex = reg.lastIndex - 1; 273 | if (isParamDefined(prevCh) && tempIndex >= prevCh - offset && tempIndex < curCh - offset) { 274 | content = content.substring(0, tempIndex) + " " + content.substring(tempIndex); 275 | curCh += 1; 276 | } 277 | } 278 | return [content, curCh]; 279 | } 280 | 281 | // Text.1 处理中英文之间空格 282 | if (settings.ChineseEnglishSpace) { 283 | let reg1 = /([A-Za-z])([\u4e00-\u9fa5])/gi; 284 | let reg2 = /([\u4e00-\u9fa5])([A-Za-z])/gi; 285 | [content, curCh] = insertSpace(content, reg1, prevCh, curCh, offset); 286 | [content, curCh] = insertSpace(content, reg2, prevCh, curCh, offset); 287 | lineParts[i].content = content; 288 | } 289 | 290 | if (settings.ChineseNumberSpace){ 291 | let reg = /([0-9])([\u4e00-\u9fa5])/g; 292 | let reg1 = /([\u4e00-\u9fa5])([0-9])/g; 293 | [content, curCh] = insertSpace(content, reg, prevCh, curCh, offset); 294 | [content, curCh] = insertSpace(content, reg1, prevCh, curCh, offset); 295 | lineParts[i].content = content; 296 | } 297 | 298 | if (settings.EnglishNumberSpace){ 299 | let reg = /([A-Za-z])(\d)/g; 300 | let reg1 = /(\d)([A-Za-z])/g; 301 | [content, curCh] = insertSpace(content, reg, prevCh, curCh, offset); 302 | [content, curCh] = insertSpace(content, reg1, prevCh, curCh, offset); 303 | lineParts[i].content = content; 304 | } 305 | 306 | // Text.2 处理中文间无空格 307 | if (settings.ChineseNoSpace) { 308 | let reg = /([\u4e00-\u9fa5,。、!;‘’《》]+)(\s+)([\u4e00-\u9fa5,。、!;‘’《》]+)/g; 309 | while (reg.exec(content)) { 310 | lineParts[i].content = content.replace(reg, "$1$3"); 311 | content = lineParts[i].content; 312 | } 313 | } 314 | 315 | // 标点与文本空格 316 | if (settings.PunctuationSpace) { 317 | // Text.3 处理标点与文本空格 318 | // if(settings.EnglishSpace) 319 | { 320 | let reg = /([,\.;\?\!\)])([0-9A-Za-z\u0401\u0451\u0410-\u044f\u4e00-\u9fa5])|([A-Za-z0-9\u4e00-\u9fa5:,\.\?\!'"]+)(\()|[,\.;\?:!][\u4e00-\u9fa5]/gi; 321 | while (true) { 322 | let match = reg.exec(content); 323 | if (!match) break; 324 | let tempIndex = reg.lastIndex - 1; 325 | let isSpaceDot = '!.?;,'.contains(content.charAt(tempIndex-1)) && ((tempIndex-2<0 && i==0) || content.charAt(tempIndex-2)==' '); 326 | let isNumPuncNum = /[,.]\d/.test(content.substring(tempIndex-1, tempIndex+1)) && 327 | (tempIndex-2<0 || /\d/.test(content.charAt(tempIndex-2))) 328 | 329 | if (settings.PunctuationSpaceMode == WorkMode.Globally && !isSpaceDot && !isNumPuncNum) { 330 | content = content.substring(0, tempIndex) + " " + content.substring(tempIndex); 331 | } 332 | else if (isParamDefined(prevCh) && tempIndex >= prevCh - offset 333 | && tempIndex < curCh - offset 334 | && !isSpaceDot && !isNumPuncNum) { 335 | content = content.substring(0, tempIndex) + " " + content.substring(tempIndex); 336 | curCh += 1; 337 | } 338 | } 339 | 340 | // 单独处理冒号后文本的自动空格,为了兼容 :emoji: 格式的输入 341 | let reg2 = /(:)([A-Za-z0-9_]+[ ,\.\?\\\/;'",。?;‘“”’、\[\]\-\{\}])/gi; 342 | lineParts[i].content = content.replace(reg2, "$1 $2"); 343 | content = lineParts[i].content; 344 | 345 | let reg3 = /(:)(["'])/g; 346 | lineParts[i].content = content.replace(reg3, "$1 $2"); 347 | content = lineParts[i].content; 348 | } 349 | } 350 | 351 | // Text.7 得到文本部分是否以空白符开始或结束,用来判断后续文本前后是否需要添加空格 352 | let regStrictSpaceStart = /^\0?\s/; 353 | let regStrictSpaceEnd = /\s\0?$/; 354 | let regStartWithSpace = /^\0?[\s,\.;\?\!,。;》?::!~\*、()"”\[\]\)\{\}]/; 355 | let regEndWithSpace = /[\s,。、:;?!()~\*"《“\[\]\(\{\}]\0?$/; 356 | let txtStartSpaceSate = SpaceState.none; 357 | let txtEndSpaceState = SpaceState.none; 358 | if (regStartWithSpace.test(content)||content.startsWith("
")) { 359 | if (regStrictSpaceStart.test(content)) 360 | txtStartSpaceSate = SpaceState.strict 361 | else 362 | txtStartSpaceSate = SpaceState.soft 363 | } 364 | 365 | if (regEndWithSpace.test(content) || content.endsWith("
")) { 366 | if (regStrictSpaceEnd.test(content)) 367 | txtEndSpaceState = SpaceState.strict; 368 | else 369 | txtEndSpaceState = SpaceState.soft; 370 | } 371 | 372 | // Text.8 根据前一部分的区块类型处理空格添加的问题 373 | switch (prevPartType) { 374 | case InlineType.none: 375 | break; 376 | case InlineType.code: 377 | if (settings.InlineCodeSpaceMode > txtStartSpaceSate) { 378 | lineParts[i].content = ' ' + content; 379 | content = lineParts[i].content; 380 | } 381 | break; 382 | case InlineType.formula: 383 | if (settings.InlineFormulaSpaceMode > txtStartSpaceSate) { 384 | lineParts[i].content = ' ' + content; 385 | content = lineParts[i].content; 386 | } 387 | break; 388 | case InlineType.wikilink: 389 | case InlineType.mdlink: 390 | if (!settings.InlineLinkSmartSpace && settings.InlineLinkSpaceMode > txtStartSpaceSate) { 391 | lineParts[i].content = ' ' + content; 392 | content = lineParts[i].content; 393 | } 394 | else if (settings.InlineLinkSmartSpace && txtStartSpaceSate == SpaceState.none) { 395 | let charAtTextBegin = content.charAt(0); 396 | let regMdLinkEnd = /\]/; 397 | let charAtLinkEndIndex = lineParts[i - 1].content.search(regMdLinkEnd) - 1; 398 | let charAtLinkEnd = lineParts[i - 1].content.charAt(charAtLinkEndIndex); 399 | if (charAtLinkEnd === '[') break; 400 | let twoNeighborChars = charAtLinkEnd + charAtTextBegin; 401 | let regNotNeedSpace = /[\u4e00-\u9fa5,。?:;”“’‘-)}][\u4e00-\u9fa5]/g; 402 | if (!regNotNeedSpace.test(twoNeighborChars)) { 403 | lineParts[i].content = ' ' + content; 404 | content = lineParts[i].content; 405 | } 406 | } 407 | break; 408 | case InlineType.user: 409 | if (lineParts[i - 1].rightSpaceRequire > txtStartSpaceSate) { 410 | lineParts[i].content = ' ' + content; 411 | content = lineParts[i].content; 412 | } 413 | break; 414 | } 415 | 416 | // Text.9 如果光标在该区块,则计算最终光标的位置 417 | if (i === cursorLinePartIndex) { 418 | let reg = '\0'; 419 | let n = content.search(reg) 420 | resultCursorCh = offset + n; 421 | // 删除 \0 422 | lineParts[i].content = stringDeleteAt(content, n); 423 | } 424 | 425 | resultLine += lineParts[i].content; 426 | offset += lineParts[i].content.length; 427 | prevPartType = InlineType.text; 428 | prevTextEndSpaceState = txtEndSpaceState; 429 | break; 430 | 431 | // 3.2.2 处理行内代码块部分 432 | case InlineType.code: 433 | // Code.1 根据前一区块类型和settings添加空格 434 | switch(prevPartType) 435 | { 436 | case InlineType.none: 437 | break; 438 | case InlineType.text: 439 | if (settings.InlineCodeSpaceMode > prevTextEndSpaceState) 440 | { 441 | lineParts[i-1].content += ' '; 442 | resultLine += ' '; 443 | offset += 1; 444 | } 445 | break; 446 | case InlineType.code: 447 | if (settings.InlineCodeSpaceMode>SpaceState.none) 448 | { 449 | inlineChangeList.push( 450 | { 451 | text:' ', 452 | begin: lineParts[i].begin, 453 | end: lineParts[i].begin, 454 | origin:'' 455 | } 456 | ); 457 | resultLine += ' '; 458 | offset += 1; 459 | } 460 | break; 461 | case InlineType.formula: 462 | if (settings.InlineCodeSpaceMode>SpaceState.none || 463 | settings.InlineFormulaSpaceMode>SpaceState.none) 464 | { 465 | inlineChangeList.push( 466 | { 467 | text:' ', 468 | begin: lineParts[i].begin, 469 | end: lineParts[i].begin, 470 | origin:'' 471 | } 472 | ); 473 | resultLine += ' '; 474 | offset += 1; 475 | } 476 | break; 477 | case InlineType.mdlink: 478 | case InlineType.wikilink: 479 | if (settings.InlineCodeSpaceMode>SpaceState.none || 480 | settings.InlineLinkSpaceMode>SpaceState.none) 481 | { 482 | inlineChangeList.push( 483 | { 484 | text:' ', 485 | begin: lineParts[i].begin, 486 | end: lineParts[i].begin, 487 | origin:'' 488 | } 489 | ); 490 | resultLine += ' '; 491 | offset += 1; 492 | } 493 | break; 494 | case InlineType.user: 495 | if (settings.InlineCodeSpaceMode>SpaceState.none && 496 | lineParts[i-1].rightSpaceRequire>SpaceState.none) 497 | { 498 | inlineChangeList.push( 499 | { 500 | text:' ', 501 | begin: lineParts[i].begin, 502 | end: lineParts[i].begin, 503 | origin:'' 504 | } 505 | ); 506 | resultLine += ' '; 507 | offset += 1; 508 | } 509 | break; 510 | } 511 | // Code.2 如果光标在该区块,则计算最终光标的位置 512 | if(i === cursorLinePartIndex) 513 | { 514 | resultCursorCh = offset + cursorRelativeIndex; 515 | } 516 | // Code.3 变量更新 517 | resultLine += lineParts[i].content; 518 | offset += lineParts[i].content.length; 519 | prevPartType = InlineType.code; 520 | prevTextEndSpaceState = SpaceState.none; 521 | break; 522 | 523 | // 3.2.3 处理行内公式的部分 524 | case InlineType.formula: 525 | if (lineParts[i].content == "$\\qquad$") { 526 | prevPartType = InlineType.text; 527 | prevTextEndSpaceState = SpaceState.strict; 528 | break; 529 | } 530 | // Formula.1 根据前一区块类型和settings添加空格 531 | switch(prevPartType) 532 | { 533 | case InlineType.none: 534 | break; 535 | case InlineType.text: 536 | if (settings.InlineFormulaSpaceMode>prevTextEndSpaceState) 537 | { 538 | lineParts[i-1].content += ' '; 539 | resultLine += ' '; 540 | offset += 1; 541 | } 542 | break; 543 | case InlineType.code: 544 | if (settings.InlineFormulaSpaceMode>SpaceState.none || 545 | settings.InlineCodeSpaceMode>SpaceState.none) 546 | { 547 | inlineChangeList.push( 548 | { 549 | text:' ', 550 | begin: lineParts[i].begin, 551 | end: lineParts[i].begin, 552 | origin:'' 553 | } 554 | ); 555 | resultLine += ' '; 556 | offset += 1; 557 | } 558 | break; 559 | case InlineType.formula: 560 | if (settings.InlineCodeSpaceMode>SpaceState.none) 561 | { 562 | inlineChangeList.push( 563 | { 564 | text:' ', 565 | begin: lineParts[i].begin, 566 | end: lineParts[i].begin, 567 | origin:'' 568 | } 569 | ); 570 | resultLine += ' '; 571 | offset += 1; 572 | } 573 | break; 574 | case InlineType.mdlink: 575 | case InlineType.wikilink: 576 | if (settings.InlineFormulaSpaceMode>SpaceState.none || 577 | settings.InlineLinkSpaceMode>SpaceState.none) 578 | { 579 | inlineChangeList.push( 580 | { 581 | text:' ', 582 | begin: lineParts[i].begin, 583 | end: lineParts[i].begin, 584 | origin:'' 585 | } 586 | ); 587 | resultLine += ' '; 588 | offset += 1; 589 | } 590 | break; 591 | case InlineType.user: 592 | if (settings.InlineFormulaSpaceMode>SpaceState.none && 593 | lineParts[i-1].rightSpaceRequire>SpaceState.none) 594 | { 595 | inlineChangeList.push( 596 | { 597 | text:' ', 598 | begin: lineParts[i].begin, 599 | end: lineParts[i].begin, 600 | origin:'' 601 | } 602 | ); 603 | resultLine += ' '; 604 | offset += 1; 605 | } 606 | break; 607 | } 608 | // Formula.2 如果光标在该区块,则计算最终光标的位置 609 | if(i === cursorLinePartIndex) 610 | { 611 | resultCursorCh = offset + cursorRelativeIndex; 612 | } 613 | // Formula.3 变量更新 614 | resultLine += lineParts[i].content; 615 | offset += lineParts[i].content.length; 616 | prevPartType = InlineType.formula; 617 | prevTextEndSpaceState = SpaceState.none; 618 | break; 619 | 620 | case InlineType.mdlink: 621 | case InlineType.wikilink: 622 | switch(prevPartType) 623 | { 624 | case InlineType.none: 625 | break; 626 | case InlineType.text: 627 | if (prevTextEndSpaceState>=settings.InlineLinkSpaceMode && !settings.InlineLinkSmartSpace) break; 628 | if (prevTextEndSpaceState==SpaceState.strict && settings.InlineLinkSpaceMode==SpaceState.strict) break; 629 | 630 | let charAtTextEnd = lineParts[i - 1].content.charAt(lineParts[i - 1].content.length - 1); 631 | let charAtLinkBegin: string = ""; 632 | if (lineParts[i].type == InlineType.wikilink) { 633 | let regAlias = /\|/; 634 | let charOfAliasBegin = lineParts[i].content.search(regAlias); 635 | // console.log("charOfAliasBegin",charOfAliasBegin) 636 | let beginIndex = 2; 637 | if (lineParts[i].content.charAt(0) === '!') beginIndex = 3; 638 | 639 | if (charOfAliasBegin != -1) { 640 | beginIndex = charOfAliasBegin + 1; 641 | } 642 | else if (lineParts[i].content.charAt(beginIndex) == '#') { 643 | beginIndex += 1; 644 | } 645 | 646 | charAtLinkBegin = lineParts[i].content.charAt(beginIndex); 647 | // console.log("beginIndex", beginIndex); 648 | if (charAtLinkBegin == ']') break; 649 | } 650 | else { 651 | let regMdLinkBegin = /\[/; 652 | let charAtLinkBeginIndex = lineParts[i].content.search(regMdLinkBegin) + 1; 653 | charAtLinkBegin = lineParts[i].content.charAt(charAtLinkBeginIndex); 654 | if (charAtLinkBegin === ']') break; 655 | } 656 | 657 | 658 | if (settings.InlineLinkSpaceMode==SpaceState.strict && prevTextEndSpaceStateprevTextEndSpaceState){ 685 | lineParts[i-1].content += ' '; 686 | resultLine += ' '; 687 | offset += 1; 688 | } 689 | break; 690 | case InlineType.code: 691 | if (settings.InlineLinkSpaceMode>SpaceState.none || 692 | settings.InlineCodeSpaceMode>SpaceState.none) 693 | { 694 | inlineChangeList.push( 695 | { 696 | text:' ', 697 | begin: lineParts[i].begin, 698 | end: lineParts[i].begin, 699 | origin:'' 700 | } 701 | ); 702 | resultLine += ' '; 703 | offset += 1; 704 | } 705 | break; 706 | case InlineType.formula: 707 | if (settings.InlineLinkSpaceMode>SpaceState.none|| 708 | settings.InlineFormulaSpaceMode>SpaceState.none) 709 | { 710 | inlineChangeList.push( 711 | { 712 | text:' ', 713 | begin: lineParts[i].begin, 714 | end: lineParts[i].begin, 715 | origin:'' 716 | } 717 | ); 718 | resultLine += ' '; 719 | offset += 1; 720 | } 721 | break; 722 | case InlineType.mdlink: 723 | case InlineType.wikilink: 724 | if (settings.InlineLinkSpaceMode>SpaceState.none) 725 | { 726 | inlineChangeList.push( 727 | { 728 | text:' ', 729 | begin: lineParts[i].begin, 730 | end: lineParts[i].begin, 731 | origin:'' 732 | } 733 | ); 734 | resultLine += ' '; 735 | offset += 1; 736 | } 737 | break; 738 | case InlineType.user: 739 | if (lineParts[i-1].rightSpaceRequire>SpaceState.none && 740 | settings.InlineLinkSpaceMode>SpaceState.none) 741 | { 742 | inlineChangeList.push( 743 | { 744 | text:' ', 745 | begin: lineParts[i].begin, 746 | end: lineParts[i].begin, 747 | origin:'' 748 | } 749 | ); 750 | resultLine += ' '; 751 | offset += 1; 752 | } 753 | } 754 | // Link.2 如果该区块有光标,则计算最终光标位置 755 | if(i === cursorLinePartIndex) 756 | { 757 | resultCursorCh = offset + cursorRelativeIndex; 758 | } 759 | // Link.3 更新变量 760 | resultLine += lineParts[i].content; 761 | offset += lineParts[i].content.length; 762 | prevPartType = lineParts[i].type; 763 | prevTextEndSpaceState = SpaceState.none; 764 | break; 765 | 766 | // 3.2.5 处理用户自定义区块的部分 767 | case InlineType.user: 768 | // User.1 根据前一区块类型和settings添加空格 769 | switch(prevPartType) 770 | { 771 | case InlineType.none: 772 | break; 773 | case InlineType.text: 774 | if (lineParts[i].leftSpaceRequire>prevTextEndSpaceState) 775 | { 776 | lineParts[i-1].content += ' '; 777 | resultLine += ' '; 778 | offset += 1; 779 | } 780 | break; 781 | case InlineType.code: 782 | if (lineParts[i].leftSpaceRequire>SpaceState.none && 783 | settings.InlineCodeSpaceMode>SpaceState.none) 784 | { 785 | inlineChangeList.push( 786 | { 787 | text:' ', 788 | begin: lineParts[i].begin, 789 | end: lineParts[i].begin, 790 | origin:'' 791 | } 792 | ); 793 | resultLine += ' '; 794 | offset += 1; 795 | } 796 | break; 797 | case InlineType.formula: 798 | if (lineParts[i].leftSpaceRequire>SpaceState.none && 799 | settings.InlineFormulaSpaceMode>SpaceState.none) 800 | { 801 | inlineChangeList.push( 802 | { 803 | text:' ', 804 | begin: lineParts[i].begin, 805 | end: lineParts[i].begin, 806 | origin:'' 807 | } 808 | ); 809 | resultLine += ' '; 810 | offset += 1; 811 | } 812 | break; 813 | case InlineType.mdlink: 814 | case InlineType.wikilink: 815 | if (lineParts[i].leftSpaceRequire>SpaceState.none && 816 | settings.InlineLinkSpaceMode>SpaceState.none) 817 | { 818 | inlineChangeList.push( 819 | { 820 | text:' ', 821 | begin: lineParts[i].begin, 822 | end: lineParts[i].begin, 823 | origin:'' 824 | } 825 | ); 826 | resultLine += ' '; 827 | offset += 1; 828 | } 829 | break; 830 | case InlineType.user: 831 | if (lineParts[i].leftSpaceRequire>SpaceState.none && 832 | lineParts[i-1].rightSpaceRequire>SpaceState.none) 833 | { 834 | inlineChangeList.push( 835 | { 836 | text:' ', 837 | begin: lineParts[i].begin, 838 | end: lineParts[i].begin, 839 | origin:'' 840 | } 841 | ); 842 | resultLine += ' '; 843 | offset += 1; 844 | } 845 | break; 846 | } 847 | // User.2 如果该区块有光标,则计算最终光标位置 848 | if(i === cursorLinePartIndex) 849 | { 850 | resultCursorCh = offset + cursorRelativeIndex; 851 | } 852 | // Link.3 更新变量 853 | resultLine += lineParts[i].content; 854 | offset += lineParts[i].content.length; 855 | prevPartType = InlineType.user; 856 | prevTextEndSpaceState = SpaceState.none; 857 | break; 858 | } 859 | } 860 | 861 | for(let i=0;ia.begin-b.begin); 877 | // console.log('resultLine', resultLine) 878 | return [resultLine, resultCursorCh, inlineChangeList]; 879 | } 880 | 881 | } 882 | 883 | 884 | export class MarkdownParser{ 885 | constructor(){} 886 | 887 | } 888 | 889 | function matchWithReg(text: string, regExp: RegExp, type: InlineType, inlineTypeArray: InlinePart[], 890 | checkArray = false, leftSpaceRe: SpaceState = SpaceState.none, rightSpaceRe: SpaceState = SpaceState.none): InlinePart[] { 891 | let retArray = inlineTypeArray; 892 | let matchArray: InlinePart[] = []; 893 | retArray = retArray.sort((a, b): number => a.begin - b.begin); 894 | // console.log('before-----------\n',retArray) 895 | while (true) { 896 | let match = regExp.exec(text); 897 | if (!match) break; 898 | let valid = true; 899 | // 检查冲突 900 | if (checkArray) { 901 | for (let i = 0; i < retArray.length; i++) { 902 | if(regExp.lastIndex>retArray[i].begin && retArray[i].end>match.index){ 903 | valid = false; 904 | break; 905 | } 906 | } 907 | } 908 | if (!valid) continue; 909 | matchArray.push( 910 | { 911 | content: match[0], 912 | type: type, 913 | begin: match.index, 914 | end: regExp.lastIndex, 915 | leftSpaceRequire: leftSpaceRe, 916 | rightSpaceRequire: rightSpaceRe 917 | } 918 | ); 919 | } 920 | retArray = retArray.concat(matchArray); 921 | // console.log('After===========\n', retArray); 922 | return retArray; 923 | } 924 | 925 | function matchWithAbbr(text: string, type: InlineType, inlineTypeArray: InlinePart[], checkArray = false){ 926 | let retArray = inlineTypeArray; 927 | let matchArray: InlinePart[] = []; 928 | retArray = retArray.sort((a, b): number => a.begin - b.begin); 929 | let regAbbr = /([a-zA-Z]\.)+/g; 930 | while (true) { 931 | let match = regAbbr.exec(text); 932 | if (!match) break; 933 | let valid = true; 934 | let isInBlockBegin:boolean = (match.index==0); 935 | // 检查冲突 936 | if (checkArray) { 937 | for (let i = 0; i < retArray.length; i++) { 938 | if(match.index == retArray[i].end){ 939 | isInBlockBegin = true; 940 | } 941 | if(regAbbr.lastIndex>retArray[i].begin && retArray[i].end>match.index){ 942 | valid = false; 943 | break; 944 | } 945 | } 946 | } 947 | if(!isInBlockBegin && valid) 948 | { 949 | let regChar = /[a-zA-Z0-9]/; 950 | if(regChar.test(text.charAt(match.index-1))){ 951 | valid = false; 952 | } 953 | } 954 | 955 | if (!valid) continue; 956 | matchArray.push( 957 | { 958 | content: match[0], 959 | type: type, 960 | begin: match.index, 961 | end: regAbbr.lastIndex, 962 | leftSpaceRequire: SpaceState.none, 963 | rightSpaceRequire: SpaceState.none 964 | } 965 | ); 966 | } 967 | retArray = retArray.concat(matchArray); 968 | // console.log('After===========\n', retArray); 969 | return retArray; 970 | } 971 | 972 | /** 973 | * 分割一行文本中的链接和用户自定义的正则部分,得到 InlinePart 的不同区域 974 | */ 975 | function splitTextWithLinkAndUserDefined(text: string, regExps?: string): InlinePart[] { 976 | let retArray: InlinePart[] = []; 977 | let regWikiLink = /\!{0,2}\[\[[^\[\]]*?\]\]/g; 978 | let regMdLink = /\!{0,2}\[[^\[\]]*?\]\([^\s]*\)/g; 979 | // let regBareLink = /(https?:\/\/|ftp:\/\/|obsidian:\/\/|zotero:\/\/|www.)[^\s()《》。,!?;:“”‘’\)\(\[\]\{\}']+/g; 980 | 981 | // 1. 匹配wikilink 982 | retArray = matchWithReg(text, regWikiLink, InlineType.wikilink, retArray); 983 | // 2. 匹配mdlink 984 | retArray = matchWithReg(text, regMdLink, InlineType.mdlink, retArray); 985 | 986 | // 3. 匹配用户自定义正则 987 | let regExpList: RegExp[] = []; 988 | let leftSRequireList: SpaceState[] = []; 989 | let rightSRequireList: SpaceState[] = []; 990 | let regNull = /^\s*$|^\/\//g; 991 | let regSRequire = /\|[\-=\+][\-=\+]$/; 992 | if (regExps) { 993 | let regs = regExps.split('\n'); 994 | for (let i = 0; i < regs.length; i++) { 995 | 996 | if (regNull.test(regs[i])) continue; 997 | 998 | if ((!regSRequire.test(regs[i])) || regs[i].length <= 3) { 999 | new Notice("EasyTyping: 第" + String(i) + "行自定义正则不符合规范\n"+regs[i]); 1000 | continue; 1001 | } 1002 | let regItem = regs[i].substring(0, regs[i].length - 3); 1003 | let spaceReqString = regs[i].substring(regs[i].length - 3); 1004 | 1005 | let isValidReg = true; 1006 | try { 1007 | let regTemp = new RegExp(regItem, 'g') 1008 | } 1009 | catch (error) { 1010 | isValidReg = false; 1011 | if(this.settings.debug){ 1012 | new Notice("EasuTyping: Bad RegExp:\n" + regItem); 1013 | } 1014 | } 1015 | 1016 | if (isValidReg) { 1017 | regExpList.push(new RegExp(regItem, 'g')); 1018 | leftSRequireList.push(str2SpaceState(spaceReqString.charAt(1))); 1019 | rightSRequireList.push(str2SpaceState(spaceReqString.charAt(2))); 1020 | } 1021 | } 1022 | let regLen = regExpList.length; 1023 | 1024 | for (let i = 0; i < regLen; i++) { 1025 | retArray = matchWithReg(text, regExpList[i], InlineType.user, retArray, true, leftSRequireList[i], rightSRequireList[i]); 1026 | } 1027 | } 1028 | 1029 | // 匹配时间戳 1030 | retArray = matchWithReg(text, /\d{1,2}:\d{1,2}(:\d{0,2}){0,1}/g, InlineType.user, retArray, true, SpaceState.none, SpaceState.none); 1031 | 1032 | // 4. 匹配缩写如 a.m. 1033 | retArray = matchWithAbbr(text, InlineType.user, retArray, true); 1034 | 1035 | // 5. 得到剩余的文本部分 1036 | retArray = retArray.sort((a, b): number => a.begin - b.begin); 1037 | 1038 | let textArray: InlinePart[] = []; 1039 | let textBegin = 0; 1040 | let textEnd = 0; 1041 | for (let i = 0; i < retArray.length; i++) { 1042 | if (textBegin < retArray[i].begin) { 1043 | textEnd = retArray[i].begin; 1044 | textArray.push( 1045 | { 1046 | content: text.substring(textBegin, textEnd), 1047 | type: InlineType.text, 1048 | begin: textBegin, 1049 | end: textEnd, 1050 | leftSpaceRequire: SpaceState.none, 1051 | rightSpaceRequire: SpaceState.none 1052 | } 1053 | ); 1054 | } 1055 | textBegin = retArray[i].end; 1056 | } 1057 | 1058 | if (textBegin != text.length) { 1059 | textArray.push( 1060 | { 1061 | content: text.substring(textBegin, text.length), 1062 | type: InlineType.text, 1063 | begin: textBegin, 1064 | end: text.length, 1065 | leftSpaceRequire: SpaceState.none, 1066 | rightSpaceRequire: SpaceState.none 1067 | } 1068 | ); 1069 | } 1070 | 1071 | // 6. 合并文本部分和其他部分 1072 | retArray = retArray.concat(textArray); 1073 | retArray = retArray.sort((a, b): number => a.begin - b.begin); 1074 | return retArray 1075 | } 1076 | 1077 | // 字符转化成空格状态要求 1078 | function str2SpaceState(s: string): SpaceState { 1079 | switch (s) { 1080 | case "+": 1081 | return SpaceState.strict; 1082 | case '=': 1083 | return SpaceState.soft; 1084 | case '-': 1085 | default: 1086 | return SpaceState.none; 1087 | } 1088 | } 1089 | 1090 | 1091 | export function string2SpaceState(s:string):SpaceState 1092 | { 1093 | if(Number(s)==SpaceState.none) return SpaceState.none; 1094 | if(Number(s)==SpaceState.soft) return SpaceState.soft; 1095 | if(Number(s)==SpaceState.strict) return SpaceState.strict; 1096 | return SpaceState.none; 1097 | } 1098 | 1099 | 1100 | export function getPosLineType(state: EditorState, pos: number):LineType { 1101 | const line = state.doc.lineAt(pos) 1102 | let line_number = line.number 1103 | // const tree = syntaxTree(state); 1104 | const tree = ensureSyntaxTree(state, line.to); 1105 | const token = tree.resolve(line.from, 1).name 1106 | 1107 | // for (let p=line.from; p=1; l-=1){ 1141 | let l_line = state.doc.line(l) 1142 | let l_token = tree.resolve(l_line.from, 1).name 1143 | if(!l_token.contains('quote')){ 1144 | break; 1145 | } 1146 | if (l_token.contains('callout')){ 1147 | callout_start_line = l; 1148 | break; 1149 | } 1150 | } 1151 | if (callout_start_line==-1) return LineType.text; 1152 | 1153 | // 然后判断是否为代码块 1154 | let is_code_block:boolean = false; 1155 | let reset:boolean = false; 1156 | let reg_code_begin = /^>+ ```/; 1157 | let reg_code_end = /^>+ ```$/; 1158 | for (let l=callout_start_line+1; l<=line_number; l+=1){ 1159 | let l_line = state.doc.line(l) 1160 | if (reset){ 1161 | is_code_block = false; 1162 | reset = false; 1163 | } 1164 | if(is_code_block && reg_code_end.test(l_line.text)){ 1165 | is_code_block = true; 1166 | reset = true; 1167 | } 1168 | else if(!is_code_block && reg_code_begin.test(l_line.text)){ 1169 | is_code_block = true; 1170 | } 1171 | } 1172 | if (is_code_block) { 1173 | return LineType.codeblock; 1174 | } 1175 | else return LineType.text; 1176 | } 1177 | else if(token.contains('list')){ 1178 | for(let p=line.from+1;p" 14 | }, 15 | basicInputEnhance: { 16 | name: "Basic symbol input enhance for Obsidian", 17 | desc: "Basic input enhancement for Obsidian, e.g., 【【| → [[|]], starting with 、→ /, starting with 》→ >, ··| → `|`, `·|` becomes code block, ¥¥| → $|$" 18 | }, 19 | codeblockEdit: { 20 | name: "Enhance codeblock edit", 21 | desc: "Improve editing in codeblocks (Tab, delete, paste, Cmd/Ctrl+A select)." 22 | }, 23 | backspaceEdit: { 24 | name: "Enhance backspace edit", 25 | desc: "Improve backspace featurefor empty list item or empty quote line." 26 | }, 27 | tabOut: { 28 | name: "Tabout", 29 | desc: "Tab out of inline code or paired symbols." 30 | }, 31 | autoFormatting: { 32 | name: "Auto formatting when typing", 33 | desc: "Toggle auto-formatting of text while editing the document." 34 | }, 35 | spaceBetweenChineseEnglish: { 36 | name: "Space between Chinese and English", 37 | desc: "Insert space between Chinese and English characters." 38 | }, 39 | spaceBetweenChineseNumber: { 40 | name: "Space between Chinese and Number", 41 | desc: "Insert space between Chinese characters and numbers." 42 | }, 43 | spaceBetweenEnglishNumber: { 44 | name: "Space between English and Number", 45 | desc: "Insert space between English characters and numbers." 46 | }, 47 | quoteSpace: { 48 | name: "Space between quote character > and text", 49 | desc: "Insert space between quote character > and text." 50 | }, 51 | deleteSpaceBetweenChinese: { 52 | name: "Delete the Space between Chinese characters", 53 | desc: "Remove spaces between Chinese characters." 54 | }, 55 | capitalizeFirstLetter: { 56 | name: "Capitalize the first letter of every sentence", 57 | desc: "Capitalize the first letter of each sentence in English." 58 | }, 59 | textPunctuationSpace: { 60 | name: "Smartly insert space between text and punctuation", 61 | desc: "Insert space between text and punctuation intelligently." 62 | }, 63 | spaceStrategyInlineCode: { 64 | name: "Space strategy between inline code and text", 65 | desc: "No requirement: No space requirement between this category block and the surrounding text. " + 66 | "Soft space: Only requires a soft space between this category block and the surrounding blocks. " + 67 | "Soft space example: If the adjacent text on the left side of the current block is full-width punctuation like . , ; ? etc., and the adjacent text on the right side of the current block is all full-width or half-width punctuation. " + 68 | "Strict space: Strictly add spaces between the current block and the adjacent text." 69 | }, 70 | spaceStrategyInlineFormula: { 71 | name: "Space strategy between inline formula and text", 72 | desc: "Define the spacing strategy between inline formulas and text." 73 | }, 74 | spaceStrategyLinkText: { 75 | name: "Space strategy between link and text", 76 | desc: "Define the spacing strategy between [[wikilink]] [mdlink](...) and text." 77 | }, 78 | userDefinedRegexpSwitch: { 79 | name: "User Defined RegExp Switch", 80 | desc: "Toggle custom regular expressions, preventing formatting and setting space strategy between matched content and other text." 81 | }, 82 | userDefinedRegexp: { 83 | name: "User-defined Regular Expression, one expression per line", 84 | desc: "User-defined regular expression, matched to the content is not formatted, one expression per line, do not feel free to add spaces at the end of the line."+ 85 | "The end of each line of three characters fixed as | and two space strategy symbols, space strategy symbols for - = +, respectively, on behalf of not requiring spaces (-), soft spaces (=), strict spaces (+)."+ 86 | "These two space strategy symbols are the space strategy for the left and right sides of the matching block respectively" 87 | }, 88 | excludeFoldersFiles: { 89 | name: "Exclude Folders/Files", 90 | desc: "This plugin will parse each line as an exclude folder or file. For example: DailyNote/, DailyNote/WeekNotes/, DailyNote/test.md" 91 | }, 92 | fixMacOSContextMenu: { 93 | name: "Fix MacOS context-menu cursor position (Need to restart Obsidian)", 94 | desc: "Fix the issue where the cursor jumps to the next line when the context menu is invoked on MacOS (requires restarting Obsidian)." 95 | }, 96 | fixMicrosoftIME: { 97 | name: "Fix Microsoft Input Method Issue", 98 | desc: "Adapt for older versions of Microsoft Input Method." 99 | }, 100 | strictLineBreaks: { 101 | name: "Strict Line breaks Mode", 102 | desc: "In strict line breaks mode, pressing Enter once in normal text lines will produce two line breaks or two spaces and Enter." 103 | }, 104 | enhanceModA: { 105 | name: "Enhance Mod+A selection in text", 106 | desc: "First select the current line, second select the current text block, third select the entire text." 107 | }, 108 | collapsePersistentEnter: { 109 | name: "Collapse Persistent Enter", 110 | desc: "Adds same-level header line without expanding when pressing Enter on a collapsed header." 111 | }, 112 | puncRectify: { 113 | name: "Punc rectify", 114 | desc: "Automatically convert English punctuation (, . ? !) between Chinese characters to full-width punctuation during typing (reversible)." 115 | }, 116 | printDebugInfo: { 117 | name: "Print debug info in console", 118 | desc: "Print debug information in the console." 119 | }, 120 | selectionReplaceRule: { 121 | name: "Selection Replace Rule", 122 | desc: "User defined Selection Replace Rule" 123 | }, 124 | deleteRule: { 125 | name: "Delete Rule", 126 | desc: "Rule: Use | to indicate the cursor position. Tips: Using | to indicate the cursor position." 127 | }, 128 | convertRule: { 129 | name: "Convert Rule", 130 | desc: "Rule: Use | to indicate the cursor position. Tips: Using | to indicate the cursor position." 131 | }, 132 | trigger: { 133 | name: "Trigger" 134 | }, 135 | left: { 136 | name: "Left" 137 | }, 138 | right: { 139 | name: "Right" 140 | }, 141 | oldPattern: { 142 | name: "Old Pattern" 143 | }, 144 | newPattern: { 145 | name: "New Pattern" 146 | } 147 | }, 148 | headers: { 149 | main: "Obsidian EasyTyping Plugin", 150 | githubDetail: "More detail is in Github: ", 151 | enhancedEditing: "Enhanced Editing Setting", 152 | customizeEditRule: "Customize Edit Convertion Rule", 153 | autoformatSetting: "Autoformat Setting", 154 | detailedSetting: "Detailed Setting Below", 155 | customRegexpBlock: "Custom regular expressions block", 156 | excludeFoldersFiles: "Exclude Folders/Files", 157 | experimentalFeatures: "Experimental Features", 158 | aboutRegexp: { 159 | header: "For knowledge about regular expressions, see ", 160 | text: "Yifeng Nguyen: A Concise Tutorial on Regular Expressions", 161 | }, 162 | instructionsRegexp: { 163 | header: "Instructions and examples for using regular expression rules: ", 164 | text:"Customizing Regular Expression Rules", 165 | }, 166 | customizeSelectionRule: "Customize Selection Replace Rule", 167 | customizeDeleteRule: "Customize Delete Rule", 168 | customizeConvertRule: "Customize Convert Rule", 169 | editSelectionReplaceRule: "Edit Selection Replace Rule", 170 | }, 171 | dropdownOptions: { 172 | enterTwice: "Enter Twice", 173 | twoSpace: "Two Space", 174 | mixMode: "Mix Mode", 175 | onlyWhenTyping: "Only When Typing", 176 | globally: "Work Globally", 177 | noRequire: "No Require", 178 | softSpace: "Soft Space", 179 | strictSpace: "Strict Space", 180 | dummy: "Dummy", 181 | smart: "Smart" 182 | }, 183 | toolTip: { 184 | switch: "Switch", 185 | editRule: "Edit rule", 186 | removeRule: "Remove rule", 187 | addRule: "Add Rule", 188 | }, 189 | placeHolder: { 190 | triggerSymbol: "Trigger Symbol", 191 | newLeftSideString: "New Left Side String", 192 | newRightSideString: "New Right Side String", 193 | addRule: "Add Rule", 194 | noticeInvaidTrigger: "Inlvalid trigger, trigger must be a symbol of length 1 or symbol ——, ……", 195 | noticeWarnTriggerExists: "warning! Trigger %s is already exist!", 196 | noticeMissingInput: "missing input", 197 | beforeDelete: "Before Delete", 198 | newPattern: "New Pattern", 199 | noticeInvaidTriggerPatternContainSymbol: "Inlvalid trigger, pattern must contain symbol \| which indicate cursor position", 200 | beforeConvert: "Before Convert", 201 | noticeInvalidPatternString:"Invalid pattern string!", 202 | }, 203 | button: { 204 | update: "Update", 205 | } 206 | }; 207 | 208 | export default locale; 209 | -------------------------------------------------------------------------------- /src/lang/locale/index.ts: -------------------------------------------------------------------------------- 1 | export { default as enUS } from './en-US'; 2 | export { default as zhCN } from './zh-CN'; 3 | export { default as ruRU } from "./ru-RU"; 4 | export { default as zhTW } from "./zh-TW"; 5 | 6 | // TODO: wrote a langUtlis, who return labels 7 | // from language, if label not exist on, for example, 8 | // on ruRU, langUtils return label from enUS or from zhCH(where label exist) -------------------------------------------------------------------------------- /src/lang/locale/ru-RU.ts: -------------------------------------------------------------------------------- 1 | import { enUS } from "."; 2 | 3 | // machine translation 4 | const locale: typeof enUS = { 5 | settings: { 6 | symbolAutoPair: { 7 | name: "Автоматическое добавление и удаление символов пара", 8 | desc: "Добавить автозакрытие и автозакрытие для различных символов, таких как 《》, “”, 「」, 『』, 【】 и т.д." 9 | }, 10 | selectionReplace: { 11 | name: "Улучшение замены выделенного текста", 12 | desc: "Улучшенное редактирование выделенного текста, например, нажатие ¥ → $выделенный текст$, нажатие · → `выделенный текст`, 《 → 《выделенный текст》 и т.д." 13 | }, 14 | fullWidthToHalfWidth: { 15 | name: "Конвертация последовательных полноширинных символов в полуширинные", 16 | desc: "Конвертация последовательных полноширинных символов в полуширинные, например, 。。→ ., !!→ !, 》》→ >" 17 | }, 18 | basicInputEnhance: { 19 | name: "Улучшение базового ввода символов для Obsidian", 20 | desc: "Улучшение базового ввода для Obsidian, например, 【【| → [[|]], начало с 、→ /, начало с 》→ >, ··| → `|`, `·|` становится кодовым блоком, ¥¥| → $|$" 21 | }, 22 | codeblockEdit: { 23 | name: "Улучшение редактирования кодовых блоков", 24 | desc: "Улучшение редактирования в кодовых блоках (Tab, удаление, вставка, Cmd/Ctrl+A выделение)." 25 | }, 26 | backspaceEdit: { 27 | name: "Улучшение удаления", 28 | desc: "Улучшение удаления пустых элементов списка или пустых строк ссылок." 29 | }, 30 | tabOut: { 31 | name: "Tabout", 32 | desc: "Выйти из встроенного кода или парных символов." 33 | }, 34 | autoFormatting: { 35 | name: "Автоформатирование при наборе текста", 36 | desc: "Включение/выключение автоформатирования текста во время редактирования документа." 37 | }, 38 | spaceBetweenChineseEnglish: { 39 | name: "Пробел между китайскими и английскими символами", 40 | desc: "Вставка пробела между китайскими и английскими символами." 41 | }, 42 | spaceBetweenChineseNumber: { 43 | name: "Пробел между китайскими символами и числами", 44 | desc: "Вставка пробела между китайскими символами и числами." 45 | }, 46 | spaceBetweenEnglishNumber: { 47 | name: "Пробел между английскими символами и числами", 48 | desc: "Вставка пробела между английскими символами и числами." 49 | }, 50 | quoteSpace: { 51 | name: "Пробел между символом > и текстом", 52 | desc: "Вставка пробела между символом > и текстом." 53 | }, 54 | deleteSpaceBetweenChinese: { 55 | name: "Удаление пробела между китайскими символами", 56 | desc: "Удаление пробелов между китайскими символами." 57 | }, 58 | capitalizeFirstLetter: { 59 | name: "Заглавная буква в начале каждого предложения", 60 | desc: "Преобразование первой буквы каждого предложения в английском в заглавную." 61 | }, 62 | textPunctuationSpace: { 63 | name: "Интеллектуальная вставка пробела между текстом и пунктуацией", 64 | desc: "Интеллектуальная вставка пробела между текстом и пунктуацией." 65 | }, 66 | spaceStrategyInlineCode: { 67 | name: "Стратегия пробелов между встроенным кодом и текстом", 68 | desc: "Нет требований: Нет требований к пробелам между этим блоком категории и окружающим текстом. " + 69 | "Мягкий пробел: Требуется только мягкий пробел между этим блоком категории и окружающими блоками. " + 70 | "Пример мягкого пробела: Если прилегающий текст слева от текущего блока - это полноширинная пунктуация, такая как . , ; ? и т.д., а прилегающий текст справа от текущего блока - это вся полноширинная или полуширинная пунктуация. " + 71 | "Строгий пробел: Строгое добавление пробелов между текущим блоком и прилегающим текстом." 72 | }, 73 | spaceStrategyInlineFormula: { 74 | name: "Стратегия пробелов между встроенной формулой и текстом", 75 | desc: "Определение стратегии пробелов между встроенными формулами и текстом." 76 | }, 77 | spaceStrategyLinkText: { 78 | name: "Стратегия пробелов между ссылкой и текстом", 79 | desc: "Определение стратегии пробелов между [[викиссылками]] [markdown-ссылками](...) и текстом." 80 | }, 81 | userDefinedRegexpSwitch: { 82 | name: "Переключение пользовательских регулярных выражений", 83 | desc: "Включение/выключение пользовательских регулярных выражений, предотвращение форматирования и установка стратегии пробелов между совпадающим содержимым и другим текстом." 84 | }, 85 | userDefinedRegexp: { 86 | name: "Пользовательское регулярное выражение, одно выражение на строку", 87 | desc: "Пользовательское регулярное выражение, совпадающее с содержимым, не форматируется, одно выражение на строку, не добавляйте пробелы в конце строки."+ 88 | "Конец каждой строки фиксирован тремя символами: | и двумя символами стратегии пробелов, символы стратегии пробелов - это - = +, которые соответственно обозначают отсутствие требования пробелов (-), мягкие пробелы (=), строгие пробелы (+)."+ 89 | "Эти два символа стратегии пробелов являются стратегией пробелов для левой и правой сторон совпадающего блока соответственно" 90 | }, 91 | excludeFoldersFiles: { 92 | name: "Исключить папки/файлы", 93 | desc: "Этот плагин будет обрабатывать каждую строку как исключаемую папку или файл. Например: DailyNote/, DailyNote/WeekNotes/, DailyNote/test.md" 94 | }, 95 | fixMacOSContextMenu: { 96 | name: "Исправление положения курсора контекстного меню MacOS (требуется перезапуск Obsidian)", 97 | desc: "Исправление проблемы, когда курсор перескакивает на следующую строку при вызове контекстного меню на MacOS (требуется перезапуск Obsidian)." 98 | }, 99 | fixMicrosoftIME: { 100 | name: "Исправление проблемы с Microsoft Input Method", 101 | desc: "Адаптация для старых версий Microsoft Input Method." 102 | }, 103 | strictLineBreaks: { 104 | name: "Режим строгих разрывов строк", 105 | desc: "В режиме строгих разрывов строк, однократное нажатие Enter в обычных текстовых строках создаст два разрыва строки или два пробела и Enter." 106 | }, 107 | enhanceModA: { 108 | name: "Улучшить выделение Mod+A в тексте", 109 | desc: "Сначала выделите текущую строку, затем выделите текущий текстовый блок, затем выделите весь текст." 110 | }, 111 | collapsePersistentEnter: { 112 | name: "Сворачивание постоянного ввода", 113 | desc: "Добавляет одинаковые уровни заголовков без раскрытия при нажатии Enter на свернутом заголовке." 114 | }, 115 | puncRectify: { 116 | name: "Коррекция пунктуации", 117 | desc: "Автоматическая конвертация английской пунктуации (, . ? !) между китайскими символами в полноширинную пунктуацию при наборе текста (обратимо)." 118 | }, 119 | printDebugInfo: { 120 | name: "Вывод отладочной информации в консоль", 121 | desc: "Вывод отладочной информации в консоль." 122 | }, 123 | selectionReplaceRule: { 124 | name: "Правило замены выделенного текста", 125 | desc: "Пользовательское правило замены выделенного текста" 126 | }, 127 | deleteRule: { 128 | name: "Правило удаления", 129 | desc: "Правило: Используйте | для указания позиции курсора. Подсказка: Использование | для указания позиции курсора." 130 | }, 131 | convertRule: { 132 | name: "Правило преобразования", 133 | desc: "Правило: Используйте | для указания позиции курсора. Подсказка: Использование | для указания позиции курсора." 134 | }, 135 | trigger: { 136 | name: "Триггер" 137 | }, 138 | left: { 139 | name: "Левый" 140 | }, 141 | right: { 142 | name: "Правый" 143 | }, 144 | oldPattern: { 145 | name: "Старый шаблон" 146 | }, 147 | newPattern: { 148 | name: "Новый шаблон" 149 | } 150 | }, 151 | headers: { 152 | main: "Плагин Obsidian EasyTyping", 153 | githubDetail: "Подробнее на Github: ", 154 | enhancedEditing: "Настройка улучшенного редактирования", 155 | customizeEditRule: "Настройка правила преобразования редактирования", 156 | autoformatSetting: "Настройка автоформатирования", 157 | detailedSetting: "Подробная настройка ниже", 158 | customRegexpBlock: "Блок пользовательских регулярных выражений", 159 | excludeFoldersFiles: "Исключить папки/файлы", 160 | experimentalFeatures: "Экспериментальные функции", 161 | aboutRegexp: { 162 | header: "Для информации о регулярных выражениях см. ", 163 | text: "Yifeng Nguyen: Краткое руководство по регулярным выражениям", 164 | }, 165 | instructionsRegexp: { 166 | header: "Инструкции и примеры использования правил регулярных выражений: ", 167 | text:"Настройка пользовательских правил регулярных выражений", 168 | }, 169 | customizeSelectionRule: "Настройка правила замены выделенного текста", 170 | customizeDeleteRule: "Настройка правила удаления", 171 | customizeConvertRule: "Настройка правила преобразования", 172 | editSelectionReplaceRule: "Редактирование правила замены выделенного текста", 173 | }, 174 | dropdownOptions: { 175 | enterTwice: "Дважды нажмите Enter", 176 | twoSpace: "Два пробела", 177 | mixMode: "Смешанный режим", 178 | onlyWhenTyping: "Только при наборе текста", 179 | globally: "Работать глобально", 180 | noRequire: "Нет требований", 181 | softSpace: "Мягкий пробел", 182 | strictSpace: "Строгий пробел", 183 | dummy: "Фиктивный", 184 | smart: "Умный" 185 | }, 186 | toolTip: { 187 | switch: "Переключить", 188 | editRule: "Редактировать правило", 189 | removeRule: "Удалить правило", 190 | addRule: "Добавить правило", 191 | }, 192 | placeHolder: { 193 | triggerSymbol: "Символ триггера", 194 | newLeftSideString: "Новая строка с левой стороны", 195 | newRightSideString: "Новая строка с правой стороны", 196 | addRule: "Добавить правило", 197 | noticeInvaidTrigger: "Недействительный триггер, триггер должен быть символом длиной 1 или символом ——, ……", 198 | noticeWarnTriggerExists: "Внимание! Триггер %s уже существует!", 199 | noticeMissingInput: "Отсутствует ввод", 200 | beforeDelete: "До удаления", 201 | newPattern: "Новый шаблон", 202 | noticeInvaidTriggerPatternContainSymbol: "Недействительный триггер, шаблон должен содержать символ \|, указывающий на позицию курсора", 203 | beforeConvert: "До преобразования", 204 | noticeInvalidPatternString:"Недействительная строка шаблона!", 205 | }, 206 | button: { 207 | update: "Обновить", 208 | } 209 | }; 210 | 211 | export default locale; 212 | -------------------------------------------------------------------------------- /src/lang/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import { enUS } from "."; 2 | 3 | const locale: typeof enUS = { 4 | settings: { 5 | symbolAutoPair: { 6 | name: "符号自动配对及删除配对", 7 | desc: "增加多种符号配对输入,配对删除,如《》, “”, 「」, 『』, 【】等" 8 | }, 9 | selectionReplace: { 10 | name: "选中文本替换增强", 11 | desc: "选中文本情况下的编辑增强,按¥→$选中的文本$, 按·→`选中的文本`,《 → 《选中的文本》等等" 12 | }, 13 | fullWidthToHalfWidth: { 14 | name: "连续输入全角符号转半角符号", 15 | desc: "连续输入全角符号转半角,。。→ .,!!→ !, 》》→ >" 16 | }, 17 | basicInputEnhance: { 18 | name: "Obsidian 的基础符号输入增强", 19 | desc: "Obsidian 的基础输入增强,如【【| → [[|]],句首的、→ /,句首的》→ >,··| → `|`, `·|` 变成代码块,¥¥| → $|$" 20 | }, 21 | codeblockEdit: { 22 | name: "增强代码块编辑", 23 | desc: "增强代码块内的编辑(Cmd/Ctrl+A 选中、Tab、删除、粘贴)" 24 | }, 25 | backspaceEdit: { 26 | name: "增强删除功能", 27 | desc: "增强删除空列表项或空引用行的功能" 28 | }, 29 | tabOut: { 30 | name: "Tab 键光标跳出", 31 | desc: "Tab 键跳出行内代码块或配对符号块" 32 | }, 33 | autoFormatting: { 34 | name: "输入时自动格式化", 35 | desc: "是否在编辑文档时自动格式化文本,自动格式化的总开关" 36 | }, 37 | spaceBetweenChineseEnglish: { 38 | name: "中文与英文之间的空格", 39 | desc: "在中文和英文之间插入空格,可撤销" 40 | }, 41 | spaceBetweenChineseNumber: { 42 | name: "中文与数字之间的空格", 43 | desc: "在中文和数字之间插入空格,可撤销" 44 | }, 45 | spaceBetweenEnglishNumber: { 46 | name: "英文与数字之间的空格", 47 | desc: "在英文和数字之间插入空格,可撤销" 48 | }, 49 | quoteSpace: { 50 | name: "引用符号 > 与文本之间自动空格", 51 | desc: "在引用符号 > 与文本之间自动插入空格,不可撤销" 52 | }, 53 | deleteSpaceBetweenChinese: { 54 | name: "删除中文字符间的空格", 55 | desc: "去除中文字符之间的空格,不可撤销" 56 | }, 57 | capitalizeFirstLetter: { 58 | name: "句首字母大写", 59 | desc: "英文每个句首字母大写,可撤销" 60 | }, 61 | textPunctuationSpace: { 62 | name: "文本和标点间空格", 63 | desc: "在文本和标点之间智能插入空格" 64 | }, 65 | spaceStrategyInlineCode: { 66 | name: "行内代码和文本之间的空格策略", 67 | desc: "无要求:对本类别块与左右文本没有空格的要求," + 68 | "软空格:对本类别块与周围区块只要求有软空格,软空格如当前块左边的临近文本为。,;?等全角标点,当前块右边的临近文本为所有全半角标点," + 69 | "严格空格:当前块与临近文本之间严格添加空格。" 70 | }, 71 | spaceStrategyInlineFormula: { 72 | name: "行内公式和文本之间的空格策略", 73 | desc: "定义行内公式和文本之间的空格策略" 74 | }, 75 | spaceStrategyLinkText: { 76 | name: "链接和文本之间的空格策略", 77 | desc: "定义 [[wikilink]] [mdlink](...) 和文本之间的空格策略" 78 | }, 79 | userDefinedRegexpSwitch: { 80 | name: "用户定义的正则表达式开关", 81 | desc: "自定义正则表达式开关,匹配到的内容不进行格式化,且可以设置匹配到的内容块与其他内容之间的空格策略" 82 | }, 83 | userDefinedRegexp: { 84 | name: "用户定义的正则表达式", 85 | desc: "用户自定义正则表达式,匹配到的内容不进行格式化,每行一个表达式,行尾不要随意加空格。" + 86 | "每行末尾3个字符的固定为|和两个空格策略符号,空格策略符号为-=+,分别代表不要求空格(-),软空格(=),严格空格(+)。" + 87 | "这两个空格策略符号分别为匹配区块的左右两边的空格策略" 88 | }, 89 | excludeFoldersFiles: { 90 | name: "排除文件夹/文件", 91 | desc: "该插件将每行解析为一个排除文件夹或文件。例如:DailyNote/, DailyNote/WeekNotes/, DailyNote/test.md" 92 | }, 93 | fixMacOSContextMenu: { 94 | name: "修复 MacOS 右键菜单光标位置", 95 | desc: "修复 MacOS 鼠标右键呼出菜单时光标跳到下一行的问题 (需要重启 Obsidian 生效)" 96 | }, 97 | fixMicrosoftIME: { 98 | name: "修复微软输入法问题", 99 | desc: "适配旧版微软输入法" 100 | }, 101 | strictLineBreaks: { 102 | name: "严格换行模式回车增强", 103 | desc: "严格换行的设置下,在普通文本行进行一次回车会根据模式产生两个换行符或者两个空格和回车" 104 | }, 105 | enhanceModA: { 106 | name: "增强 Ctrl/Cmd+A 功能", 107 | desc: "第一次选中当前行,第二次选中当前文本块,第三次选中全文。" 108 | }, 109 | collapsePersistentEnter: { 110 | name: "标题折叠保序", 111 | desc: "在折叠的同级标题行按回车不会展开,直接添加同级标题行" 112 | }, 113 | puncRectify: { 114 | name: "标点矫正", 115 | desc: "仅在输入过程中,中文间的英文标点(,.?!)自动转换为全角(可撤销)" 116 | }, 117 | printDebugInfo: { 118 | name: "在控制台输出调试信息", 119 | desc: "在控制台输出调试信息" 120 | }, 121 | selectionReplaceRule: { 122 | name: "选中替换规则", 123 | desc: "用户定义的选择替换规则" 124 | }, 125 | deleteRule: { 126 | name: "删除规则", 127 | desc: "规则:用 | 代表光标位置,必须包含光标。提示:使用 | 表示光标位置。" 128 | }, 129 | convertRule: { 130 | name: "转换规则", 131 | desc: "规则:用 | 代表光标位置,必须包含光标。提示:使用 | 表示光标位置。" 132 | }, 133 | trigger: { 134 | name: "触发器" 135 | }, 136 | left: { 137 | name: "左" 138 | }, 139 | right: { 140 | name: "右" 141 | }, 142 | oldPattern: { 143 | name: "旧模式" 144 | }, 145 | newPattern: { 146 | name: "新模式" 147 | } 148 | }, 149 | headers: { 150 | main: "Obsidian EasyTyping 插件", 151 | githubDetail: "详情见 Github:", 152 | enhancedEditing: "增强编辑设置", 153 | customizeEditRule: "自定义编辑转换规则", 154 | autoformatSetting: "自动格式化设置", 155 | detailedSetting: "详细设置如下", 156 | customRegexpBlock: "自定义正则区块", 157 | excludeFoldersFiles: "指定文件不自动格式化", 158 | experimentalFeatures: "实验功能", 159 | aboutRegexp: { 160 | header:"正则表达式相关知识,见 ", 161 | text: "《阮一峰:正则表达式简明教程》", 162 | }, 163 | instructionsRegexp: { 164 | header: "正则表达式规则使用说明与示例: ", 165 | text:"自定义正则表达式规则", 166 | }, 167 | customizeSelectionRule: "自定义选中文本编辑增强规则", 168 | customizeDeleteRule: "自定义删除编辑增强规则", 169 | customizeConvertRule: "自定义编辑转换规则", 170 | editSelectionReplaceRule: "编辑选中替换规则", 171 | }, 172 | dropdownOptions: { 173 | enterTwice: "两次回车", 174 | twoSpace: "加两个空格", 175 | mixMode: "混合模式", 176 | onlyWhenTyping: "输入时生效", 177 | globally: "全局生效", 178 | noRequire: "无要求", 179 | softSpace: "软空格", 180 | strictSpace: "严格空格", 181 | dummy: "呆空格", 182 | smart: "智能空格" 183 | }, 184 | toolTip: { 185 | switch: "功能开关", 186 | editRule: "编辑规则", 187 | removeRule: "删除规则", 188 | addRule: "添加规则", 189 | }, 190 | placeHolder: { 191 | triggerSymbol: "触发符", 192 | newLeftSideString: "左边符号", 193 | newRightSideString: "右边符号", 194 | addRule: "添加规则", 195 | noticeInvaidTrigger: "无效的触发符, 触发符必须是单字符或者是 ——、……", 196 | noticeWarnTriggerExists: "无效规则! 触发符 %s 已存在", 197 | noticeMissingInput: "missing input", 198 | beforeDelete: "删除前|", 199 | newPattern: "触发规则后字符串模式", 200 | noticeInvaidTriggerPatternContainSymbol: "无效规则, 转换前模式必须包含代表光标位置的符号 \|", 201 | beforeConvert: "转换前|", 202 | noticeInvalidPatternString:"Invalid pattern string!", 203 | }, 204 | button: { 205 | update: "更新", 206 | } 207 | }; 208 | 209 | export default locale; 210 | -------------------------------------------------------------------------------- /src/lang/locale/zh-TW.ts: -------------------------------------------------------------------------------- 1 | import { enUS } from "."; 2 | 3 | const locale: typeof enUS = { 4 | settings: { 5 | symbolAutoPair: { 6 | name: "符號自動配對及刪除配對", 7 | desc: "增加多種符號配對輸入,配對刪除,如《》, “”, 「」, 『』, 【】等" 8 | }, 9 | selectionReplace: { 10 | name: "選中文本替換增强", 11 | desc: "選中文本情況下的編輯增强,按¥→$選中的文本$, 按·→`選中的文本`,《 → 《選中的文本》等等" 12 | }, 13 | fullWidthToHalfWidth: { 14 | name: "連續輸入全角符號轉半角符號", 15 | desc: "連續輸入全角符號轉半角,。。→ .,!!→ !, 》》→ >" 16 | }, 17 | basicInputEnhance: { 18 | name: "Obsidian 的基礎符號輸入增强", 19 | desc: "Obsidian 的基礎輸入增强,如【【| → [[|]],句首的、→ /,句首的》→ >,··| → `|`, `·|` 變成代碼塊,¥¥| → $|$" 20 | }, 21 | codeblockEdit: { 22 | name: "增强代碼塊編輯", 23 | desc: "增强代碼塊內的編輯(Cmd/Ctrl+A 選中、Tab、刪除、粘貼)" 24 | }, 25 | backspaceEdit: { 26 | name: "增強刪除功能", 27 | desc: "增強刪除空列表項或空引用行的功能" 28 | }, 29 | tabOut: { 30 | name: "Tab 键光标跳出", 31 | desc: "Tab 键跳出行內代碼塊或配對符號塊" 32 | }, 33 | autoFormatting: { 34 | name: "輸入時自動格式化", 35 | desc: "是否在編輯文檔時自動格式化文本,自動格式化的總開關" 36 | }, 37 | spaceBetweenChineseEnglish: { 38 | name: "中文與英文之間的空格", 39 | desc: "在中文和英文之間插入空格,可取消" 40 | }, 41 | spaceBetweenChineseNumber: { 42 | name: "中文與數字之間的空格", 43 | desc: "在中文和數字之間插入空格,可取消" 44 | }, 45 | spaceBetweenEnglishNumber: { 46 | name: "英文與數字之間的空格", 47 | desc: "在英文和數字之間插入空格,可取消" 48 | }, 49 | quoteSpace: { 50 | name: "引用符號 > 與文本之間自動空格", 51 | desc: "在引用符號 > 與文本之間自動插入空格,不可取消" 52 | }, 53 | deleteSpaceBetweenChinese: { 54 | name: "刪除中文字符間的空格", 55 | desc: "去除中文字符之間的空格,不可取消" 56 | }, 57 | capitalizeFirstLetter: { 58 | name: "句首字母大寫", 59 | desc: "英文每個句首字母大寫,可取消" 60 | }, 61 | smartInsertSpace: { 62 | name: "文本和標點間空格", 63 | desc: "在文本和標點之間智能插入空格" 64 | }, 65 | spaceStrategyInlineCode: { 66 | name: "行內代碼和文本之間的空格策略", 67 | desc: "無要求:對本類別塊與左右文本沒有空格的要求," + 68 | "軟空格:對本類別塊與周圍區塊只要求有軟空格,軟空格如當前塊左邊的臨近文本為。,;?等全角標點,當前塊右邊的臨近文本為所有全半角標點," + 69 | "嚴格空格:當前塊與臨近文本之間嚴格添加空格。" 70 | }, 71 | spaceStrategyInlineFormula: { 72 | name: "行內公式和文本之間的空格策略", 73 | desc: "定義行內公式和文本之間的空格策略" 74 | }, 75 | spaceStrategyLinkText: { 76 | name: "連結和文本之間的空格策略", 77 | desc: "定義 [[wikilink]] [mdlink](...) 和文本之間的空格策略" 78 | }, 79 | userDefinedRegexpSwitch: { 80 | name: "用戶定義的正則表達式開關", 81 | desc: "自定義正則表達式開關,匹配到的內容不進行格式化,且可以設置匹配到的內容塊與其他內容之間的空格策略" 82 | }, 83 | userDefinedRegexp: { 84 | name: "用戶定義的正則表達式", 85 | desc: "用戶自定義正則表達式,匹配到的內容不進行格式化,每行一個表達式,行尾不要隨意加空格。" + 86 | "每行末尾3個字符的固定為|和兩個空格策略符號,空格策略符號為-=+,分別代表不要求空格(-),軟空格(=),嚴格空格(+)。" + 87 | "這兩個空格策略符號分別為匹配區塊的左右兩邊的空格策略" 88 | }, 89 | excludeFoldersFiles: { 90 | name: "排除文件夾/文件", 91 | desc: "該插件將每行解析為一個排除文件夾或文件。例如:DailyNote/, DailyNote/WeekNotes/, DailyNote/test.md" 92 | }, 93 | fixMacOSContextMenu: { 94 | name: "修復 MacOS 右鍵菜單光標位置", 95 | desc: "修復 MacOS 鼠標右鍵呼出菜單時光標跳到下一行的問題 (需要重啟 Obsidian 生效)" 96 | }, 97 | fixMicrosoftIME: { 98 | name: "修復微軟輸入法問題", 99 | desc: "適配舊版微軟輸入法" 100 | }, 101 | strictLineBreaks: { 102 | name: "嚴格換行模式回車增強", 103 | desc: "嚴格換行的設置下,在普通文本行進行一次回車會根據模式產生兩個換行符或者兩個空格和回車" 104 | }, 105 | enhanceModA: { 106 | name: "增強 Mod+A 功能", 107 | desc: "第一次選中當前行,第二次選中當前文本塊,第三次選中全文。" 108 | }, 109 | collapsePersistentEnter: { 110 | name: "標題折叠保序", 111 | desc: "在折叠的同级標題行按回車不會展開,直接添加同級標題行" 112 | }, 113 | puncRectify: { 114 | name: "標點矫正", 115 | desc: "僅在輸入過程中,中文間的英文標點(,.?!)自動轉換為全角(可取消)" 116 | }, 117 | printDebugInfo: { 118 | name: "在控制台輸出調試資訊", 119 | desc: "在控制台輸出調試資訊" 120 | }, 121 | selectionReplaceRule: { 122 | name: "選中替換規則", 123 | desc: "用戶定義的選中替換規則" 124 | }, 125 | deleteRule: { 126 | name: "刪除規則", 127 | desc: "規則:用 | 代表光標位置,必須包含光標。提示:使用 | 表示光標位置。" 128 | }, 129 | convertRule: { 130 | name: "轉換規則", 131 | desc: "規則:用 | 代表光標位置,必須包含光標。提示:使用 | 表示光標位置。" 132 | }, 133 | trigger: { 134 | name: "觸發器" 135 | }, 136 | left: { 137 | name: "左" 138 | }, 139 | right: { 140 | name: "右" 141 | }, 142 | oldPattern: { 143 | name: "舊模式" 144 | }, 145 | newPattern: { 146 | name: "新模式" 147 | } 148 | }, 149 | headers: { 150 | main: "Obsidian EasyTyping 插件", 151 | githubDetail: "詳情見 Github:", 152 | enhancedEditing: "增強編輯設置", 153 | customizeEditRule: "自定義編輯轉換規則", 154 | autoformatSetting: "自動格式化設置", 155 | detailedSetting: "詳細設置如下", 156 | customRegexpBlock: "自定義正則區塊", 157 | excludeFoldersFiles: "指定文件不自動格式化", 158 | experimentalFeatures: "實驗功能", 159 | aboutRegexp: { 160 | header: "正則表達式相關知識,見 ", 161 | text: "《阮一峰:正則表達式簡明教程》", 162 | }, 163 | instructionsRegexp: { 164 | header: "正則表達式規則使用說明與示例: ", 165 | text: "自定義正則表達式規則", 166 | }, 167 | customizeSelectionRule: "自定義選中文本編輯增强規則", 168 | customizeDeleteRule: "自定義刪除編輯增强規則", 169 | customizeConvertRule: "自定義編輯轉換規則", 170 | editSelectionReplaceRule: "編輯選中替換規則", 171 | }, 172 | dropdownOptions: { 173 | enterTwice: "兩次回車", 174 | twoSpace: "加兩個空格", 175 | mixMode: "混合模式", 176 | onlyWhenTyping: "輸入時生效", 177 | globally: "全局生效", 178 | noRequire: "無要求", 179 | softSpace: "軟空格", 180 | strictSpace: "嚴格空格", 181 | dummy: "呆空格", 182 | smart: "智能空格" 183 | }, 184 | toolTip: { 185 | switch: "功能開關", 186 | editRule: "編輯規則", 187 | removeRule: "刪除規則", 188 | addRule: "添加規則", 189 | }, 190 | placeHolder: { 191 | triggerSymbol: "觸發符", 192 | newLeftSideString: "左邊符號", 193 | newRightSideString: "右邊符號", 194 | addRule: "添加規則", 195 | noticeInvaidTrigger: "無效的觸發符, 觸發符必須是單字符或者是 ——、……", 196 | noticeWarnTriggerExists: "無效規則! 觸發符 %s 已存在", 197 | noticeMissingInput: "missing input", 198 | beforeDelete: "刪除前|", 199 | newPattern: "觸發規則後字串模式", 200 | noticeInvaidTriggerPatternContainSymbol: "無效規則, 轉換前模式必須包含代表光標位置的符號 |", 201 | beforeConvert: "轉換前|", 202 | noticeInvalidPatternString: "Invalid pattern string!", 203 | }, 204 | button: { 205 | update: "更新", 206 | } 207 | }; 208 | 209 | export default locale; -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { SpaceState, string2SpaceState } from 'src/core'; 2 | import { App, TextComponent, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, Workspace, WorkspaceLeaf, TextAreaComponent, moment } from 'obsidian'; 3 | import EasyTypingPlugin from './main'; 4 | import { showString, findFirstPipeNotPrecededByBackslash } from './utils'; 5 | import { enUS, ruRU, zhCN, zhTW } from './lang/locale'; 6 | import {sprintf} from "sprintf-js"; 7 | import { setDebug } from './utils'; 8 | 9 | export interface PairString { 10 | left: string; 11 | right: string; 12 | } 13 | 14 | export interface ConvertRule { 15 | before: PairString; 16 | after: PairString; 17 | after_pattern?: string; 18 | } 19 | 20 | export enum RuleType {delete= "Delete Rule", convert='Convert Rule'} 21 | export enum WorkMode { OnlyWhenTyping = "typing", Globally = "global" } 22 | export enum StrictLineMode { EnterTwice = "enter_twice", TwoSpace = "two_space", Mix = "mix_mode" } 23 | 24 | export interface EasyTypingSettings { 25 | Tabout: boolean; 26 | SelectionEnhance: boolean; 27 | IntrinsicSymbolPairs: boolean; 28 | BaseObEditEnhance: boolean; 29 | FW2HWEnhance: boolean; 30 | BetterCodeEdit: boolean; 31 | BetterBackspace: boolean; 32 | AutoFormat: boolean; 33 | ExcludeFiles: string; 34 | AutoCapital: boolean; 35 | AutoCapitalMode: WorkMode; 36 | ChineseEnglishSpace: boolean; 37 | EnglishNumberSpace: boolean; 38 | QuoteSpace: boolean; 39 | ChineseNoSpace: boolean; 40 | ChineseNumberSpace: boolean; 41 | PunctuationSpace: boolean; 42 | PunctuationSpaceMode: WorkMode; 43 | InlineCodeSpaceMode: SpaceState; 44 | InlineFormulaSpaceMode: SpaceState; 45 | InlineLinkSpaceMode: SpaceState; 46 | InlineLinkSmartSpace: boolean; 47 | UserDefinedRegSwitch: boolean; 48 | UserDefinedRegExp: string; 49 | debug: boolean; 50 | 51 | userSelRepRuleTrigger: string[]; 52 | userSelRepRuleValue: PairString[]; 53 | userDeleteRulesStrList: [string, string][]; 54 | userConvertRulesStrList: [string, string][]; 55 | userSelRuleSettingsOpen: boolean; 56 | userDelRuleSettingsOpen: boolean; 57 | userCvtRuleSettingsOpen: boolean; 58 | 59 | StrictModeEnter: boolean; 60 | StrictLineMode: StrictLineMode; 61 | EnhanceModA: boolean; 62 | PuncRectify: boolean; 63 | TryFixChineseIM: boolean; 64 | FixMacOSContextMenu: boolean; 65 | TryFixMSIME: boolean; 66 | CollapsePersistentEnter: boolean; 67 | } 68 | 69 | export const DEFAULT_SETTINGS: EasyTypingSettings = { 70 | Tabout: true, 71 | SelectionEnhance: true, 72 | IntrinsicSymbolPairs: true, 73 | BaseObEditEnhance: true, 74 | FW2HWEnhance: true, 75 | BetterCodeEdit: true, 76 | BetterBackspace: true, 77 | AutoFormat: true, 78 | ExcludeFiles: "", 79 | ChineseEnglishSpace: true, 80 | ChineseNumberSpace: true, 81 | EnglishNumberSpace: true, 82 | ChineseNoSpace: true, 83 | QuoteSpace: true, 84 | PunctuationSpace: true, 85 | AutoCapital: true, 86 | AutoCapitalMode: WorkMode.OnlyWhenTyping, 87 | PunctuationSpaceMode: WorkMode.OnlyWhenTyping, 88 | InlineCodeSpaceMode: SpaceState.soft, 89 | InlineFormulaSpaceMode: SpaceState.soft, 90 | InlineLinkSpaceMode: SpaceState.soft, 91 | InlineLinkSmartSpace: true, 92 | UserDefinedRegSwitch: true, 93 | UserDefinedRegExp: "{{.*?}}|++\n"+ 94 | "<.*?>|--\n" + 95 | "\\[\\!.*?\\][-+]{0,1}|-+\n"+ 96 | "(file:///|https?://|ftp://|obsidian://|zotero://|www.)[^\\s()《》。,,!?;;:“”‘’\\)\\(\\[\\]\\{\\}']+|--\n"+ 97 | "\n[a-zA-Z0-9_\\-.]+@[a-zA-Z0-9_\\-.]+|++\n"+ 98 | "(? { 157 | toggle.setValue(this.plugin.settings.IntrinsicSymbolPairs) 158 | .onChange(async (value) => { 159 | this.plugin.settings.IntrinsicSymbolPairs = value; 160 | await this.plugin.saveSettings(); 161 | }); 162 | }); 163 | 164 | new Setting(containerEl) 165 | .setName(locale.settings.selectionReplace.name) 166 | .setDesc(locale.settings.selectionReplace.desc) 167 | .addToggle((toggle) => { 168 | toggle.setValue(this.plugin.settings.SelectionEnhance) 169 | .onChange(async (value) => { 170 | this.plugin.settings.SelectionEnhance = value; 171 | await this.plugin.saveSettings(); 172 | }); 173 | }); 174 | 175 | new Setting(containerEl) 176 | .setName(locale.settings.fullWidthToHalfWidth.name) 177 | .setDesc(locale.settings.fullWidthToHalfWidth.desc) 178 | .addToggle((toggle) => { 179 | toggle.setValue(this.plugin.settings.FW2HWEnhance) 180 | .onChange(async (value) => { 181 | this.plugin.settings.FW2HWEnhance = value; 182 | await this.plugin.saveSettings(); 183 | }); 184 | }); 185 | 186 | new Setting(containerEl) 187 | .setName(locale.settings.basicInputEnhance.name) 188 | .setDesc(locale.settings.basicInputEnhance.desc) 189 | .addToggle((toggle) => { 190 | toggle.setValue(this.plugin.settings.BaseObEditEnhance) 191 | .onChange(async (value) => { 192 | this.plugin.settings.BaseObEditEnhance = value; 193 | await this.plugin.saveSettings(); 194 | }); 195 | }); 196 | 197 | new Setting(containerEl) 198 | .setName(locale.settings.codeblockEdit.name) 199 | .setDesc(locale.settings.codeblockEdit.desc) 200 | .addToggle((toggle) => { 201 | toggle.setValue(this.plugin.settings.BetterCodeEdit) 202 | .onChange(async (value) => { 203 | this.plugin.settings.BetterCodeEdit = value; 204 | await this.plugin.saveSettings(); 205 | }); 206 | }); 207 | 208 | new Setting(containerEl) 209 | .setName(locale.settings.backspaceEdit.name) 210 | .setDesc(locale.settings.backspaceEdit.desc) 211 | .addToggle((toggle) => { 212 | toggle.setValue(this.plugin.settings.BetterBackspace) 213 | .onChange(async (value) => { 214 | this.plugin.settings.BetterBackspace = value; 215 | await this.plugin.saveSettings(); 216 | }); 217 | }); 218 | 219 | 220 | new Setting(containerEl) 221 | .setName(locale.settings.tabOut.name) 222 | .setDesc(locale.settings.tabOut.desc) 223 | .addToggle((toggle) => { 224 | toggle.setValue(this.plugin.settings.Tabout) 225 | .onChange(async (value) => { 226 | this.plugin.settings.Tabout = value; 227 | await this.plugin.saveSettings(); 228 | }); 229 | }); 230 | 231 | containerEl.createEl('h2', { text: locale.headers.customizeEditRule }); 232 | this.buildUserSelRepRuleSetting(this.containerEl.createEl("details", { 233 | cls: "easytyping-nested-settings", 234 | attr: { 235 | ...(this.plugin.settings.userSelRuleSettingsOpen?{ open: true }:{}) 236 | } 237 | })) 238 | 239 | this.buildUserDeleteRuleSetting(this.containerEl.createEl("details", { 240 | cls: "easytyping-nested-settings", 241 | attr: { 242 | ...(this.plugin.settings.userDelRuleSettingsOpen?{ open: true }:{}) 243 | } 244 | })) 245 | 246 | this.buildUserConvertRuleSetting(this.containerEl.createEl("details", { 247 | cls: "easytyping-nested-settings", 248 | attr: { 249 | ...(this.plugin.settings.userCvtRuleSettingsOpen?{ open: true }:{}) 250 | } 251 | })) 252 | 253 | 254 | containerEl.createEl('h2', { text: locale.headers.autoformatSetting }); 255 | 256 | new Setting(containerEl) 257 | .setName(locale.settings.autoFormatting.name) 258 | .setDesc(locale.settings.autoFormatting.desc) 259 | .addToggle((toggle) => { 260 | toggle.setValue(this.plugin.settings.AutoFormat) 261 | .onChange(async (value) => { 262 | this.plugin.settings.AutoFormat = value; 263 | await this.plugin.saveSettings(); 264 | }); 265 | }); 266 | containerEl.createEl('p', { text: locale.headers.detailedSetting }); 267 | 268 | new Setting(containerEl) 269 | .setName(locale.settings.spaceBetweenChineseEnglish.name) 270 | .setDesc(locale.settings.spaceBetweenChineseEnglish.desc) 271 | .addToggle((toggle) => { 272 | toggle.setValue(this.plugin.settings.ChineseEnglishSpace).onChange(async (value) => { 273 | this.plugin.settings.ChineseEnglishSpace = value; 274 | await this.plugin.saveSettings(); 275 | }); 276 | }); 277 | 278 | new Setting(containerEl) 279 | .setName(locale.settings.spaceBetweenChineseNumber.name) 280 | .setDesc(locale.settings.spaceBetweenChineseNumber.desc) 281 | .addToggle((toggle) => { 282 | toggle.setValue(this.plugin.settings.ChineseNumberSpace).onChange(async (value) => { 283 | this.plugin.settings.ChineseNumberSpace = value; 284 | await this.plugin.saveSettings(); 285 | }); 286 | }); 287 | 288 | new Setting(containerEl) 289 | .setName(locale.settings.spaceBetweenEnglishNumber.name) 290 | .setDesc(locale.settings.spaceBetweenEnglishNumber.desc) 291 | .addToggle((toggle) => { 292 | toggle.setValue(this.plugin.settings.EnglishNumberSpace).onChange(async (value) => { 293 | this.plugin.settings.EnglishNumberSpace = value; 294 | await this.plugin.saveSettings(); 295 | }); 296 | }); 297 | 298 | new Setting(containerEl) 299 | .setName(locale.settings.deleteSpaceBetweenChinese.name) 300 | .setDesc(locale.settings.deleteSpaceBetweenChinese.desc) 301 | .addToggle((toggle) => { 302 | toggle.setValue(this.plugin.settings.ChineseNoSpace).onChange(async (value) => { 303 | this.plugin.settings.ChineseNoSpace = value; 304 | await this.plugin.saveSettings(); 305 | }); 306 | }); 307 | 308 | new Setting(containerEl) 309 | .setName(locale.settings.quoteSpace.name) 310 | .setDesc(locale.settings.quoteSpace.desc) 311 | .addToggle((toggle) => { 312 | toggle.setValue(this.plugin.settings.QuoteSpace).onChange(async (value) => { 313 | this.plugin.settings.QuoteSpace = value; 314 | await this.plugin.saveSettings(); 315 | }); 316 | }); 317 | 318 | new Setting(containerEl) 319 | .setName(locale.settings.capitalizeFirstLetter.name) 320 | .setDesc(locale.settings.capitalizeFirstLetter.desc) 321 | .addDropdown((dropdown) => { 322 | dropdown.addOption(WorkMode.OnlyWhenTyping, locale.dropdownOptions.onlyWhenTyping); 323 | dropdown.addOption(WorkMode.Globally, locale.dropdownOptions.globally); 324 | dropdown.setValue(this.plugin.settings.AutoCapitalMode); 325 | dropdown.onChange(async (v: WorkMode.OnlyWhenTyping | WorkMode.Globally) => { 326 | this.plugin.settings.AutoCapitalMode = v; 327 | await this.plugin.saveSettings(); 328 | }) 329 | }) 330 | .addToggle((toggle) => { 331 | toggle.setTooltip(locale.toolTip.switch); 332 | toggle.setValue(this.plugin.settings.AutoCapital).onChange(async (value) => { 333 | this.plugin.settings.AutoCapital = value; 334 | await this.plugin.saveSettings(); 335 | }); 336 | }); 337 | 338 | new Setting(containerEl) 339 | .setName(locale.settings.textPunctuationSpace.name) 340 | .setDesc(locale.settings.textPunctuationSpace.desc) 341 | .addDropdown((dropdown) => { 342 | dropdown.addOption(WorkMode.OnlyWhenTyping, locale.dropdownOptions.onlyWhenTyping); 343 | dropdown.addOption(WorkMode.Globally, locale.dropdownOptions.globally); 344 | dropdown.setValue(this.plugin.settings.PunctuationSpaceMode); 345 | dropdown.onChange(async (v: WorkMode.OnlyWhenTyping | WorkMode.Globally) => { 346 | this.plugin.settings.PunctuationSpaceMode = v; 347 | await this.plugin.saveSettings(); 348 | }) 349 | }) 350 | .addToggle((toggle) => { 351 | toggle.setValue(this.plugin.settings.PunctuationSpace).onChange(async (value) => { 352 | this.plugin.settings.PunctuationSpace = value; 353 | await this.plugin.saveSettings(); 354 | }); 355 | }); 356 | 357 | new Setting(containerEl) 358 | .setName(locale.settings.spaceStrategyInlineCode.name) 359 | .setDesc(locale.settings.spaceStrategyInlineCode.desc) 360 | .addDropdown((dropdown) => { 361 | dropdown.addOption(String(SpaceState.none), locale.dropdownOptions.noRequire); 362 | dropdown.addOption(String(SpaceState.soft), locale.dropdownOptions.softSpace); 363 | dropdown.addOption(String(SpaceState.strict), locale.dropdownOptions.strictSpace); 364 | dropdown.setValue(String(this.plugin.settings.InlineCodeSpaceMode)); 365 | dropdown.onChange(async (v: string) => { 366 | this.plugin.settings.InlineCodeSpaceMode = string2SpaceState(v); 367 | await this.plugin.saveSettings(); 368 | }) 369 | }); 370 | 371 | new Setting(containerEl) 372 | .setName(locale.settings.spaceStrategyInlineFormula.name) 373 | .setDesc(locale.settings.spaceStrategyInlineFormula.desc) 374 | .addDropdown((dropdown) => { 375 | dropdown.addOption(String(SpaceState.none), locale.dropdownOptions.noRequire); 376 | dropdown.addOption(String(SpaceState.soft), locale.dropdownOptions.softSpace); 377 | dropdown.addOption(String(SpaceState.strict), locale.dropdownOptions.strictSpace); 378 | dropdown.setValue(String(this.plugin.settings.InlineFormulaSpaceMode)); 379 | dropdown.onChange(async (v: string) => { 380 | this.plugin.settings.InlineFormulaSpaceMode = string2SpaceState(v); 381 | await this.plugin.saveSettings(); 382 | }) 383 | }); 384 | 385 | new Setting(containerEl) 386 | .setName(locale.settings.spaceStrategyLinkText.name) 387 | .setDesc(locale.settings.spaceStrategyLinkText.desc) 388 | .addDropdown((dropdown) => { 389 | dropdown.addOption("dummy", locale.dropdownOptions.dummy); 390 | dropdown.addOption("smart", locale.dropdownOptions.smart); 391 | dropdown.setValue(this.plugin.settings.InlineLinkSmartSpace ? "smart" : "dummy"); 392 | dropdown.onChange(async (v: string) => { 393 | this.plugin.settings.InlineLinkSmartSpace = v == "smart" ? true : false; 394 | // new Notice(String(this.plugin.settings.InlineLinkSmartSpace)); 395 | await this.plugin.saveSettings(); 396 | }) 397 | }) 398 | .addDropdown((dropdown) => { 399 | dropdown.addOption(String(SpaceState.none), locale.dropdownOptions.noRequire); 400 | dropdown.addOption(String(SpaceState.soft), locale.dropdownOptions.softSpace); 401 | dropdown.addOption(String(SpaceState.strict), locale.dropdownOptions.strictSpace); 402 | dropdown.setValue(String(this.plugin.settings.InlineLinkSpaceMode)); 403 | dropdown.onChange(async (v: string) => { 404 | this.plugin.settings.InlineLinkSpaceMode = string2SpaceState(v); 405 | await this.plugin.saveSettings(); 406 | }) 407 | }) 408 | 409 | containerEl.createEl('h2', { text: locale.headers.customRegexpBlock }); 410 | new Setting(containerEl) 411 | .setName(locale.settings.userDefinedRegexpSwitch.name) 412 | .setDesc(locale.settings.userDefinedRegexpSwitch.desc) 413 | .addToggle((toggle) => { 414 | toggle.setValue(this.plugin.settings.UserDefinedRegSwitch).onChange(async (value) => { 415 | this.plugin.settings.UserDefinedRegSwitch = value; 416 | await this.plugin.saveSettings(); 417 | }); 418 | }); 419 | 420 | containerEl.createEl("p", { text: locale.headers.aboutRegexp.header }).createEl("a", { 421 | text: locale.headers.aboutRegexp.text, 422 | href: "https://javascript.ruanyifeng.com/stdlib/regexp.html#", 423 | }); 424 | 425 | containerEl.createEl("p", { text: locale.headers.instructionsRegexp.header }).createEl("a", { 426 | text: locale.headers.instructionsRegexp.text, 427 | href: "https://github.com/Yaozhuwa/easy-typing-obsidian/blob/master/UserDefinedRegExp.md", 428 | }); 429 | 430 | const regContentAreaSetting = new Setting(containerEl); 431 | regContentAreaSetting.settingEl.setAttribute( 432 | "style", 433 | "display: grid; grid-template-columns: 1fr;" 434 | ); 435 | regContentAreaSetting 436 | .setName(locale.settings.userDefinedRegexp.name) 437 | .setDesc(locale.settings.userDefinedRegexp.desc); 438 | const regContentArea = new TextAreaComponent( 439 | regContentAreaSetting.controlEl 440 | ); 441 | 442 | setAttributes(regContentArea.inputEl, { 443 | style: "margin-top: 12px; width: 100%; height: 30vh;", 444 | // class: "ms-css-editor", 445 | }); 446 | regContentArea 447 | .setValue(this.plugin.settings.UserDefinedRegExp) 448 | .onChange(async (value) => { 449 | this.plugin.settings.UserDefinedRegExp = value; 450 | this.plugin.saveSettings(); 451 | }); 452 | 453 | containerEl.createEl('h2', { text: locale.headers.excludeFoldersFiles }); 454 | new Setting(containerEl) 455 | .setName(locale.settings.excludeFoldersFiles.name) 456 | .setDesc(locale.settings.excludeFoldersFiles.desc) 457 | .addTextArea((text) => 458 | text 459 | .setValue(this.plugin.settings.ExcludeFiles) 460 | .onChange(async (value) => { 461 | this.plugin.settings.ExcludeFiles = value; 462 | this.plugin.saveSettings(); 463 | }) 464 | ); 465 | 466 | containerEl.createEl('h2', { text: locale.headers.experimentalFeatures }); 467 | 468 | new Setting(containerEl) 469 | .setName(locale.settings.strictLineBreaks.name) 470 | .setDesc(locale.settings.strictLineBreaks.desc) 471 | .addDropdown((dropdown) => { 472 | dropdown.addOption(StrictLineMode.EnterTwice, locale.dropdownOptions.enterTwice); 473 | dropdown.addOption(StrictLineMode.TwoSpace, locale.dropdownOptions.twoSpace); 474 | dropdown.addOption(StrictLineMode.Mix, locale.dropdownOptions.mixMode); 475 | dropdown.setValue(this.plugin.settings.StrictLineMode); 476 | dropdown.onChange(async (v: StrictLineMode) => { 477 | this.plugin.settings.StrictLineMode = v; 478 | await this.plugin.saveSettings(); 479 | }) 480 | }) 481 | .addToggle((toggle) => { 482 | toggle.setValue(this.plugin.settings.StrictModeEnter).onChange(async (value) => { 483 | this.plugin.settings.StrictModeEnter = value; 484 | await this.plugin.saveSettings(); 485 | }); 486 | }); 487 | 488 | new Setting(containerEl) 489 | .setName(locale.settings.enhanceModA.name) 490 | .setDesc(locale.settings.enhanceModA.desc) 491 | .addToggle((toggle) => { 492 | toggle.setValue(this.plugin.settings.EnhanceModA).onChange(async (value) => { 493 | this.plugin.settings.EnhanceModA = value; 494 | await this.plugin.saveSettings(); 495 | }); 496 | }); 497 | 498 | new Setting(containerEl) 499 | .setName(locale.settings.collapsePersistentEnter.name) 500 | .setDesc(locale.settings.collapsePersistentEnter.desc) 501 | .addToggle((toggle) => { 502 | toggle.setValue(this.plugin.settings.CollapsePersistentEnter).onChange(async (value) => { 503 | this.plugin.settings.CollapsePersistentEnter = value; 504 | await this.plugin.saveSettings(); 505 | }); 506 | }); 507 | 508 | new Setting(containerEl) 509 | .setName(locale.settings.fixMicrosoftIME.name) 510 | .setDesc(locale.settings.fixMicrosoftIME.desc) 511 | .addToggle((toggle) => { 512 | toggle.setValue(this.plugin.settings.TryFixMSIME).onChange(async (value) => { 513 | this.plugin.settings.TryFixMSIME = value; 514 | await this.plugin.saveSettings(); 515 | }); 516 | }); 517 | 518 | new Setting(containerEl) 519 | .setName(locale.settings.fixMacOSContextMenu.name) 520 | .setDesc(locale.settings.fixMacOSContextMenu.desc) 521 | .addToggle((toggle) => { 522 | toggle.setValue(this.plugin.settings.FixMacOSContextMenu).onChange(async (value) => { 523 | this.plugin.settings.FixMacOSContextMenu = value; 524 | await this.plugin.saveSettings(); 525 | }); 526 | }); 527 | 528 | new Setting(containerEl) 529 | .setName(locale.settings.puncRectify.name) 530 | .setDesc(locale.settings.puncRectify.desc) 531 | .addToggle((toggle) => { 532 | toggle.setValue(this.plugin.settings.PuncRectify).onChange(async (value) => { 533 | this.plugin.settings.PuncRectify = value; 534 | await this.plugin.saveSettings(); 535 | }); 536 | }); 537 | 538 | new Setting(containerEl) 539 | .setName(locale.settings.printDebugInfo.name) 540 | .setDesc(locale.settings.printDebugInfo.desc) 541 | .addToggle((toggle) => { 542 | toggle.setValue(this.plugin.settings.debug).onChange(async (value) => { 543 | this.plugin.settings.debug = value; 544 | setDebug(value); 545 | await this.plugin.saveSettings(); 546 | }); 547 | }); 548 | } 549 | 550 | buildUserSelRepRuleSetting(containerEl: HTMLDetailsElement){ 551 | containerEl.empty(); 552 | containerEl.ontoggle = async () => { 553 | this.plugin.settings.userSelRuleSettingsOpen = containerEl.open; 554 | await this.plugin.saveSettings(); 555 | }; 556 | 557 | const summary = containerEl.createEl("summary", {cls: "easytyping-nested-settings"}); 558 | summary.setText(locale.headers.customizeSelectionRule) 559 | 560 | // summary.setHeading().setName("User defined Selection Replace Rule"); 561 | // summary.createDiv("collapser").createDiv("handle"); 562 | 563 | const selectionRuleSetting = new Setting(containerEl); 564 | selectionRuleSetting 565 | .setName(locale.settings.selectionReplaceRule.name) 566 | // .setDesc(locale.settings.selectionReplaceRule.desc) 567 | 568 | const replaceRuleTrigger = new TextComponent(selectionRuleSetting.controlEl); 569 | replaceRuleTrigger.setPlaceholder(locale.placeHolder.triggerSymbol); 570 | 571 | const replaceLeftString = new TextComponent(selectionRuleSetting.controlEl); 572 | replaceLeftString.setPlaceholder(locale.placeHolder.newLeftSideString); 573 | 574 | const replaceRightString = new TextComponent(selectionRuleSetting.controlEl); 575 | replaceRightString.setPlaceholder(locale.placeHolder.newRightSideString); 576 | 577 | selectionRuleSetting 578 | .addButton((button) => { 579 | button 580 | .setButtonText("+") 581 | .setTooltip(locale.placeHolder.addRule) 582 | .onClick(async (buttonEl: any) => { 583 | let trigger = replaceRuleTrigger.inputEl.value; 584 | let left = replaceLeftString.inputEl.value; 585 | let right = replaceRightString.inputEl.value; 586 | if (trigger && (left || right)) { 587 | if(trigger.length!=1 && trigger!="——" && trigger!="……"){ 588 | new Notice(locale.placeHolder.noticeInvaidTrigger); 589 | return; 590 | } 591 | if (this.plugin.addUserSelectionRepRule(trigger, left, right)){ 592 | await this.plugin.saveSettings(); 593 | this.display(); 594 | } 595 | else{ 596 | new Notice(sprintf(locale.placeHolder.noticeWarnTriggerExists, trigger)) 597 | } 598 | } 599 | else { 600 | new Notice(locale.placeHolder.noticeMissingInput); 601 | } 602 | }); 603 | }); 604 | 605 | // const selRepRuleContainer = containerEl.createEl("div"); 606 | for (let i = 0; i < this.plugin.settings.userSelRepRuleTrigger.length; i++) { 607 | let trigger = this.plugin.settings.userSelRepRuleTrigger[i]; 608 | let left_s = this.plugin.settings.userSelRepRuleValue[i].left; 609 | let right_s = this.plugin.settings.userSelRepRuleValue[i].right; 610 | let showStr = "Trigger: " + trigger + " → " + showString(left_s) + "selected" + showString(right_s); 611 | // const settingItem = selRepRuleContainer.createEl("div"); 612 | new Setting(containerEl) 613 | .setName(showStr) 614 | .addExtraButton(button => { 615 | button.setIcon("gear") 616 | .setTooltip(locale.toolTip.editRule) 617 | .onClick(() => { 618 | new SelectRuleEditModal(this.app, trigger,left_s, right_s, async (new_left, new_right) => { 619 | this.plugin.updateUserSelectionRepRule(i, new_left, new_right); 620 | await this.plugin.saveSettings(); 621 | this.display(); 622 | }).open(); 623 | }) 624 | }) 625 | .addExtraButton(button => { 626 | button.setIcon("trash") 627 | .setTooltip(locale.toolTip.removeRule) 628 | .onClick(async () => { 629 | this.plugin.deleteUserSelectionRepRule(i); 630 | await this.plugin.saveSettings(); 631 | this.display(); 632 | }) 633 | }); 634 | } 635 | 636 | 637 | } 638 | 639 | buildUserDeleteRuleSetting(containerEl: HTMLDetailsElement){ 640 | containerEl.empty(); 641 | containerEl.ontoggle = async () => { 642 | this.plugin.settings.userDelRuleSettingsOpen = containerEl.open; 643 | await this.plugin.saveSettings(); 644 | }; 645 | const summary = containerEl.createEl("summary", {cls: "easytyping-nested-settings"}); 646 | summary.setText(locale.headers.customizeDeleteRule) 647 | 648 | const deleteRuleSetting = new Setting(containerEl); 649 | deleteRuleSetting 650 | .setName(locale.settings.deleteRule.name) 651 | .setDesc(locale.settings.deleteRule.desc) 652 | 653 | const patternBefore = new TextAreaComponent(deleteRuleSetting.controlEl); 654 | patternBefore.setPlaceholder(locale.placeHolder.beforeDelete); 655 | 656 | const patternAfter = new TextAreaComponent(deleteRuleSetting.controlEl); 657 | patternAfter.setPlaceholder(locale.placeHolder.newPattern); 658 | 659 | deleteRuleSetting 660 | .addButton((button) => { 661 | button 662 | .setButtonText("+") 663 | .setTooltip(locale.toolTip.addRule) 664 | .onClick(async (buttonEl: any) => { 665 | let before = patternBefore.inputEl.value; 666 | let after = patternAfter.inputEl.value; 667 | if (before && after) { 668 | if(findFirstPipeNotPrecededByBackslash(before)==-1){ 669 | new Notice(locale.placeHolder.noticeInvaidTriggerPatternContainSymbol); 670 | return; 671 | } 672 | else{ 673 | this.plugin.addUserDeleteRule(before, after); 674 | await this.plugin.saveSettings(); 675 | this.display(); 676 | } 677 | } 678 | else { 679 | new Notice(locale.placeHolder.noticeMissingInput); 680 | } 681 | }); 682 | }); 683 | 684 | for (let i = 0; i < this.plugin.settings.userDeleteRulesStrList.length; i++){ 685 | let before = this.plugin.settings.userDeleteRulesStrList[i][0]; 686 | let after = this.plugin.settings.userDeleteRulesStrList[i][1]; 687 | let showStr = "\"" + showString(before) + "\" delete.backwards → \""+ showString(after)+"\""; 688 | new Setting(containerEl) 689 | .setName(showStr) 690 | .addExtraButton(button => { 691 | button.setIcon("gear") 692 | .setTooltip(locale.toolTip.editRule) 693 | .onClick(() => { 694 | new EditConvertRuleModal(this.app, RuleType.delete, before, after, async (new_before, new_after) => { 695 | this.plugin.updateUserDeleteRule(i, new_before, new_after); 696 | await this.plugin.saveSettings(); 697 | this.display(); 698 | }).open(); 699 | }) 700 | }) 701 | .addExtraButton(button => { 702 | button.setIcon("trash") 703 | .setTooltip(locale.toolTip.removeRule) 704 | .onClick(async () => { 705 | this.plugin.deleteUserDeleteRule(i); 706 | await this.plugin.saveSettings(); 707 | this.display(); 708 | }) 709 | }); 710 | } 711 | 712 | } 713 | 714 | buildUserConvertRuleSetting(containerEl: HTMLDetailsElement){ 715 | containerEl.empty(); 716 | containerEl.ontoggle = async () => { 717 | this.plugin.settings.userCvtRuleSettingsOpen = containerEl.open; 718 | await this.plugin.saveSettings(); 719 | }; 720 | const summary = containerEl.createEl("summary", {cls: "easytyping-nested-settings"}); 721 | summary.setText(locale.headers.customizeConvertRule) 722 | 723 | const convertRuleSetting = new Setting(containerEl); 724 | convertRuleSetting 725 | .setName(locale.settings.convertRule.name) 726 | .setDesc(locale.settings.convertRule.desc) 727 | 728 | const patternBefore = new TextAreaComponent(convertRuleSetting.controlEl); 729 | patternBefore.setPlaceholder(locale.placeHolder.beforeConvert); 730 | 731 | const patternAfter = new TextAreaComponent(convertRuleSetting.controlEl); 732 | patternAfter.setPlaceholder(locale.placeHolder.newPattern); 733 | 734 | convertRuleSetting 735 | .addButton((button) => { 736 | button 737 | .setButtonText("+") 738 | .setTooltip(locale.toolTip.addRule) 739 | .onClick(async (buttonEl: any) => { 740 | let before = patternBefore.inputEl.value; 741 | let after = patternAfter.inputEl.value; 742 | if (before && after) { 743 | if(findFirstPipeNotPrecededByBackslash(before)==-1){ 744 | new Notice(locale.placeHolder.noticeInvaidTriggerPatternContainSymbol); 745 | return; 746 | } 747 | else{ 748 | this.plugin.addUserConvertRule(before, after); 749 | await this.plugin.saveSettings(); 750 | this.display(); 751 | } 752 | } 753 | else { 754 | new Notice(locale.placeHolder.noticeMissingInput); 755 | } 756 | }); 757 | }); 758 | 759 | for (let i = 0; i < this.plugin.settings.userConvertRulesStrList.length; i++){ 760 | let before = this.plugin.settings.userConvertRulesStrList[i][0]; 761 | let after = this.plugin.settings.userConvertRulesStrList[i][1]; 762 | let showStr = "\"" + showString(before) + "\" auto convert to \""+ showString(after)+"\""; 763 | new Setting(containerEl) 764 | .setName(showStr) 765 | .addExtraButton(button => { 766 | button.setIcon("gear") 767 | .setTooltip(locale.toolTip.editRule) 768 | .onClick(() => { 769 | new EditConvertRuleModal(this.app, RuleType.convert, before, after, async (new_before, new_after) => { 770 | this.plugin.updateUserConvertRule(i, new_before, new_after); 771 | await this.plugin.saveSettings(); 772 | this.display(); 773 | }).open(); 774 | }) 775 | }) 776 | .addExtraButton(button => { 777 | button.setIcon("trash") 778 | .setTooltip(locale.toolTip.removeRule) 779 | .onClick(async () => { 780 | this.plugin.deleteUserConvertRule(i); 781 | await this.plugin.saveSettings(); 782 | this.display(); 783 | }) 784 | }); 785 | } 786 | } 787 | 788 | } 789 | 790 | 791 | function setAttributes(element: any, attributes: any) { 792 | for (let key in attributes) { 793 | element.setAttribute(key, attributes[key]); 794 | } 795 | } 796 | 797 | 798 | export class SelectRuleEditModal extends Modal { 799 | trigger: string; 800 | old_left: string; 801 | old_right: string; 802 | new_left: string; 803 | new_right: string; 804 | onSubmit: (new_left: string, new_right:string) => void; 805 | 806 | constructor(app: App, trigger: string, left: string, right: string, onSubmit: (new_left: string, new_right:string) => void) { 807 | super(app); 808 | this.trigger = trigger; 809 | this.old_left = left; 810 | this.old_right = right; 811 | this.new_left = left; 812 | this.new_right = right; 813 | 814 | this.onSubmit = onSubmit; 815 | } 816 | 817 | onOpen() { 818 | const { contentEl } = this; 819 | 820 | contentEl.createEl("h1", { text: locale.headers.editSelectionReplaceRule }); 821 | 822 | new Setting(contentEl) 823 | .setName(locale.settings.trigger.name) 824 | .addText((text) => { 825 | text.setValue(this.trigger); 826 | text.setDisabled(true); 827 | }) 828 | 829 | new Setting(contentEl) 830 | .setName(locale.settings.left.name) 831 | .addTextArea((text) => { 832 | text.setValue(this.old_left); 833 | text.onChange((value) => { 834 | this.new_left = value 835 | }) 836 | }) 837 | new Setting(contentEl) 838 | .setName(locale.settings.right.name) 839 | .addTextArea((text) => { 840 | text.setValue(this.old_right); 841 | text.onChange((value) => { 842 | this.new_right = value 843 | }) 844 | }); 845 | 846 | 847 | new Setting(contentEl) 848 | .addButton((btn) => 849 | btn 850 | .setButtonText(locale.button.update) 851 | .setCta() 852 | .onClick(() => { 853 | this.close(); 854 | this.onSubmit(this.new_left, this.new_right); 855 | })); 856 | } 857 | 858 | onClose() { 859 | let { contentEl } = this; 860 | contentEl.empty(); 861 | } 862 | } 863 | 864 | 865 | 866 | export class EditConvertRuleModal extends Modal { 867 | type: RuleType; 868 | old_before: string; 869 | old_after: string; 870 | new_before: string; 871 | new_after: string; 872 | onSubmit: (new_before: string, new_after:string) => void; 873 | 874 | constructor(app: App, type: RuleType, before: string, after: string, onSubmit: (new_before: string, new_after:string) => void) { 875 | super(app); 876 | this.type = type; 877 | this.old_before = before; 878 | this.old_after = after; 879 | this.new_before = before; 880 | this.new_after = after; 881 | 882 | this.onSubmit = onSubmit; 883 | } 884 | 885 | onOpen() { 886 | const { contentEl } = this; 887 | 888 | contentEl.createEl("h1", { text: "Edit " + this.type}); 889 | 890 | new Setting(contentEl) 891 | .setName(locale.settings.oldPattern.name) 892 | .addTextArea((text) => { 893 | text.setValue(this.old_before); 894 | text.onChange((value) => { 895 | this.new_before = value 896 | }) 897 | }) 898 | new Setting(contentEl) 899 | .setName(locale.settings.newPattern.name) 900 | .addTextArea((text) => { 901 | text.setValue(this.old_after); 902 | text.onChange((value) => { 903 | this.new_after = value 904 | }) 905 | }); 906 | 907 | 908 | new Setting(contentEl) 909 | .addButton((btn) => 910 | btn 911 | .setButtonText(locale.button.update) 912 | .setCta() 913 | .onClick(() => { 914 | if (this.checkConvertPatternString(this.new_before, this.new_after)) 915 | { 916 | this.close(); 917 | this.onSubmit(this.new_before, this.new_after); 918 | } 919 | else{ 920 | new Notice(locale.placeHolder.noticeInvalidPatternString); 921 | } 922 | 923 | })); 924 | } 925 | 926 | checkConvertPatternString(before: string, after:string):boolean{ 927 | if(findFirstPipeNotPrecededByBackslash(before)==-1) return false; 928 | return true; 929 | } 930 | 931 | onClose() { 932 | let { contentEl } = this; 933 | contentEl.empty(); 934 | } 935 | } 936 | 937 | -------------------------------------------------------------------------------- /src/syntax.ts: -------------------------------------------------------------------------------- 1 | import { ensureSyntaxTree, syntaxTree } from "@codemirror/language"; 2 | import { EditorView } from '@codemirror/view'; 3 | import { EditorState, SelectionRange } from '@codemirror/state'; 4 | import { getPosLineType2 } from "./core"; 5 | export interface CodeBlockInfo { 6 | start_pos: number; 7 | end_pos: number; 8 | code_start_pos: number; 9 | code_end_pos: number; 10 | language: string; 11 | indent: number; 12 | } 13 | 14 | export interface QuoteInfo { 15 | start_pos: number; 16 | end_pos: number; 17 | is_callout: boolean; 18 | cur_start_pos: number; 19 | cur_end_pos: number; 20 | } 21 | 22 | export function isCodeBlockInPos(state: EditorState, pos: number): boolean { 23 | let codeBlockInfos = getCodeBlocksInfos(state); 24 | for (let i = 0; i < codeBlockInfos.length; i++) { 25 | if (pos >= codeBlockInfos[i].start_pos && pos <= codeBlockInfos[i].end_pos) { 26 | return true; 27 | } 28 | } 29 | return false; 30 | } 31 | 32 | export function getCodeBlockInfoInPos(state: EditorState, pos: number): CodeBlockInfo | null { 33 | let codeBlockInfos = getCodeBlocksInfos(state); 34 | for (let i = 0; i < codeBlockInfos.length; i++) { 35 | if (pos >= codeBlockInfos[i].start_pos && pos <= codeBlockInfos[i].end_pos) { 36 | return codeBlockInfos[i]; 37 | } 38 | } 39 | return null; 40 | } 41 | 42 | export function selectCodeBlockInPos(view: EditorView, selection: SelectionRange):boolean { 43 | let pos = selection.anchor; 44 | // let selected = selection.anchor !== selection.head; 45 | let codeBlockInfos = getCodeBlocksInfos(view.state); 46 | for (let i = 0; i < codeBlockInfos.length; i++) { 47 | if (pos >= codeBlockInfos[i].start_pos && pos <= codeBlockInfos[i].end_pos) { 48 | if (codeBlockInfos[i].code_start_pos == codeBlockInfos[i].code_end_pos) { 49 | view.dispatch({ 50 | selection: { 51 | anchor: codeBlockInfos[i].start_pos, 52 | head: codeBlockInfos[i].end_pos 53 | } 54 | }); 55 | return true; 56 | } 57 | let code_line_start = view.state.doc.lineAt(codeBlockInfos[i].code_start_pos); 58 | let isCodeSelected = selection.anchor == code_line_start.from && 59 | selection.head == codeBlockInfos[i].code_end_pos; 60 | let isCodeBlockSelected = selection.anchor == codeBlockInfos[i].start_pos && 61 | selection.head == codeBlockInfos[i].end_pos; 62 | if (isCodeSelected) { 63 | view.dispatch({ 64 | selection: { 65 | anchor: codeBlockInfos[i].start_pos, 66 | head: codeBlockInfos[i].end_pos 67 | } 68 | }); 69 | return true; 70 | } 71 | if (isCodeBlockSelected) return false; 72 | view.dispatch({ 73 | selection: { 74 | anchor: code_line_start.from, 75 | head: codeBlockInfos[i].code_end_pos 76 | } 77 | }); 78 | return true; 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | export function getCodeBlocksInfos(state: EditorState): CodeBlockInfo[]{ 85 | let isCodeBlockBegin = false; 86 | let codeBlockInfos: CodeBlockInfo[] = []; 87 | let curCodeBlockInfo: CodeBlockInfo | null = null; 88 | const doc = state.doc; 89 | 90 | syntaxTree(state).iterate({ 91 | enter(node) { 92 | const nodeName = node.name; 93 | const nodeFrom = node.from; 94 | const nodeTo = node.to; 95 | const nodeText = state.sliceDoc(nodeFrom, nodeTo); 96 | // console.log(nodeName, nodeFrom, nodeTo, nodeText); 97 | if (nodeName.includes('codeblock-begin')) { 98 | isCodeBlockBegin = true; 99 | let start_pos = nodeFrom + nodeText.indexOf('`'); 100 | let indent = start_pos - state.doc.lineAt(start_pos).from; 101 | let language = nodeText.trim().substring(3); 102 | curCodeBlockInfo = { 103 | start_pos: start_pos, 104 | end_pos: -1, 105 | code_start_pos: -1, 106 | code_end_pos: -1, 107 | language: language.toLowerCase(), 108 | indent: indent 109 | } 110 | } else if (nodeName.includes('codeblock-end')) { 111 | isCodeBlockBegin = false; 112 | if (curCodeBlockInfo != null) { 113 | curCodeBlockInfo.end_pos = nodeTo; 114 | if (doc.lineAt(curCodeBlockInfo.start_pos).number == 115 | doc.lineAt(curCodeBlockInfo.end_pos).number - 1) { 116 | curCodeBlockInfo.code_start_pos = doc.lineAt(curCodeBlockInfo.start_pos).to; 117 | curCodeBlockInfo.code_end_pos = doc.lineAt(curCodeBlockInfo.start_pos).to; 118 | } 119 | else { 120 | let code_start_line = doc.lineAt(curCodeBlockInfo.start_pos).number + 1; 121 | let code_end_line = doc.lineAt(curCodeBlockInfo.end_pos).number - 1; 122 | curCodeBlockInfo.code_start_pos = doc.line(code_start_line).from + curCodeBlockInfo.indent; 123 | curCodeBlockInfo.code_end_pos = doc.line(code_end_line).to; 124 | } 125 | codeBlockInfos.push(curCodeBlockInfo); 126 | curCodeBlockInfo = null; 127 | } 128 | } 129 | } 130 | }); 131 | 132 | if (isCodeBlockBegin && curCodeBlockInfo) { 133 | curCodeBlockInfo.end_pos = doc.length; 134 | curCodeBlockInfo.code_end_pos = doc.length; 135 | if (doc.lines > doc.lineAt(curCodeBlockInfo.start_pos).number) { 136 | let start_line = doc.lineAt(curCodeBlockInfo.start_pos).number + 1; 137 | let code_start_pos = doc.line(start_line).from + curCodeBlockInfo.indent; 138 | curCodeBlockInfo.code_start_pos = code_start_pos < doc.length ? code_start_pos : 139 | doc.lineAt(curCodeBlockInfo.start_pos + 1).from ; 140 | } 141 | else { 142 | curCodeBlockInfo.code_start_pos = doc.lineAt(curCodeBlockInfo.start_pos).to; 143 | } 144 | codeBlockInfos.push(curCodeBlockInfo); 145 | curCodeBlockInfo = null; 146 | } 147 | return codeBlockInfos; 148 | } 149 | 150 | export function getQuoteInfoInPos(state: EditorState, pos: number): QuoteInfo | null { 151 | let quote_regex = /^(\s*)(>+) ?/; 152 | let callout_regex = /^(\s*)(>)+ \[![^\s]+\][+-]? ?/; 153 | let cur_line = state.doc.lineAt(pos); 154 | let match = cur_line.text.match(quote_regex); 155 | let is_callout = false; 156 | let cur_start_pos = -1; 157 | let cur_end_pos = -1; 158 | if (match){ 159 | let match_callout = cur_line.text.match(callout_regex); 160 | cur_start_pos = cur_line.from + (match_callout ? match_callout[0].length : match[0].length); 161 | cur_end_pos = cur_line.to; 162 | let quote_start_line = cur_line.number; 163 | let quote_end_line = quote_start_line; 164 | for(let i=quote_start_line+1;i<=state.doc.lines;i+=1){ 165 | let line = state.doc.line(i); 166 | if (line.text.match(quote_regex)){ 167 | quote_end_line = i; 168 | } 169 | else break; 170 | } 171 | for (let i=quote_start_line;i>=1;i-=1){ 172 | let line = state.doc.line(i); 173 | let match_callout = line.text.match(callout_regex); 174 | let match_quote = line.text.match(quote_regex); 175 | if (match_callout){ 176 | is_callout = true; 177 | quote_start_line = i; 178 | // break; 179 | } 180 | else if (match_quote){ 181 | quote_start_line = i; 182 | } 183 | else break; 184 | } 185 | return { 186 | start_pos: state.doc.line(quote_start_line).from, 187 | end_pos: state.doc.line(quote_end_line).to, 188 | is_callout: is_callout, 189 | cur_start_pos: cur_start_pos, 190 | cur_end_pos: cur_end_pos 191 | }; 192 | } 193 | else{ 194 | return null; 195 | } 196 | } -------------------------------------------------------------------------------- /src/tabstop.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDesc, EditorSelection, SelectionRange } from "@codemirror/state"; 2 | import { Decoration, DecorationSet, EditorView, WidgetType } from "@codemirror/view"; 3 | 4 | 5 | const TABSTOP_DECO_CLASS = "easy-typing-tabstops"; 6 | const CURSOR_WIDGET_CLASS = "easy-typing-cursor-widget"; 7 | 8 | export interface TabstopSpec { 9 | number: number, 10 | from: number, 11 | to: number 12 | } 13 | 14 | function getMarkerDecoration(from: number, to: number) { 15 | const className = `${TABSTOP_DECO_CLASS}`; 16 | 17 | if (from==to){ 18 | return Decoration.widget({ 19 | widget: new CursorWidget(), 20 | side: 1, 21 | }).range(from); 22 | } 23 | 24 | return Decoration.mark({ 25 | inclusive: true, 26 | class: className, 27 | }).range(from, to); 28 | } 29 | 30 | export class TabstopGroup { 31 | decos: DecorationSet; 32 | selections: SelectionRange[]; 33 | 34 | constructor(tabstopSpecs: TabstopSpec[]) { 35 | // const tabstopSpecsRange = tabstopSpecs.filter(spec => spec.from != spec.to); 36 | const decos = tabstopSpecs.map(spec => getMarkerDecoration(spec.from, spec.to)); 37 | this.selections = tabstopSpecs.map(spec => EditorSelection.range(spec.from, spec.to)); 38 | this.decos = Decoration.set(decos, true); 39 | } 40 | 41 | select(view: EditorView, selectEndpoints: boolean) { 42 | const sel = this.toEditorSelection(); 43 | const toSelect = selectEndpoints ? getEditorSelectionEndpoints(sel) : sel; 44 | 45 | view.dispatch({ 46 | selection: toSelect, 47 | }); 48 | } 49 | 50 | toSelectionRanges() { 51 | return this.selections; 52 | } 53 | 54 | toEditorSelection() { 55 | return EditorSelection.create(this.toSelectionRanges()); 56 | } 57 | 58 | containsSelection(selection: EditorSelection) { 59 | function rangeLiesWithinSelection(range: SelectionRange, sel: SelectionRange[]) { 60 | for (const selRange of sel) { 61 | if (selRange.from <= range.from && selRange.to >= range.to) { 62 | return true; 63 | } 64 | } 65 | return false; 66 | } 67 | 68 | const tabstopRanges = this.toSelectionRanges(); 69 | let result = true; 70 | 71 | for (const range of selection.ranges) { 72 | if (!rangeLiesWithinSelection(range, tabstopRanges)) { 73 | result = false; 74 | break; 75 | } 76 | } 77 | return result; 78 | } 79 | 80 | map(changes: ChangeDesc) { 81 | this.decos = this.decos.map(changes); 82 | this.selections = this.selections.map(range => { 83 | let rangeFrom = changes.mapPos(range.from, -1); 84 | let rangeTo = changes.mapPos(range.to, 1); 85 | return EditorSelection.range(rangeFrom, rangeTo); 86 | }); 87 | } 88 | 89 | getDecoRanges() { 90 | const ranges = []; 91 | const cur = this.decos.iter(); 92 | 93 | while (cur.value != null) { 94 | if (cur.from != cur.to){ 95 | ranges.push(cur.value.range(cur.from, cur.to)); 96 | }else{ 97 | ranges.push(cur.value.range(cur.from)); 98 | } 99 | // ranges.push(cur.value.range(cur.from, cur.to)); 100 | cur.next(); 101 | } 102 | 103 | return ranges; 104 | } 105 | } 106 | 107 | export function tabstopSpecsToTabstopGroups(tabstops: TabstopSpec[]):TabstopGroup[] { 108 | const tabstopsByNumber: {[n: string]: TabstopSpec[]} = {}; 109 | 110 | for (const tabstop of tabstops) { 111 | const n = String(tabstop.number); 112 | 113 | if (tabstopsByNumber[n]) { 114 | tabstopsByNumber[n].push(tabstop); 115 | } 116 | else { 117 | tabstopsByNumber[n] = [tabstop]; 118 | } 119 | } 120 | 121 | const result = []; 122 | const numbers = Object.keys(tabstopsByNumber); 123 | numbers.sort((a,b) => parseInt(a) - parseInt(b)); 124 | 125 | for (const number of numbers) { 126 | const grp = new TabstopGroup(tabstopsByNumber[number]); 127 | result.push(grp); 128 | } 129 | 130 | return result; 131 | } 132 | 133 | export function getEditorSelectionEndpoints(sel: EditorSelection) { 134 | const endpoints = sel.ranges.map(range => EditorSelection.range(range.to, range.to)); 135 | 136 | return EditorSelection.create(endpoints); 137 | } 138 | 139 | class CursorWidget extends WidgetType { 140 | 141 | eq(widget: WidgetType): boolean { 142 | return true; 143 | } 144 | 145 | toDOM(view: EditorView): HTMLElement { 146 | const cursorEl = document.createElement("span"); 147 | cursorEl.className = `${CURSOR_WIDGET_CLASS}`; 148 | cursorEl.textContent = '|'; 149 | return cursorEl; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/tabstops_state_field.ts: -------------------------------------------------------------------------------- 1 | import { EditorView, Decoration } from "@codemirror/view"; 2 | import { EditorSelection, StateEffect, StateField, Transaction } from "@codemirror/state"; 3 | import { TabstopGroup } from "./tabstop"; 4 | 5 | export const addTabstopsEffect = StateEffect.define(); 6 | const removeTabstopEffect = StateEffect.define(); 7 | const removeAllTabstopsEffect = StateEffect.define(); 8 | 9 | export const tabstopsStateField = StateField.define({ 10 | create(){ 11 | return []; 12 | }, 13 | 14 | update(value: TabstopGroup[], transaction: Transaction){ 15 | let tabstopGroups = value; 16 | tabstopGroups.forEach(grp => grp.map(transaction.changes)); 17 | 18 | for (const effect of transaction.effects) { 19 | if (effect.is(addTabstopsEffect)) { 20 | tabstopGroups = []; 21 | tabstopGroups.unshift(...effect.value); 22 | } 23 | else if (effect.is(removeTabstopEffect)) { 24 | tabstopGroups.shift(); 25 | } 26 | else if (effect.is(removeAllTabstopsEffect)) { 27 | tabstopGroups = []; 28 | } 29 | } 30 | 31 | return tabstopGroups; 32 | }, 33 | 34 | provide: (field) => { 35 | return EditorView.decorations.of(view => { 36 | // "Flatten" the array of DecorationSets to produce a single DecorationSet 37 | const tabstopGroups = view.state.field(field); 38 | 39 | const decos = []; 40 | 41 | if (tabstopGroups.length >= 2){ 42 | decos.push(...tabstopGroups[1].getDecoRanges()); 43 | } 44 | 45 | return Decoration.set(decos, true); 46 | }); 47 | } 48 | }); 49 | 50 | 51 | export function getTabstopGroupsFromView(view: EditorView) { 52 | const currentTabstopGroups = view.state.field(tabstopsStateField); 53 | 54 | return currentTabstopGroups; 55 | } 56 | 57 | export function hasTabstops(view: EditorView) { 58 | return getTabstopGroupsFromView(view).length > 0; 59 | } 60 | 61 | export function addTabstops(view: EditorView, tabstopGroups: TabstopGroup[]) { 62 | view.dispatch({ 63 | effects: [addTabstopsEffect.of(tabstopGroups)], 64 | }); 65 | } 66 | 67 | export function removeTabstop(view: EditorView) { 68 | view.dispatch({ 69 | effects: [removeTabstopEffect.of(null)], 70 | }); 71 | } 72 | 73 | export function removeAllTabstops(view: EditorView) { 74 | view.dispatch({ 75 | effects: [removeAllTabstopsEffect.of(null)], 76 | }); 77 | } 78 | 79 | export function addTabstopsAndSelect(view: EditorView, tabstopGroups: TabstopGroup[]) { 80 | addTabstops(view, tabstopGroups); 81 | tabstopGroups[0].select(view, false); 82 | } 83 | 84 | 85 | export function tidyTabstops(view: EditorView) { 86 | // Clear all tabstop groups if there's just one remaining 87 | const currentTabstopGroups = getTabstopGroupsFromView(view); 88 | 89 | if (currentTabstopGroups.length === 1) { 90 | removeAllTabstops(view); 91 | } 92 | } 93 | 94 | export function isInsideATabstop(view: EditorView):boolean { 95 | const currentTabstopGroups = getTabstopGroupsFromView(view); 96 | 97 | for (const tabstopGroup of currentTabstopGroups) { 98 | if (tabstopGroup.containsSelection(view.state.selection)) { 99 | return true; 100 | } 101 | } 102 | 103 | return false; 104 | } 105 | 106 | export function isInsideCurTabstop(view: EditorView):boolean { 107 | const currentTabstopGroups = getTabstopGroupsFromView(view); 108 | 109 | if (currentTabstopGroups.length>1 && currentTabstopGroups[0].containsSelection(view.state.selection)) { 110 | return true; 111 | } 112 | 113 | return false; 114 | } 115 | 116 | export function consumeAndGotoNextTabstop(view: EditorView): boolean { 117 | // console.log('before-consume', getTabstopGroupsFromView(view)) 118 | // Check whether there are currently any tabstops 119 | if (getTabstopGroupsFromView(view).length === 0) return false; 120 | 121 | // Remove the tabstop that we're inside of 122 | removeTabstop(view); 123 | 124 | // Select the next tabstop 125 | const oldSel = view.state.selection; 126 | const nextGrp = getTabstopGroupsFromView(view)[0]; 127 | if (!nextGrp) return false; 128 | 129 | // If the old tabstop(s) lie within the new tabstop(s), simply move the cursor 130 | const shouldMoveToEndpoints = nextGrp.containsSelection(oldSel); 131 | nextGrp.select(view, shouldMoveToEndpoints); 132 | 133 | // If we haven't moved, go again 134 | const newSel = view.state.selection; 135 | 136 | if (oldSel.eq(newSel)) 137 | return consumeAndGotoNextTabstop(view); 138 | 139 | // If this was the last tabstop group waiting to be selected, remove it 140 | tidyTabstops(view); 141 | // console.log('after-consume', getTabstopGroupsFromView(view)) 142 | return true; 143 | } 144 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Annotation, EditorState, Extension, StateField, Transaction, TransactionSpec, Text} from '@codemirror/state'; 2 | import { EasyTypingSettingTab, EasyTypingSettings, PairString, ConvertRule} from "./settings" 3 | import { App, Plugin, Platform } from 'obsidian' 4 | import { TabstopSpec } from './tabstop'; 5 | 6 | let DEBUG = false; 7 | 8 | export const print=(message?: any, ...optionalParams: any[]) =>{ 9 | if (DEBUG) { 10 | console.log(message, ...optionalParams); 11 | } 12 | } 13 | 14 | export function setDebug(value: boolean) { 15 | DEBUG = value; 16 | } 17 | 18 | export function posToOffset(doc:Text, pos:{line:number, ch:number}) { 19 | return doc.line(pos.line + 1).from + pos.ch 20 | } 21 | export function offsetToPos(doc:Text, offset:number) { 22 | let line = doc.lineAt(offset) 23 | return {line: line.number - 1, ch: offset - line.from} 24 | } 25 | 26 | export function getTypeStrOfTransac(tr: Transaction): string { 27 | let TransacTypeArray:string[] = ["EasyTyping.change", "EasyTyping.paste", 28 | "input.type.compose", "input.type", "input.paste", "input.drop", "input.complete", "input", 29 | "delete.selection", "delete.forward", "delete.backward", "delete.cut", "delete", 30 | "move.drop", 31 | "undo", "redo", 32 | "select.pointer"]; 33 | for (let i: number = 0; i < TransacTypeArray.length; i++) 34 | { 35 | if (tr.isUserEvent(TransacTypeArray[i])) 36 | return TransacTypeArray[i]; 37 | } 38 | return "none" 39 | } 40 | 41 | export function string2pairstring(s: string):PairString{ 42 | let cursorIdx = findFirstPipeNotPrecededByBackslash(s); 43 | let left = s.substring(0, cursorIdx); 44 | let _left = isRegexp(left) ? left : convertEscapeChar(left); 45 | let right = s.substring(cursorIdx+1); 46 | let _right = isRegexp(right) ? right : convertEscapeChar(right) 47 | return {left:_left, right:_right}; 48 | } 49 | 50 | export function replacePlaceholders(str: string, replacements: string[]): string { 51 | let replace_matches = str.replace(/\[\[(\d+)\]\]/g, function (match, index) { 52 | return replacements[parseInt(index, 10)] || match; 53 | }); 54 | return replace_matches; 55 | } 56 | 57 | export function replacePlaceholdersAndTabstops(str: string, replacements: string[]): [string, TabstopSpec[]]{ 58 | let tabstops: TabstopSpec[] = []; 59 | const regex = /\$(\d+)|\$\{(\d+): *([^ {}]*?)\}|\[\[(\d+)\]\]/g; 60 | let match; 61 | interface ReplaceString { 62 | from: number; 63 | to: number; 64 | replacement: string; 65 | tabstop: boolean; 66 | tabstopNumber?: number; 67 | } 68 | 69 | let replaceStrings: ReplaceString[] = []; 70 | while ((match = regex.exec(str)) !== null) { 71 | // 检查是哪种模式的匹配 72 | const isSimpleVar = match[1]; // 用于 $n 形式 73 | const isNamedVar = match[2]; // 用于 ${n: 内容} 形式 74 | const content = match[3]; // 用于 ${n: 内容} 形式的内容 75 | const replaceN = match[4]; // 用于 [[n]] 形式 76 | 77 | const tabstopN = isSimpleVar || isNamedVar; // 取 n 的值,无论是简单还是命名变量 78 | const startIndex = match.index; 79 | const endIndex = startIndex + match[0].length; 80 | if (replaceN){ 81 | let matchedN = parseInt(replaceN, 10); 82 | if(matchedN < replacements.length){ 83 | replaceStrings.push({from: startIndex, to: endIndex, replacement: replacements[matchedN], tabstop:false}); 84 | } 85 | } 86 | else { 87 | let n = parseInt(tabstopN, 10); 88 | let contentStr = replacePlaceholders(content?content:"", replacements); 89 | replaceStrings.push({from: startIndex, to: endIndex, replacement: contentStr, tabstop:true, tabstopNumber: n}); 90 | } 91 | } 92 | 93 | let newString = str; 94 | let offset = 0; 95 | for (let i=0; i):ConvertRule[] { 139 | let res:ConvertRule[] = []; 140 | for (let i in list){ 141 | res[i] = {before: string2pairstring(list[i][0]), after: string2pairstring(list[i][1]), after_pattern: list[i][1]} 142 | } 143 | return res; 144 | } 145 | 146 | export function findFirstPipeNotPrecededByBackslash(s: string): number { 147 | let regex = /^r\/[^]*?\/\|/; 148 | let regMatch = s.match(regex); 149 | if (regMatch) return regMatch[0].length - 1; 150 | const match = s.match(/((^|[^\\])(\\\\)*)\|/); 151 | return match ? s.indexOf(match[0]) + match[1].length : -1; 152 | } 153 | 154 | export function stringDeleteAt(str: string, index: number):string 155 | { 156 | return str.substring(0, index)+str.substring(index+1); 157 | } 158 | 159 | export function stringInsertAt(str:string, index: number, s: string):string 160 | { 161 | return str.substring(0, index)+s+str.substring(index); 162 | } 163 | 164 | export function isParamDefined(param: any):boolean 165 | { 166 | return typeof param!=="undefined"; 167 | } 168 | 169 | export function showString(s: string):string{ 170 | return s.replace(/\n/g, '\\n'); 171 | } 172 | 173 | 174 | type TabOutResult = { 175 | isSuccess: boolean; 176 | newPosition: number; 177 | }; 178 | 179 | 180 | 181 | 182 | export function taboutCursorInPairedString(input: string, cursorPosition: number, symbolPairs: PairString[]): TabOutResult { 183 | let stack: string[] = []; 184 | let fail: TabOutResult = { isSuccess: false, newPosition: 0 }; 185 | 186 | for (let i = 0; i < cursorPosition; i++) { 187 | for (const { left: open, right: close } of symbolPairs) { 188 | if (input.startsWith(open, i) && (open !== close || stack.lastIndexOf(open) === -1)) { 189 | stack.push(open); 190 | i += open.length - 1; 191 | } else if (input.startsWith(close, i) && stack.length > 0) { 192 | const lastOpenIndex = stack.lastIndexOf(open); 193 | if (lastOpenIndex !== -1) { 194 | stack = stack.slice(0, lastOpenIndex); 195 | } 196 | i += close.length - 1; 197 | } 198 | } 199 | } 200 | 201 | if (stack.length === 0) { 202 | return fail; 203 | } 204 | 205 | let tempStack: string[] = []; 206 | for (let i = cursorPosition; i < input.length; i++) { 207 | for (const { left: open, right: close } of symbolPairs) { 208 | if (input.startsWith(open, i) && (open !== close || (stack.lastIndexOf(open) === -1 && tempStack.lastIndexOf(open) === -1))) { 209 | tempStack.push(open); 210 | i += open.length - 1; 211 | } else if (input.startsWith(close, i)) { 212 | const lastOpenIndex = tempStack.lastIndexOf(open); 213 | if (lastOpenIndex === -1 && stack.lastIndexOf(open) !== -1) { 214 | return { isSuccess: true, newPosition: cursorPosition === i ? i + close.length : i }; 215 | } else if (lastOpenIndex !== -1) { 216 | tempStack = tempStack.slice(0, lastOpenIndex); 217 | } 218 | i += close.length - 1; 219 | } 220 | } 221 | } 222 | 223 | return fail; 224 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | span[class="easy-typing-tabstops"] { 2 | border-radius: 2px; 3 | background-color: #87cefa2e; 4 | outline: #87cefa6e solid 1px; 5 | } 6 | 7 | span[class="easy-typing-cursor-widget"] { 8 | color: #1364ce6e; 9 | /* animation: blink 1s step-start 0s infinite; */ 10 | display: inline; 11 | position: absolute; 12 | white-space: pre; 13 | } 14 | 15 | /* @keyframes blink { 16 | 50% { opacity: 0; } 17 | } */ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | // "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "4.0.0": "0.12.0", 3 | "3.4.3": "0.9.12", 4 | "5.0.0": "0.15.0", 5 | "5.0.1": "0.15.0", 6 | "5.0.2": "0.15.0", 7 | "5.0.3": "0.15.0", 8 | "5.0.4": "0.15.0", 9 | "5.0.5": "0.15.0", 10 | "5.0.6": "0.15.0", 11 | "5.0.7": "0.15.0", 12 | "5.0.8": "0.15.0", 13 | "5.0.9": "0.15.0", 14 | "5.0.10": "0.15.0", 15 | "5.0.11": "0.15.0", 16 | "5.0.12": "0.15.0", 17 | "5.1.0": "0.15.0", 18 | "5.1.1": "0.15.0", 19 | "5.1.2": "0.15.0", 20 | "5.1.3": "0.15.0", 21 | "5.1.4": "0.15.0", 22 | "5.1.5": "0.15.0", 23 | "5.1.6": "0.15.0", 24 | "5.1.7": "0.15.0", 25 | "5.1.8": "0.15.0", 26 | "5.1.9": "0.15.0", 27 | "5.1.10": "0.15.0", 28 | "5.1.11": "0.15.0", 29 | "5.1.12": "0.15.0", 30 | "5.1.13": "0.15.0", 31 | "5.1.14": "0.15.0", 32 | "5.1.15": "0.15.0", 33 | "5.1.16": "0.15.0", 34 | "5.2.0": "0.15.0", 35 | "5.2.1": "0.15.0", 36 | "5.2.2": "0.15.0", 37 | "5.2.3": "0.15.0", 38 | "5.3.0": "0.15.0", 39 | "5.3.1": "0.15.0", 40 | "5.3.2": "0.15.0", 41 | "5.3.3": "0.15.0", 42 | "5.3.4": "0.15.0", 43 | "5.4.0": "0.15.0", 44 | "5.4.1": "0.15.0", 45 | "5.5.0": "0.15.0", 46 | "5.5.1": "0.15.0", 47 | "5.5.2": "0.15.0", 48 | "5.5.3": "0.15.0", 49 | "5.5.4": "0.15.0", 50 | "5.5.5": "0.15.0", 51 | "5.5.6": "0.15.0", 52 | "5.5.7": "0.15.0", 53 | "5.5.8": "0.15.0", 54 | "5.5.9": "0.15.0", 55 | "5.5.10": "0.15.0", 56 | "5.5.11": "0.15.0", 57 | "5.5.12": "0.15.0", 58 | "5.5.13": "0.15.0", 59 | "5.5.14": "0.15.0" 60 | } --------------------------------------------------------------------------------