├── versions.json ├── src ├── util.ts ├── types.ts ├── commands.ts ├── presets.ts ├── settings │ ├── types.ts │ ├── settingTab.ts │ └── donation.ts ├── link.ts ├── langs │ └── langs.ts ├── format.ts └── main.ts ├── Makefile ├── .gitignore ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── release.yml ├── manifest-beta.json ├── manifest.json ├── rollup.config.js ├── package.json ├── LICENSE ├── styles.css ├── docs └── Examples.md ├── CHANGELOG.md ├── README_ZH.md └── README.md /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.1": "0.9.7" 3 | } -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function renew(data: any) { 2 | return JSON.parse(JSON.stringify(data)); 3 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | push: 2 | @git tag "$(tag)" 3 | @git push origin "$(tag)" 4 | 5 | del: 6 | @git tag -d "$(tag)" 7 | @git push origin --delete "$(tag)" 8 | -------------------------------------------------------------------------------- /.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 | obsidian-text-format alias 16 | 17 | *.sh 18 | *.json 19 | !manifest.json 20 | !manifest-beta.json -------------------------------------------------------------------------------- /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 | "lib": ["dom", "es5", "scripthost", "es2015"] 13 | }, 14 | "include": ["**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { EditorSelectionOrCaret, EditorChange } from "obsidian"; 2 | 3 | export enum selectionBehavior { 4 | default = "do-nothing", 5 | wholeLine = "select-whole-line", 6 | } 7 | 8 | export interface FormatSelectionReturn { 9 | editorChange: EditorChange, 10 | resetSelection?: EditorSelectionOrCaret, 11 | resetSelectionOffset?: { anchor: number, head: number }, 12 | } 13 | 14 | // export interface TextFormatMemory { 15 | // lastCallout: string; 16 | // } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: I want the plugin can do ... 4 | title: '[FR] ' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | ### Feature Description 12 | 13 | 14 | ### Example 15 | 16 | Input: 17 | ```md 18 | The text that is selected. 19 | ``` 20 | 21 | Result: 22 | ``` 23 | Your expected result 24 | ``` 25 | 26 | -------------------------------------------------------------------------------- /manifest-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-text-format", 3 | "name": "Text Format", 4 | "version": "3.2.1-b1", 5 | "minAppVersion": "0.9.7", 6 | "description": "Format text such as lowercase/uppercase/capitalize/titlecase, converting order/bullet list, removing redundant spaces/newline characters.", 7 | "author": "Benature", 8 | "authorUrl": "https://github.com/Benature", 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://www.buymeacoffee.com/benature", 11 | "爱发电": "https://afdian.net/a/Benature-K", 12 | "微信/支付宝": "https://s2.loli.net/2024/01/30/jQ9fTSyBxvXRoOM.png" 13 | }, 14 | "isDesktopOnly": false 15 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-text-format", 3 | "name": "Text Format", 4 | "version": "3.1.0", 5 | "minAppVersion": "0.9.7", 6 | "description": "Format text such as lowercase/uppercase/capitalize/titlecase, converting order/bullet list, removing redundant spaces/newline characters.", 7 | "author": "Benature", 8 | "authorUrl": "https://github.com/Benature", 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://www.buymeacoffee.com/benature", 11 | "爱发电": "https://afdian.net/a/Benature-K", 12 | "微信/支付宝": "https://s2.loli.net/2024/01/30/jQ9fTSyBxvXRoOM.png" 13 | }, 14 | "isDesktopOnly": false 15 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | 5 | const isProd = process.env.BUILD === "production"; 6 | 7 | const banner = `/* 8 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 9 | if you want to view the source visit the plugins github repository 10 | */ 11 | `; 12 | 13 | export default { 14 | input: "src/main.ts", 15 | output: { 16 | dir: ".", 17 | sourcemap: "inline", 18 | sourcemapExcludeSources: isProd, 19 | format: "cjs", 20 | exports: "default", 21 | banner, 22 | }, 23 | external: ["obsidian"], 24 | plugins: [typescript(), nodeResolve({ browser: true }), commonjs()], 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-text-format", 3 | "version": "0.1.0", 4 | "description": "Format selected text upper/lower/capitalize/title or remove redundant blanks/enters.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js --environment BUILD:production" 9 | }, 10 | "keywords": [], 11 | "author": "Benature", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^18.0.0", 15 | "@rollup/plugin-node-resolve": "^11.2.1", 16 | "@rollup/plugin-typescript": "^8.2.1", 17 | "@types/node": "^14.14.37", 18 | "@types/uuid": "^9.0.8", 19 | "obsidian": "latest", 20 | "rollup": "^2.32.1", 21 | "tslib": "^2.2.0", 22 | "typescript": "^4.2.4", 23 | "uuid": "9.0.0" 24 | }, 25 | "dependencies": { 26 | "handlebars": "^4.7.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Benature Wong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: The plugin did not work as expectation 4 | title: "[Bug] " 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | **Problem Description**: 16 | 17 | 18 | **How to reproduce the problem**: 19 | 20 | 21 | Input: 22 | ```md 23 | The text that is selected 24 | ``` 25 | 26 | Result: 27 | ```md 28 | The result of Text Format's command 29 | ``` 30 | 31 | Expected: 32 | ```md 33 | The expected result 34 | ``` 35 | 36 | 37 | 38 | 39 | **Environment** 40 | - Obsidian version: 41 | - Text Format version: 42 | - Platform: 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | export const GlobalCommands = [ 2 | { 3 | id: "lowercase", 4 | icon: "case-sensitive", 5 | }, { 6 | id: "uppercase", 7 | icon: "case-sensitive", 8 | }, { 9 | id: "capitalize-word", 10 | icon: "case-sensitive", 11 | }, { 12 | id: "capitalize-sentence", 13 | icon: "case-sensitive", 14 | }, { 15 | id: "title-case", 16 | icon: "case-sensitive", 17 | }, { 18 | id: "cycle-case", 19 | icon: "case-sensitive", 20 | }, { 21 | id: "decodeURI", 22 | icon: "link", 23 | } 24 | ] 25 | 26 | export const CustomReplacementBuiltInCommands = [ 27 | { 28 | id: "remove-trailing-spaces", 29 | data: [{ 30 | search: String.raw`(\s*)(?=\n)|(\s*)$`, 31 | replace: "", 32 | }] 33 | }, 34 | { 35 | id: "remove-blank-line", 36 | data: [{ 37 | search: String.raw`\n\s*\n`, 38 | replace: String.raw`\n`, 39 | }] 40 | }, 41 | { 42 | id: "add-line-break", 43 | data: [{ 44 | search: String.raw`\n`, 45 | replace: String.raw`\n\n`, 46 | }] 47 | }, 48 | { 49 | id: "split-lines-by-blank", 50 | data: [{ 51 | search: String.raw` `, 52 | replace: String.raw`\n`, 53 | }] 54 | }, 55 | ] -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .plugin-text-format H3, 2 | .plugin-text-format H4 { 3 | margin-bottom: 0px; 4 | padding-bottom: 0px; 5 | } 6 | .plugin-text-format H4{ 7 | opacity: 0.7; 8 | } 9 | .plugin-text-format .heading-description { 10 | padding-top: 0px; 11 | color: var(--text-faint); 12 | } 13 | 14 | .plugin-text-format .header-div:has(H3) + .tf-collapsible-content { 15 | margin-top: 10px; 16 | } 17 | .plugin-text-format .header-div:has(H4) + .tf-collapsible-content { 18 | margin-top: 5px; 19 | } 20 | 21 | .plugin-text-format .setting-item > .setting-item-control > input { 22 | min-width: 145px; 23 | } 24 | 25 | .plugin-text-format .setting-item.custom-replace input, 26 | .plugin-text-format .setting-item.wrapper input { 27 | width: 33%; 28 | } 29 | .plugin-text-format .setting-item.api-request input:first-child { 30 | width: 30%; 31 | } 32 | .plugin-text-format .setting-item.api-request input { 33 | width: 70%; 34 | } 35 | 36 | .tf-collapsible-content.is-active { 37 | display: block; 38 | } 39 | 40 | .tf-collapsible-content { 41 | display: none; 42 | } 43 | 44 | .plugin-text-format .header-div:hover { 45 | background-color: var(--interactive-hover, #363636); 46 | border-radius: var(--button-radius, 5px); 47 | } 48 | 49 | .tf-collapsible-header { 50 | padding-bottom: 8px; 51 | padding-top: 4px; 52 | } 53 | 54 | .tf-collapsible-icon { 55 | position: relative; 56 | top: 4px; 57 | left: 16px; 58 | } 59 | -------------------------------------------------------------------------------- /src/presets.ts: -------------------------------------------------------------------------------- 1 | export const Ligatures = { 2 | "Ꜳ": "AA", 3 | "Æ": "AE", 4 | "Ꜵ": "AO", 5 | "Ꜷ": "AU", 6 | "Ꜹ": "AV", 7 | "Ꜻ": "AV", 8 | "Ꜽ": "AY", 9 | "ꜳ": "aa", 10 | "æ": "ae", 11 | "ꜵ": "ao", 12 | "ꜷ": "au", 13 | "ꜹ": "av", 14 | "ꜻ": "av", 15 | "ꜽ": "ay", 16 | "🙰": "et", 17 | "ff": "ff", 18 | "ffi": "ffi", 19 | "ffl": "ffl", 20 | "fi": "fi", 21 | "fl": "fl", 22 | "℔": "lb", 23 | "Ƕ": "Hv", 24 | "Ỻ": "lL", 25 | "Œ": "OE", 26 | "Ꝏ": "OO", 27 | "ƕ": "hv", 28 | "ỻ": "ll", 29 | "œ": "oe", 30 | "ꝏ": "oo", 31 | "ꭢ": "ɔe", 32 | "st": "st", 33 | "ſt": "ſt", 34 | "ᵫ": "ue", 35 | "ꭣ": "uo", 36 | "ẞ": "ſs", 37 | "Ꜩ": "TZ", 38 | "W": "VV", 39 | "Ꝡ": "VY", 40 | "ß": "ſz", 41 | "ꜩ": "tz", 42 | // "w": "vv", 43 | "ꝡ": "vy", 44 | "ꬱ": "aə", 45 | "ꭁ": "əø", 46 | "ȸ": "db", 47 | "ʣ": "dz", 48 | "ꭦ": "dʐ", 49 | "ʥ": "dʑ", 50 | "ʤ": "dʒ", 51 | "ʩ": "fŋ", 52 | "ʪ": "ls", 53 | "ʫ": "lz", 54 | "ɮ": "lʒ", 55 | "ꭀ": "oə", 56 | "ȹ": "qp[c]", 57 | "ʨ": "tɕ", 58 | "ʦ": "ts", 59 | "ꭧ": "tʂ", 60 | "ʧ": "tʃ", 61 | "ꭐ": "ui", 62 | "ꭑ": "ui", 63 | "ɯ": "uu", 64 | }; 65 | 66 | export const GreekLetters: { [key: string]: string } = { 67 | 'α': '\\alpha', 68 | 'β': '\\beta', 69 | 'γ': '\\gamma', 70 | 'δ': '\\delta', 71 | 'ε': '\\varepsilon', 72 | 'ζ': '\\zeta', 73 | 'η': '\\eta', 74 | 'θ': '\\theta', 75 | 'ι': '\\iota', 76 | 'κ': '\\kappa', 77 | 'λ': '\\lambda', 78 | 'μ': '\\mu', 79 | 'ν': '\\nu', 80 | 'ξ': '\\xi', 81 | 'ο': '\\omicron', 82 | 'π': '\\pi', 83 | 'ρ': '\\rho', 84 | 'σ': '\\sigma', 85 | 'τ': '\\tau', 86 | 'υ': '\\upsilon', 87 | 'φ': '\\varphi', 88 | 'χ': '\\chi', 89 | 'ψ': '\\psi', 90 | 'ω': '\\omega', 91 | 'Α': '\\Alpha', 92 | 'Β': '\\Beta', 93 | 'Γ': '\\Gamma', 94 | 'Δ': '\\Delta', 95 | 'Ε': '\\Epsilon', 96 | 'Ζ': '\\Zeta', 97 | 'Η': '\\Eta', 98 | 'Θ': '\\Theta', 99 | 'Ι': '\\Iota', 100 | 'Κ': '\\Kappa', 101 | 'Λ': '\\Lambda', 102 | 'Μ': '\\Mu', 103 | 'Ν': '\\Nu', 104 | 'Ξ': '\\Xi', 105 | 'Ο': '\\Omicron', 106 | 'Π': '\\Pi', 107 | 'Ρ': '\\Rho', 108 | 'Σ': '\\Sigma', 109 | 'Τ': '\\Tau', 110 | 'Υ': '\\Upsilon', 111 | 'Φ': '\\Phi', 112 | 'Χ': '\\Chi', 113 | 'Ψ': '\\Psi', 114 | 'Ω': '\\Omega', 115 | ' ̃': '\\tilde ', 116 | '∞': '\\infty', 117 | '≠': '\\neq', 118 | '≠': '\\neq', 119 | '≤': '\\leq', 120 | '≥': '\\geq', 121 | ',...,': ',\\dots,', 122 | '∂': '\\partial', 123 | }; -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release plugin 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10 8 | 9 | env: 10 | PLUGIN_NAME: obsidian-format-text 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: "14.x" # You might need to adjust this value to your own version 22 | - name: Build 23 | id: build 24 | run: | 25 | npm install -g yarn 26 | yarn 27 | yarn run build --if-present 28 | mkdir ${{ env.PLUGIN_NAME }} 29 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} 30 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 31 | ls 32 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 33 | - name: Create Release 34 | id: create_release 35 | uses: actions/create-release@v1 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | VERSION: ${{ github.ref }} 39 | with: 40 | tag_name: ${{ github.ref }} 41 | release_name: ${{ github.ref }} 42 | draft: false 43 | prerelease: ${{ contains(github.ref, 'b') && true || false }} 44 | - name: Upload zip file 45 | id: upload-zip 46 | uses: actions/upload-release-asset@v1 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | upload_url: ${{ steps.create_release.outputs.upload_url }} 51 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 52 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 53 | asset_content_type: application/zip 54 | - name: Upload main.js 55 | id: upload-main 56 | uses: actions/upload-release-asset@v1 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | upload_url: ${{ steps.create_release.outputs.upload_url }} 61 | asset_path: ./main.js 62 | asset_name: main.js 63 | asset_content_type: text/javascript 64 | - name: Upload manifest.json 65 | id: upload-manifest 66 | uses: actions/upload-release-asset@v1 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | upload_url: ${{ steps.create_release.outputs.upload_url }} 71 | asset_path: ./manifest.json 72 | asset_name: manifest.json 73 | asset_content_type: application/json 74 | - name: Upload styles.css 75 | id: upload-styles 76 | uses: actions/upload-release-asset@v1 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 79 | with: 80 | upload_url: ${{ steps.create_release.outputs.upload_url }} 81 | asset_path: ./styles.css 82 | asset_name: styles.css 83 | asset_content_type: text/css 84 | -------------------------------------------------------------------------------- /src/settings/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface WrapperSetting { 3 | id: string; 4 | name: string; 5 | prefix: string; 6 | suffix: string; 7 | } 8 | 9 | export interface APIRequestSetting { 10 | id: string; 11 | name: string; 12 | url: string; 13 | } 14 | 15 | export interface WikiLinkFormatGroup { 16 | headingOnly: string; 17 | aliasOnly: string; 18 | both: string; 19 | } 20 | 21 | export interface customReplaceSettingPair { 22 | search: string; 23 | replace: string; 24 | } 25 | 26 | export interface customReplaceSetting { 27 | id: string; 28 | name: string; 29 | data: Array; 30 | } 31 | 32 | export enum Wikilink2mdPathMode { 33 | relativeObsidian = "relative-obsidian", 34 | relativeFile = "relative-file", 35 | absolute = "absolute", 36 | } 37 | 38 | export enum CalloutTypeDecider { 39 | wholeFile = "whole-file", 40 | preContent = "previous-content", 41 | // lastUsed = "last-used", 42 | fix = "fix", 43 | } 44 | 45 | export interface CustomReplaceBuiltIn { 46 | id: string; 47 | modified: boolean; 48 | data: Array; 49 | } 50 | 51 | export interface FormatSettings { 52 | manifest: { 53 | version: string; 54 | } 55 | MergeParagraph_Newlines: boolean; 56 | MergeParagraph_Spaces: boolean; 57 | LowercaseFirst: boolean; 58 | RemoveBlanksWhenChinese: boolean; 59 | ZoteroNoteRegExp: string; 60 | ZoteroNoteTemplate: string; 61 | BulletPoints: string; 62 | WrapperList: Array; 63 | RequestList: Array; 64 | customReplaceList: Array; 65 | customReplaceBuiltInLog: { [id: string]: CustomReplaceBuiltIn }; 66 | ToggleSequence: string; 67 | RemoveWikiURL2: boolean; 68 | WikiLinkFormat: WikiLinkFormatGroup; 69 | UrlLinkFormat: string; 70 | ProperNoun: string; 71 | OrderedListOtherSeparator: string; 72 | Wikilink2mdRelativePath: Wikilink2mdPathMode; 73 | calloutType: string; 74 | debugMode: boolean; 75 | headingLevelMin: number; 76 | calloutTypeDecider: CalloutTypeDecider; 77 | formatOnSaveSettings: { 78 | enabled: boolean; 79 | commandsString: string; 80 | } 81 | } 82 | 83 | export const DEFAULT_SETTINGS: FormatSettings = { 84 | manifest: { 85 | version: "0.0.0", 86 | }, 87 | MergeParagraph_Newlines: true, 88 | MergeParagraph_Spaces: true, 89 | LowercaseFirst: true, 90 | RemoveBlanksWhenChinese: false, 91 | ZoteroNoteRegExp: String.raw`“(?.*)” \((?.*?)\) \(\[pdf\]\((?.*?)\)\)`, 92 | ZoteroNoteTemplate: "{text} [🔖]({pdf_url})", 93 | BulletPoints: "•–§", 94 | WrapperList: [{ name: "underline", prefix: "", suffix: "", id: "underline" }], 95 | RequestList: [], 96 | customReplaceList: [], 97 | customReplaceBuiltInLog: {}, 98 | ToggleSequence: "titleCase\nlowerCase\nupperCase", 99 | RemoveWikiURL2: false, 100 | WikiLinkFormat: { headingOnly: "{title} (> {heading})", aliasOnly: "{alias} ({title})", both: "{alias} ({title} > {heading})" }, 101 | UrlLinkFormat: "{text}", 102 | ProperNoun: "", 103 | OrderedListOtherSeparator: String.raw``, 104 | Wikilink2mdRelativePath: Wikilink2mdPathMode.relativeObsidian, 105 | calloutType: "NOTE", 106 | debugMode: false, 107 | headingLevelMin: 0, 108 | calloutTypeDecider: CalloutTypeDecider.preContent, 109 | formatOnSaveSettings: { 110 | enabled: false, 111 | commandsString: "", 112 | } 113 | }; -------------------------------------------------------------------------------- /docs/Examples.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | ## Lowercase 3 | 4 | ```diff 5 | - I love using Obsidian. 6 | + i love using obsidian. 7 | ``` 8 | 9 | ## Uppercase 10 | 11 | ```diff 12 | - I love using Obsidian. 13 | + I LOVE USING OBSIDIAN. 14 | ``` 15 | 16 | ## capitalize word 17 | ```diff 18 | - Hello, I am using Obsidian. 19 | + Hello, I Am Using Obsidian. 20 | ``` 21 | 22 | ## capitalize sentence 23 | ```diff 24 | - hello, I am using Obsidian. 25 | + Hello, I am using Obsidian. 26 | ^ 27 | ``` 28 | ## title case 29 | ```diff 30 | - Obsidian is a good app. 31 | + Obsidian Is a Good App. 32 | ^ ^ ^ 33 | ``` 34 | 35 | ## Slugify 36 | 37 | ```diff 38 | - I love using Obsidian. 39 | + i-love-using-obsidian 40 | ``` 41 | 42 | ## Snakify 43 | 44 | ```diff 45 | - I love using Obsidian. 46 | + i_love_using_obsidian. 47 | ``` 48 | 49 | # List 50 | 51 | ## Convert table to bullet list with header 52 | 53 | Select: 54 | 55 | | key | value | 56 | | --- | ----- | 57 | | a | b | 58 | | c | d | 59 | 60 | Result: 61 | 62 | - a 63 | - value: b 64 | - c 65 | - value: d 66 | 67 | ## Convert table to bullet list without header 68 | 69 | Select: 70 | 71 | | key | value | 72 | | --- | ----- | 73 | | a | b | 74 | | c | d | 75 | 76 | Result: 77 | 78 | - a 79 | - b 80 | - c 81 | - d 82 | 83 | ## Sort to-do list 84 | 85 | Select: 86 | 87 | - [ ] Task a 88 | - [x] Task b 89 | - [ ] Task c 90 | 91 | Result: 92 | 93 | - [ ] Task a 94 | - [ ] Task c 95 | - [x] Task b 96 | 97 | # Links 98 | ## Remove WikiLinks format 99 | 100 | ```diff 101 | - It is a [[WikiLink]]. It is a [[WikiLink|link]] with alias. Link to a [[WikiLink#Heading]]. Link to a [[WikiLink#Heading|head alias]] with alias. 102 | + It is a WikiLink. It is a link (WikiLink) with alias. Link to a WikiLink (>>> Heading). Link to a head alias (WikiLink > Heading) with alias. 103 | ``` 104 | 105 | When the settings are 106 | - WikiLink with heading: `{title} (>>> {heading})` 107 | - WikiLink with alias: `{alias} ({title})` 108 | - WikiLink with both heading and alias: `{alias} ({title} > {heading})` 109 | 110 | ## Remove URL links format 111 | 112 | ```diff 113 | - A link like [Google](www.google.com). 114 | + A link like Google. 115 | ``` 116 | 117 | ## Convert URL links to WikiLinks 118 | 119 | ```diff 120 | - A link like [Google](www.google.com). 121 | + A link like [[Google]]. 122 | ``` 123 | 124 | ## Convert WikiLinks to plain markdown links 125 | 126 | ```diff 127 | - It is a [[WikiLink]]. It is a [[WikiLink|link]] with alias. Link to a [[WikiLink#Heading]]. Link to a [[WikiLink#Heading|head alias]] with alias. 128 | + It is a [WikiLink](WikiLink.md). It is a [link](WikiLink.md) with alias. Link to a [WikiLink#Heading](WikiLink.md#Heading). Link to a [head alias](WikiLink.md) with alias. 129 | ``` 130 | 131 | # Copy / OCR 132 | ## Remove redundant spaces 133 | 134 | ```diff 135 | - The text appears to have been copied from another source, and the OCR result may also be out of format. 136 | ^ 137 | + The text appears to have been copied from another source, and the OCR result may also be out of format. 138 | ``` 139 | 140 | ## Remove all spaces 141 | 142 | ```diff 143 | - The text appears to have been copied from another source, and the OCR result may also be out of format. 144 | + Thtextappearstohavebeencopiedfromanothersource,andtheOCRresultmayalsobeoutofformat. 145 | ``` 146 | 147 | ## Convert to Chinese punctuation marks 148 | 149 | ```diff 150 | - 中文标点异常.比如是复制的文本,或者是 OCR 的结果. 151 | + 中文标点异常。比如是复制的文本,或者是 OCR 的结果。 152 | ``` 153 | 154 | # Academic / Study 155 | 156 | ## Math mode 157 | 158 | ```diff 159 | - Suppose variable x1 add x2 equals to variable Y. 160 | + Suppose variable $x_1$ add $x_2$ equals to variable $Y$. 161 | 162 | 163 | ... 164 | 165 | > Note: The document is still under development. -------------------------------------------------------------------------------- /src/link.ts: -------------------------------------------------------------------------------- 1 | import { stringFormat } from "./format"; 2 | import { WikiLinkFormatGroup, Wikilink2mdPathMode } from "./settings/types"; 3 | import TextFormat from "./main"; 4 | 5 | export function removeWikiLink(s: string, formatGroup: WikiLinkFormatGroup): string { 6 | return s.replace(/\[\[.*?\]\]/g, function (t) { 7 | let wiki_exec = /\[\[(?[^\[#|]+)?(?<heading>#[^|\]]+)?(?<alias>\|[^|\]]+)?\]\]/g.exec(t); 8 | let G = wiki_exec.groups; 9 | console.log(G) 10 | let groupArgs = { 11 | title: G.title === undefined ? '' : G.title, 12 | heading: G.heading?.slice(1), 13 | alias: G.alias?.slice(1) 14 | }; 15 | console.log(groupArgs); 16 | if (G.heading === undefined && G.alias === undefined) { 17 | return G.title; 18 | } else if (G.alias !== undefined && G.heading === undefined) { 19 | return stringFormat(formatGroup.aliasOnly, groupArgs); 20 | } else if (G.alias === undefined && G.heading !== undefined) { 21 | return stringFormat(formatGroup.headingOnly, groupArgs); 22 | } else { 23 | console.log(groupArgs); 24 | return stringFormat(formatGroup.both, groupArgs); 25 | } 26 | }); 27 | } 28 | 29 | const RegexMarkdownLink = /\[(.+?)\]\((?:[^)]+\([^)]+\)[^)]*|[^)]+)\)/g; 30 | 31 | export function removeUrlLink(s: string, UrlLinkFormat: string): string { 32 | console.log(s) 33 | const rx = RegexMarkdownLink; 34 | return s.replace(rx, function (t) { 35 | // TODO: add a setting to decide whether remove url link (starts with http) only or all kinds of links 36 | // const regex = /\[(?<text>.*?)\]\((?<url>https?:\/\/[\S\s]+)\)/; 37 | const regex = /\[(?<text>.*?)\]\((?<url>[\S\s]+?)\)/; 38 | const match = t.match(regex); 39 | console.log(match) 40 | if (match && match.length === 3) { 41 | return stringFormat(UrlLinkFormat, match.groups); 42 | } else { 43 | return t; 44 | } 45 | }); 46 | } 47 | 48 | export function url2WikiLink(s: string): string { 49 | let rx = RegexMarkdownLink; 50 | return s.replace(rx, function (t) { 51 | return `[[${t.match(/\[(.*?)\]/)[1]}]]`; 52 | }); 53 | } 54 | 55 | export function convertWikiLinkToMarkdown(wikiLink: string, plugin: TextFormat): string { 56 | const regex = /\[\[([^|\]]+)\|?([^\]]+)?\]\]/g; 57 | 58 | const markdown = wikiLink.replace(regex, (match, p1, p2) => { 59 | const linkText = p2 ? p2.trim() : p1.trim(); 60 | let linkTarget = p1.trim().replace(/#.*$/g, "") + ".md"; 61 | 62 | const note = plugin.app.vault.getAllLoadedFiles().find(file => file.name === linkTarget); 63 | let linkURL = linkTarget; 64 | if (note) { 65 | linkURL = note.path; 66 | switch (plugin.settings.Wikilink2mdRelativePath) { 67 | case Wikilink2mdPathMode.absolute: 68 | // @ts-ignore 69 | linkURL = plugin.app.vault.adapter.basePath + "/" + linkURL; 70 | break; 71 | case Wikilink2mdPathMode.relativeFile: 72 | const currentFilePath = plugin.app.workspace.getActiveFile().path; 73 | linkURL = relativePath(linkURL, currentFilePath) 74 | break; 75 | } 76 | linkURL = linkURL.replace(/\s/g, "%20"); 77 | } 78 | const matchAlias = linkText.match(/#(.*)$/); 79 | let aliasLink = ""; 80 | if (matchAlias) { 81 | aliasLink = "#" + matchAlias[1].replace(/\s/g, "%20"); 82 | } 83 | return `[${linkText}](${linkURL}${aliasLink})`; 84 | }); 85 | 86 | return markdown; 87 | } 88 | 89 | function relativePath(pathA: string, pathB: string): string { 90 | const splitPathA = pathA.split('/'); 91 | const splitPathB = pathB.split('/'); 92 | 93 | // 找到共同根路径 94 | let commonRootIndex = 0; 95 | while (commonRootIndex < Math.min(splitPathA.length - 1, splitPathB.length - 1) 96 | && splitPathA[commonRootIndex] === splitPathB[commonRootIndex]) { 97 | commonRootIndex++; 98 | } 99 | 100 | // 构建相对路径 101 | let relativePath = ''; 102 | for (let i = commonRootIndex; i < splitPathB.length - 1; i++) { 103 | relativePath += '../'; 104 | } 105 | 106 | // 将路径 A 的剩余部分添加到相对路径中 107 | for (let i = commonRootIndex; i < splitPathA.length; i++) { 108 | relativePath += splitPathA[i] + '/'; 109 | } 110 | 111 | return relativePath.slice(0, -1); // 去除末尾的斜杠 112 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 🧱 [Todo Project](https://github.com/Benature/obsidian-text-format/projects/1) 4 | 5 | ## 🏗️ developed 6 | > to be update in the next version 7 | 8 | 9 | ## 3.2.1-b2 10 | - [fix] Convert Table to Bullet List, may be related to #99 11 | - [updated] latex math add: `'≠': '\\neq'` 12 | 13 | ## 3.2.0-b1 14 | - [feature] Settings: global configuration for format on paste #97 15 | 16 | ## 3.1.0 17 | - [feature] several commands (e.g. decode URI) support in front matter (metadata) 18 | - [feature] command for open settings tab directly 19 | - [fix] convert Latex has no editor when formatting on paste 20 | 21 | ## 3.0.5 22 | - [updated] Chinese: command translation and description of Upper/Lower heading level 23 | 24 | ## 3.0.4 25 | - [PR] #96 26 | - [updated] Chinese description of Upper/Lower heading level 27 | 28 | ## 3.0.3 29 | - [fix] wrapper did not work in canvas 30 | 31 | ## 3.0.2 32 | - [update] #93 33 | - [feature] #90 34 | - [feature] #89 35 | 36 | ## 3.0.1 37 | - [feature] Only sort the todo block where the caret is. more details at [#46](https://github.com/Benature/obsidian-text-format/issues/46#issuecomment-2008929183). 38 | - [fix] `#` for capitalizeSentence 39 | - [update] #81 40 | 41 | ## 3.0.0 42 | - [feature] support multi-cursor for commands ([#48](https://github.com/Benature/obsidian-text-format/issues/48)) 43 | - [feature] heading upper/lower for multi-lines ([#83](https://github.com/Benature/obsidian-text-format/issues/83)) 44 | - [fix] remove all `(?<=)` for compatible issue ([#82](https://github.com/Benature/obsidian-text-format/issues/82)) 45 | - [renamed] rename ~~`toggle case`~~ to `cycle case` (id ~~`togglecase`~~ to `cycle-case`), rename ~~`titlecase`~~ to `title-case`. (Hotkeys on these commands need to be re-configured) 46 | - [update] beautify settings ([#84](https://github.com/Benature/obsidian-text-format/issues/84)) 47 | - [feature] format on paste for plain text ([#86](https://github.com/Benature/obsidian-text-format/issues/86)) 48 | 49 | ## 2.7.1 50 | - [fix] `customReplace()`: error when search contains like `\.\!\?` 51 | - [update] `Chinese-punctuation`: select modification at last 52 | - [update] `math mode`: 53 | - calculation support `=` and Greek letters 54 | - if selectedText is surrounded by `$`, convert unicode Greek letters to latex commands 55 | 56 | ## 2.7.0 57 | - [update] `Chinese-punctuation`: only remove spaces between Chinese characters and punctuations rather than all spaces 58 | - [update] `Format bullet list` 59 | - Keep selecting whole paragraphs after formatting rather than set cursor at the end. 60 | - Ensure first line starts with `- ` if multiple lines are selected 61 | 62 | ### *2.7.0-b1* 63 | 64 | - [notice] refactoring i18n, but zh-tw language will be deprecated later because of the limitation of developing time. 65 | - [feature] support [#68](https://github.com/Benature/obsidian-text-format/issues/68) 66 | - [update] `Convert wikiLinks to plain markdown links in selection`: Three path modes 67 | - absolute 68 | - relative to Obsidian 69 | - relative to file 70 | - [feature] collapsible heading in settings 71 | - [feature] custom replacement 72 | - [update] callout format: auto select whole paragraph 73 | - [feature] `math mode`: 74 | - rename: Detect and convert characters to math mode (LaTeX) 75 | - two characters (sub case): e.g. `a0` -> `$a_0$` 76 | - [ ] More ignore case should be considered. For now: `/is|or|as|to|am|an|at|by|do|go|ha|he|hi|ho|if|in|it|my|no|of|on|so|up|us|we/g` 77 | - simple calculation: e.g. `ab+cd` -> `$a_b+c_d$` 78 | - sup case for `*`: e.g. `a*` -> `$a^*$` 79 | 80 | ## 2.6.0 81 | - [feature] Convert wikiLinks to plain markdown links in selection ([#40](https://github.com/Benature/obsidian-text-format/issues/40)) 82 | - [feature] remove trailing spaces ([#61](https://github.com/Benature/obsidian-text-format/issues/61)) 83 | - [update] decodeURI 84 | - `%2F` -> `/` 85 | - support more schemes, not only http, https, ftp, etc. 86 | - [fix] `Chinese-punctuation`: remove blanks beside punctuation marks 87 | 88 | ## 2.5.1 89 | - [update] Wrapper & Request API commands not need to re-enable plugin 90 | - [update] Custom separator RegExp for "Detect and format ordered list" 91 | - [fix] #79 92 | 93 | ## 2.5.0 94 | - [feature] Wrapper: support template for metadata (file properties). 95 | - [feature] New Setting: Proper nouns, which are ignored in title case. e.g., USA, UFO. 96 | - [update] paste Zotero note: if regex fail to match, return original text in clipboard. 97 | - [update] General: commands support Chinese language. 命令支持中文显示 98 | - [add] Chinese README 99 | 100 | ## 2.4.1 101 | - [fix] support canvas 102 | 103 | ## 2.4.0 104 | - [feature] Heading upper/lower 105 | - [update] Request API support multi API setting 106 | 107 | ## 2.3.0 108 | - [fix] merge PR #70 : fix typo 109 | - [feature] merge PR #69 : snakify command 110 | - [feature] merge PR #57 : slugify command 111 | 112 | ## 2.2.10 113 | - [update] latex-letter convert 114 | 115 | ## 2.2.9 116 | - lowercase first default as TRUE (#65) 117 | - fix #66 118 | - support #67 119 | - [feature] convert to English punctuation marks 120 | - case: url contains () pair (#72) 121 | - Swedish characters #74 122 | - [update] update url regex 123 | 124 | ## 2.2.8 125 | - [fix] decode URI 126 | - [fix] #55 127 | - [fix] #64 128 | - [fix] #60 129 | - [fix] #63 130 | 131 | ## 2.2.7 132 | - [fix] convert table to bullet list 133 | 134 | ## 2.2.6 135 | - [update] #56 136 | 137 | ## 2.2.5 138 | - [update] #45 139 | 140 | ## 2.2.4 141 | - [feature] #24 142 | - [update] #45 143 | 144 | ## 2.2.3 145 | - [feature] #45 146 | - [feature] #51 147 | 148 | ## 2.2.2 149 | - [update] #54 150 | - [fix] #53 151 | - [fix] #50 152 | - [fix] #49 153 | - [merged] #42 154 | - [update] #41 155 | 156 | ## 2.2.1 157 | - [fix] #38 158 | 159 | ## 2.2.0 160 | - [feature] merge: toggle-case 161 | 162 | ## 2.1.0 163 | - [feature] API request 164 | 165 | ## 2.0.0 166 | - [fix] support mobile 167 | - [update] sort todo -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # Text Format 文本格式化插件 2 | 3 | <div align="center"> 4 | 5 | ![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22obsidian-text-format%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) ![GitHub stars](https://img.shields.io/github/stars/Benature/obsidian-text-format?style=flat) ![latest download](https://img.shields.io/github/downloads/Benature/obsidian-text-format/latest/total?style=plastic) 6 | [中文 | [English](https://github.com/Benature/obsidian-text-format)] 7 | 8 | </div> 9 | 10 | ⚠️ **中文文档不如英文文档更新及时,如有任何疑问、建议可在公众号(恰好恰好)、[小红书](https://www.xiaohongshu.com/user/profile/5b63f42ce8ac2b773f832471)(木一Benature)直接联系作者。** 11 | 12 | 13 | >当我做笔记时,有时会遇到一些问题,比如 14 | >1. 我从PDF或其他来源复制了一些文本,但复制的内容格式不正确。例如,单词之间有超过一个空格,或者一个段落分成了几行。 15 | >2. 需要小写字母,但它们都是大写的等等。 16 | >3. 等等...... 17 | 18 | 此插件用于将所选文本格式化为小写字母/大写字母/首字母大写/标题大小写或删除多余的空格/换行字符,以及下面列出的其他功能。 19 | 20 | [点击立即安装此插件](https://obsidian.md/plugins?id=obsidian-text-format) 21 | 22 | ## 功能 23 | 24 | 按 <kbd>cmd/ctrl+P</kbd> 进入命令。👇 25 | 26 | 或者您可以考虑根据 [#29](https://github.com/Benature/obsidian-text-format/issues/29#issuecomment-1279246640) 绑定自定义热键。 27 | 28 | --- 29 | 30 | - [Text Format 文本格式化插件](#text-format-文本格式化插件) 31 | - [功能](#功能) 32 | - [基本](#基本) 33 | - [Markdown 语法](#markdown-语法) 34 | - [列表](#列表) 35 | - [链接](#链接) 36 | - [PDF 复制 / OCR](#pdf-复制--ocr) 37 | - [学术 / 学习](#学术--学习) 38 | - [Zotero 格式](#zotero-格式) 39 | - [将引用索引转换为论文笔记的文件名](#将引用索引转换为论文笔记的文件名) 40 | - [其他](#其他) 41 | - [包装](#包装) 42 | - [支持](#支持) 43 | - [一些示例](#一些示例) 44 | 45 | 46 | ⚙️: 此命令有设置。(<kbd>cmd/ctrl+,</kbd>打开首选项设置) 47 | 48 | ### 基本 49 | 50 | | 命令 | 描述 | 51 | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | 52 | | 将选中文本转换为**小写** | 将选择中的所有字母转为小写 | 53 | | 将选中文本转换为**大写** | 将选择中的所有字母转为大写 | 54 | | 将选中文本中的所有单词**首字母大写** ⚙️ | 将选择中的所有单词的首字母大写 | 55 | | 将选中文本中的**句子**的**首字母大写** ⚙️ | 仅将选择中的句子的第一个单词的首字母大写 | 56 | | 将选中文本转换为**标题格式大小写** ⚙️ | 将选择中的单词首字母大写,但保留选择中的某些单词的小写 *(注意:目前不支持 Cyrillic 字符串)* [#1](https://github.com/Benature/obsidian-text-format/issues/1) | 57 | | 触发选中文本**大小写切换** ⚙️ | 一个自定义循环,用于格式化选择 | 58 | | **Slugify** 所选文本 | 将任何输入文本转换为 URL-friendly Slug | 59 | | **Snakify** 所选文本 | 将选择中的所有字母转为小写 *并* 用下划线替换空格 | 60 | 61 | ### Markdown 语法 62 | 63 | | 命令 | 描述 | 64 | | ------------ | --------------------------------------------------------- | 65 | | 标题上升一级 | 例如:`# 标题` -> `## 标题`(默认快捷键:`Ctrl+Shift+]`) | 66 | | 标题下降一级 | 例如:`## 标题` -> `# 标题`(默认快捷键:`Ctrl+Shift+[`) | 67 | 68 | 69 | #### 列表 70 | | 命令 | 描述 | 71 | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- | 72 | | 识别并格式化**无序列表** ⚙️ | 将 `•` 更改为圆点列表,即 `- `;将每个圆点之间拆分为单独的行;并删除空行。 | 73 | | 识别并格式化**有序列表** | 将 `*)`(星号可以是任何字母)更改为有序列表(例如 `1. `, `2. `);将每个有序点之间拆分为单独的行;并删除空行。 (#4) | 74 | | 将表格转换为无序列表(不含标题) | 第一个卷是第1个列表,其他卷是子列表 | 75 | | 将表格转换为无序列表(含标题) | 子列表以 `${header}: ` 开始 | 76 | | 将待办事项列表排序 | [#37](https://github.com/Benature/obsidian-text-format/issues/37) | 77 | 78 | #### 链接 79 | 80 | | 命令 | 描述 | 81 | | -------------------------------------------- | ------------------------------------------------- | 82 | | 移除选中文本中的 WikiLinks 格式 | 将 `[[WikiLinks]]` 转换为 `WikiLinks` (#28) | 83 | | 移除选中文本中的 URL 链接格式 | 将 `[Google](www.google.com)` 转换为 `Google` | 84 | | 将选中文本中的 URL 链接转换为 WikiLinks 格式 | 将 `[Google](www.google.com)` 转换为 `[[Google]]` | 85 | 86 | ### PDF 复制 / OCR 87 | 88 | | 命令 | 描述 | 89 | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | 90 | | 将选中文本中的**多余空格**移除 | 确保英文单词之间只有一个空格 | 91 | | 将选中文本中的**所有空格**移除 | 删除所有空格 | 92 | | 将选中文本中的**行末空格**移除 | 删除行末空格([#61](https://github.com/Benature/obsidian-text-format/issues/61)) | 93 | | 将选中文本中的**空行移除** | 将 `\n\n` 替换为 `\n` | 94 | | 将选中文本中的**断行合并** ⚙️ | 将选定的行更改为单行,除非行之间用空行分隔。*同时,空行将合并为一个空行(可选,默认启用),并且多余的空格将被删除(可选,默认启用)。* | 95 | | 将选中文本**按空格分行** | 将 ` ` 替换为 `\n` 以用于 OCR 使用情况。 | 96 | | 转换为**中文标点符号**(,;:!?)⚙️ | 用于 OCR 情况。 (对于需要更多自定义设置的人,我建议使用 <https://p.gantrol.com/>) | 97 | | 转换为**英文标点符号** | 类似于 `转换为中文标点符号 (,;:!?)` | 98 | | 移除**连字符** | 移除连字符(例如,从 PDF 中粘贴文本时)[#15](https://github.com/Benature/obsidian-text-format/issues/15) | 99 | | 替换**连字** | 将 [连字](https://en.wikipedia.org/wiki/Ligature_(writing)) 替换为非连字 [#24](https://github.com/Benature/obsidian-text-format/issues/24) | 100 | 101 | 注:`\n`为换行符 102 | 103 | ### 学术 / 学习 104 | 105 | | 命令 | 描述 | 106 | | -------------------------------------------- | ----------------------------------------------------------------- | 107 | | 将选中内容转换为 **Anki** 卡片格式 | [#32](https://github.com/Benature/obsidian-text-format/pull/32) | 108 | | 移除引用索引编号 | 例如,`一项关于笔记的研究 [12]` => `一项关于笔记的研究` | 109 | | 从剪贴板获取 **Zotero** 笔记并粘贴 ⚙️ | 请参阅下面 [⬇️](#zotero格式) | 110 | | 将单个字母转换为数学模式(LaTeX) | 例如将 `P` 转换为 `$P$`(LaTeX),适用于除 `a` 之外的所有单个字母 | 111 | | 将 Mathpix 的 LaTeX 数组转换为 Markdown 表格 | 将 Mathpix 生成的 LaTeX 数组转换为 markdown 表格格式 | 112 | 113 | #### Zotero 格式 114 | 格式模板可参考 https://www.zotero.org/support/note_templates 115 | - 默认 116 | - Zotero: `<p>{{highlight quotes='true'}} {{citation}} {{comment}}</p>` 117 | - 插件配置: `“(?<text>.*)” \((?<item>.*?)\) \(\[pdf\]\((?<pdf_url>.*?)\)\)` 118 | - 结果: `{text} [🔖]({pdf_url})` 119 | 120 | #### 将引用索引转换为论文笔记的文件名 121 | 使用 [bib-cacher](https://github.com/Benature/bib-catcher),我可以通过 Python 连接到 Zotero 数据库,构建一个简单的 Flask 服务器。 122 | 123 | `自定义 API 请求` 的示例命令: 124 | 125 | ```diff 126 | - 一项调查总结认为 Obsidian 是一个好应用 [12]。此外,笔记... 127 | + 一项调查总结认为 Obsidian 是一个好应用([[Reference 文件名]])。此外,笔记... 128 | ``` 129 | 130 | ### 其他 131 | | 命令 | 描述 | 132 | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | 133 | | 自定义**包装器** ⚙️ | 在设置中添加任何任意的包装元素。(https://github.com/Benature/obsidian-underline/issues/5) 有关更多示例,请参见下面 [⬇️](#包装) | 134 | | 自定义 **API** 请求 ⚙️ | 用自定义 API 请求的返回替换选择。将选择发送到带有 `POST` 方法的自定义 API URL。(不会收集用户数据!) *这是我的用例的 [示例](#将引用索引转换为论文笔记的文件名)。 | 135 | | 解码 **URL** | 解码 URL 以便阅读更清晰且 URL 更短 | 136 | | 全文为每段段末添加双空格 | 在每个段落的末尾添加双空格 [#8](https://github.com/Benature/obsidian-text-format/issues/8) | 137 | | 在段落末添加额外换行 | 将 `\n` 替换为 `\n\n` | 138 | | 格式化单词与符号之间的空格 | 保证字母与`(`之间的空格 | 139 | 140 | #### 包装 141 | 例如 142 | - 下划线: 前缀=`<u>`,后缀=`</u>`,然后选择的文本将变为 `<u>text</u>` 143 | - 字体颜色: [#30](https://github.com/Benature/obsidian-text-format/issues/30#issuecomment-1229835540) 144 | 145 | 146 | ## 支持 147 | 148 | 如果您发现此插件有用并想要支持其开发,您可以通过 [Buy Me a Coffee ☕️](https://www.buymeacoffee.com/benature)、微信、支付宝或[爱发电](https://afdian.net/a/Benature-K)支持我,谢谢! 149 | 150 | <p align="center"> 151 | <img src="https://s2.loli.net/2024/04/01/VtX3vYLobdF6MBc.png" width="500px"> 152 | </p> 153 | 154 | 155 | ## 一些示例 156 | 157 | 158 | - lowercase 159 | ```diff 160 | - Hello, I am using Obsidian. 161 | + hello, i am using obsidian. 162 | ``` 163 | - uppercase 164 | ```diff 165 | - Hello, I am using Obsidian. 166 | + HELLO, I AM USING OBSIDIAN. 167 | ``` 168 | - capitalize word 169 | ```diff 170 | - Hello, I am using Obsidian. 171 | + Hello, I Am Using Obsidian. 172 | ``` 173 | - capitalize sentence 174 | ```diff 175 | - hello, I am using Obsidian. 176 | + Hello, I am using Obsidian. 177 | ^ 178 | ``` 179 | - title case 180 | ```diff 181 | - Obsidian is a good app. 182 | + Obsidian Is a Good App. 183 | ^ 184 | ``` 185 | - slugify 186 | ```diff 187 | - Obsidian - a good app. 188 | + obsidian-a-good-app 189 | ``` 190 | - snakify 191 | ```diff 192 | - Obsidian is a good app 193 | + obsidian_is_a_good_app 194 | ``` 195 | - redundant spaces 196 | ```diff 197 | - There are so many redundant blanks 198 | + There are so many redundant blanks 199 | ``` 200 | - merge broken paragraph 201 | ```diff 202 | - This paragraph is broken 203 | - into several lines. I want 204 | - those lines merged! 205 | - 206 | - And this is second paragraph. There is a blank line between 207 | - two paragraph, indicating that they should not be merged into 208 | - one paragraph! 209 | 210 | + This paragraph is broken into several lines. I want those lines merged! 211 | + 212 | + And this is second paragraph. There is a blank line between two paragraph, indicating that they should not be merged into one paragraph! 213 | ``` 214 | - bullet list 215 | ```diff 216 | - • first, blahblah • second, blahblah • third, blahblah 217 | 218 | + - first, blahblah 219 | + - second, blahblah 220 | + - third, blahblah 221 | ``` 222 | - ordered list 223 | ```diff 224 | - a) first, blahblah b) second, blahblah c) third, blahblah 225 | - i) first, blahblah ii) second, blahblah iii) third, blahblah 226 | 227 | + 1. first, blahblah 228 | + 2. second, blahblah 229 | + 3. third, blahblah 230 | ``` 231 | ![demo](https://user-images.githubusercontent.com/35028647/121776728-149ea500-cbc1-11eb-89ee-f4afcb0816ed.gif) 232 | -------------------------------------------------------------------------------- /src/langs/langs.ts: -------------------------------------------------------------------------------- 1 | const EN = { 2 | command: { 3 | "uppercase": "Uppercase", 4 | "lowercase": "Lowercase", 5 | "capitalize-word": "Capitalize all words", 6 | "capitalize-sentence": "Capitalize only first word of sentence", 7 | "title-case": "Title-case", 8 | "cycle-case": "Cycle-case", 9 | "slugify": "Slugify", 10 | "snakify": "Snakify", 11 | "decodeURI": "Decode URL", 12 | "remove-trailing-spaces": "Remove trailing spaces", 13 | "remove-blank-line": "Remove blank line(s)", 14 | "add-line-break": "Add extra line break between paragraphs", 15 | "split-lines-by-blank": "Split line(s) by blanks", 16 | "heading-upper": "Upper heading level (more #)", 17 | "heading-lower": "Lower heading level (less #)", 18 | "open-settings": "Open preference settings tab", 19 | }, 20 | setting: { 21 | "more-details": "More details in Github: ", 22 | "others": "Others", 23 | "remove-spaces-when-converting": "Remove spaces when converting Chinese punctuation marks", 24 | "remove-spaces-when-converting-desc": "for OCR case", 25 | "debug-logging": "Debug logging", 26 | "debug-logging-desc": "verbose logging in the console", 27 | "word-cases": "Word cases", 28 | "word-cases-desc": "lowercase / uppercase / title case / capitalize case / cycle case", 29 | "lowercase-before-capitalize": "Lowercase before capitalize/title case", 30 | "lowercase-before-capitalize-desc": "When running the capitalize or title case command, the plugin will lowercase the selection at first.", 31 | "cycle-case-sequence": "Cycle case sequence (one case per line)", 32 | "cycle-case-sequence-desc": 33 | "Support cases: `lowerCase`, `upperCase`, `capitalizeWord`, `capitalizeSentence`, `titleCase`. \n" + 34 | "Note that the result of `capitalizeWord` and `titleCase` could be the same in some cases, " + 35 | "the two cases are not recommended to be used in the same time.", 36 | "proper-noun": "Proper noun", 37 | "proper-noun-desc": "The words will be ignore to format in title case. Separated by comma, e.g. `USA, UFO`.", 38 | paragraph: { 39 | header: "Merge broken paragraphs behavior", 40 | "remove-redundant-blank-lines": "Remove redundant blank lines", 41 | "remove-redundant-blank-lines-desc": 'change blank lines into single blank lines, e.g. "\\n\\n\\n" will be changed to "\\n\\n"', 42 | }, 43 | "remove-redundant-blank-spaces": "Remove redundant blank spaces", 44 | "remove-redundant-blank-spaces-desc": "ensure only one space between words", 45 | "link-format": "Link format", 46 | "link-format-desc": "Markdown links (`[]()`), Wiki links (`[[ ]]`)", 47 | "Wikilink2mdPathMode-relative-obsidian": "Relative to Obsidian Vault", 48 | "Wikilink2mdPathModerelative-file": "Relative to current file", 49 | "Wikilink2mdPathMode-absolute": "Absolute", 50 | "path-mode": "Path mode when covering wikilinks to plain markdown links.", 51 | "result-format": "The format of result when calling `Remove URL links format in selection`", 52 | "result-format-desc": "Matching with `[{text}]({url})`, use `{text}` if you want to maintain the text, or use `{url}` if you want to maintain the url.", 53 | "remove-wikilink-url": "Remove WikiLink as well when calling `Remove URL links format in selection`", 54 | "wiki-link-format-heading": "WikiLink with heading", 55 | "wiki-link-format-heading-desc": "e.g. [[title#heading]]", 56 | "wiki-link-format-alias": "WikiLink with alias", 57 | "wiki-link-format-alias-desc": "e.g. [[title|alias]]", 58 | "wiki-link-format-both": "WikiLink with both heading and alias", 59 | "wiki-link-format-both-desc": "e.g. [[title#heading|alias]]", 60 | "list-format": "List format", 61 | "list-format-desc": "Detect and convert bullet list / ordered list", 62 | "bullet-point-characters": "Possible bullet point characters", 63 | "bullet-point-characters-desc": "The characters that will be regarded as bullet points.", 64 | "ordered-list-custom-separator": "Format ordered list custom separator RegExp", 65 | "ordered-list-custom-separator-desc": "Separated by `|`. e.g.: `\sand\s|\s?AND\s?`. Default as empty.", 66 | wrapper: { 67 | "header": "Wrapper", 68 | "desc": "Wrap the selection with prefix and suffix", 69 | "rule-desc1": "<Wrapper Name> <Prefix Template> <Suffix Template>", 70 | "rule-desc2": "Template for metadata (file properties) is supported with Handlebars syntax. For example, `{{link}}` will be replaced with the value of current file's property `link`.", 71 | "add-new-wrapper": "Add new wrapper", 72 | "new-wrapper-rule-tooltip": "Add new rule", 73 | "name-placeholder": "Wrapper Name (command name)", 74 | "prefix-placeholder": "Prefix", 75 | "suffix-placeholder": "Suffix", 76 | }, 77 | "delete-tooltip": "Delete", 78 | "api-request": "API Request", 79 | "api-request-desc": "Send a request to an API and replace the selection with the return", 80 | "api-request-url": "API Request URL", 81 | "api-request-url-desc": 82 | "The URL that plugin will send a POST and replace with return.\n" + 83 | "The return json should have two attribution: `text` and `notification`.\n" + 84 | "If `text` exist then `text` will replace the selection, or do nothing.\n" + 85 | "If `notification` exist then Send a notice if this string, or do nothing.", 86 | "new-request-tooltip": "Add new request", 87 | "request-name-placeholder": "Request Name (command name)", 88 | "request-url-placeholder": "Request URL", 89 | "custom-replacement": "Custom replacement", 90 | "custom-replacement-desc": "Replace specific pattern with custom string", 91 | "add-custom-replacement": "Add custom replacement", 92 | "add-custom-replacement-desc": "The plugin will replace the `search` string with the `replace` string in the selection. RegExp is supported.", 93 | "add-new-replacement-tooltip": "Add new replacement", 94 | "replacement-command-name-placeholder":"Command name", 95 | "replacement-search-placeholder": "Search", 96 | "replacement-replace-placeholder": "Replace (empty is fine)", 97 | "zotero-pdf-note-format": "Zotero pdf note format", 98 | "zotero-input-regexp": "Zotero pdf note (input) RegExp", 99 | "zotero-output-format": "Zotero note pasted in Obsidian (output) format", 100 | "zotero-output-format-desc": 101 | "Variables: \n" + 102 | "{text}: <text>,\n" + 103 | "{pdf_url}: <pdf_url>,\n" + 104 | "{item}: <item>.", 105 | "markdown-quicker": "Markdown quicker", 106 | "markdown-quicker-desc": "Quickly format the selection with common markdown syntax", 107 | "heading-lower-to-plain": "Heading lower to plain text", 108 | "heading-lower-to-plain-desc": "If disabled, heading level 1 cannot be lowered to plain text.", 109 | "method-decide-callout-type": "Method to decide callout type", 110 | "method-decide-callout-type-desc": "How to decide the type of new callout block for command `Callout format`? `Fix callout type` use the default callout type always, other methods only use the default type when it fails to find previous callout block.", 111 | "default-callout-type": "Default callout type", 112 | "default-callout-type-desc": "Set the default callout type for command `Callout format`. ", 113 | "format-on-paste": { 114 | "name": "Format on paste", 115 | "desc": "Format the pasted content automatically. One command per line.", 116 | } 117 | } 118 | } 119 | 120 | const ZH = { 121 | command: { 122 | "uppercase": "全部大写", 123 | "lowercase": "全部小写", 124 | "capitalize-word": "首字母大写(所有单词)", 125 | "capitalize-sentence": "首字母大写(句首单词)", 126 | "title-case": "标题格式大小写", 127 | "cycle-case": "循环切换大小写格式", 128 | "slugify": "使用 Slugify 格式化(`-`连字符)", 129 | "snakify": "使用 Snakify 格式化(`_`连字符)", 130 | "remove-trailing-spaces": "移除所有行末空格", 131 | "remove-blank-line": "移除空行", 132 | "add-line-break": "在段落间添加额外换行", 133 | "split-lines-by-blank": "按空格分行", 134 | "heading-upper": "降级标题(加 #)", 135 | "heading-lower": "升级标题(减 #)", 136 | "open-settings": "打开插件设置选项卡", 137 | "decodeURI": "解码 URL", 138 | }, 139 | setting: { 140 | "more-details": "在 Github 查看更多详情:", 141 | "others": "其他设置", 142 | "remove-spaces-when-converting": "转换中文标点时去除空格", 143 | "remove-spaces-when-converting-desc": "适用于 OCR 场景", 144 | "debug-logging": "Debug 日志", 145 | "debug-logging-desc": "在控制台中显示 Debug 详细日志", 146 | "word-cases": "文字大小写转换", 147 | "word-cases-desc": "转换为小写 / 转换为大写 / 标题式大小写 / 单词首字母大写 / 大小写循环切换", 148 | "lowercase-before-capitalize": "在首字母大写之前转换为小写", 149 | "lowercase-before-capitalize-desc": "执行首字母大写或标题式大小写命令前,先将选中文本转换为小写。", 150 | "cycle-case-sequence": "大小写循环变换(单行)", 151 | "cycle-case-sequence-desc": 152 | "支持以下大小写格式:`lowerCase`、`upperCase`、`capitalizeWord`、`capitalizeSentence`、`titleCase`。\n" + 153 | "注意,在某些情况下,`capitalizeWord`与`titleCase`的效果可能相同," + 154 | "不推荐同时使用。", 155 | "proper-noun": "专有名词例外", 156 | "proper-noun-desc": "在执行标题式大小写时,会忽略以下指定的专有名词。例如:`USA, UFO`。", 157 | paragraph: { 158 | header: "合并段落", 159 | "remove-redundant-blank-lines": "删除多余的空白行", 160 | "remove-redundant-blank-lines-desc": "将多余的空白行转换为单一空白行,例如:`\\n\\n\\n`会被转换为单一的`\\n\\n`。", 161 | }, 162 | "remove-redundant-blank-spaces": "删除多余的空格", 163 | "remove-redundant-blank-spaces-desc": "确保单词之间只有一个空格。", 164 | "link-format": "链接格式化", 165 | "link-format-desc": "Markdown 链接 (`[]()`),Wiki 链接 (`[[ ]]`)", 166 | "Wikilink2mdPathMode-relative-obsidian": "相对于Obsidian库", 167 | "Wikilink2mdPathMode-relative-file": "相对于当前文件", 168 | "Wikilink2mdPathMode-absolute": "绝对路径", 169 | "path-mode": "转换 Wikilink 为 Markdown 链接时的路径模式", 170 | "result-format": "移除选中链接格式的结果", 171 | "result-format-desc": "与 `[{text}]({url})` 匹配,使用 `{text}` 维持文本或 `{url}` 维持链接。", 172 | "remove-wikilink-url": "移除 WikiLink 时也移除 URL", 173 | "wiki-link-format-heading": "带标题的 WikiLink 格式化", 174 | "wiki-link-format-heading-desc": "如:[[title#heading]]", 175 | "wiki-link-format-alias": "带别名的 WikiLink 格式化", 176 | "wiki-link-format-alias-desc": "如:[[title|alias]]", 177 | "wiki-link-format-both": "同时带标题和别名的 WikiLink 格式化", 178 | "wiki-link-format-both-desc": "如:[[title#heading|alias]]", 179 | "list-format": "列表格式化", 180 | "list-format-desc": "检测并转换无序列表和有序列表。", 181 | "bullet-point-characters": "项目符号字符", 182 | "bullet-point-characters-desc": "被视为项目符号的字符。", 183 | "ordered-list-custom-separator": "有序列表自定义分隔符正则表达式", 184 | "ordered-list-custom-separator-desc": "使用`|`分隔,例如:`\sand\s|\s?AND\s?`。默认为空。", 185 | wrapper: { 186 | "header": "包装器", 187 | "desc": "在选中的文本前后添加前缀和后缀。", 188 | "rule-desc1": "包装器名称、前缀模板、后缀模板", 189 | "rule-desc2": "支持使用 Handlebars 语法的文件元数据属性模板。例如,`{{link}}` 将替换为当前文件的 `link` 属性值。", 190 | "add-new-wrapper": "添加新的包装器", 191 | "new-wrapper-rule-tooltip": "添加新规则", 192 | "name-placeholder": "包装器名称(命令名)", 193 | "prefix-placeholder": "前缀", 194 | "suffix-placeholder": "后缀", 195 | }, 196 | "delete-tooltip": "删除", 197 | "api-request": "API 请求", 198 | "api-request-desc": "向 API 发送请求,并用返回值替换选择文本", 199 | "api-request-url": "API 请求 URL", 200 | "api-request-url-desc": 201 | "插件将发送POST请求并用返回值替换选择文本。\n" + 202 | "返回的JSON应包含两个属性:`text` 和 `notification`。\n" + 203 | "如果存在 `text`,则用 `text` 替换选择文本,否则不做任何操作。\n" + 204 | "如果存在 `notification`,则发送此字符串作为通知,否则不做任何操作。", 205 | "new-request-tooltip": "添加新请求", 206 | "request-name-placeholder": "请求名称(命令名称)", 207 | "request-url-placeholder": "请求 URL", 208 | "custom-replacement": "自定义替换", 209 | "custom-replacement-desc": "使用自定义字符串替换特定模式", 210 | "add-custom-replacement": "添加自定义替换", 211 | "add-custom-replacement-desc": "插件将使用 `replace` 字符串替换 `search` 字符串。支持正则表达式。", 212 | "add-new-replacement-tooltip": "添加新替换", 213 | "replacement-command-name-placeholder": "命令名称", 214 | "replacement-search-placeholder": "搜索", 215 | "replacement-replace-placeholder": "替换(可为空)", 216 | "zotero-pdf-note-format": "Zotero PDF 注释格式", 217 | "zotero-input-regexp": "Zotero PDF 注释(输入)正则表达式", 218 | "zotero-output-format": "Zotero 注释粘贴到 Obsidian 中的(输出)格式", 219 | "zotero-output-format-desc": 220 | "变量: \n" + 221 | "{text}: <文本>,\n" + 222 | "{pdf_url}: <PDF链接>,\n" + 223 | "{item}: <条目>。", 224 | "markdown-quicker": "Markdown 快速格式化", 225 | "markdown-quicker-desc": "使用常见 Markdown 语法快速格式化选中文本", 226 | "heading-lower-to-plain": "标题降级为普通文本", 227 | "heading-lower-to-plain-desc": "如果禁用,一级标题不能降为普通文本。", 228 | "method-decide-callout-type": "决定标注类型的方法", 229 | "method-decide-callout-type-desc": "选择用于命令 `Callout format` 的新标注块的类型的方法。如果选择固定标注类型,则总是使用默认的标注类型。在无法找到前一个标注块时,其它方法也将使用默认类型。", 230 | "default-callout-type": "默认callout类型", 231 | "default-callout-type-desc": "设置命令 `Callout format` 的默认标注类型。", 232 | "format-on-paste": { 233 | "name": "粘贴自动格式化", 234 | "desc": "自动格式化粘贴内容。每行一条命令。", 235 | } 236 | } 237 | } 238 | 239 | interface Languages { 240 | [lang: string]: any 241 | } 242 | 243 | const languages: Languages = { 244 | en: EN, 245 | zh: ZH 246 | }; 247 | 248 | function setLanguage(lang: string): string { 249 | let currentLanguage = 'en'; 250 | if (lang === "zh-TW") { 251 | return "zh"; 252 | } 253 | if (lang in languages) { 254 | currentLanguage = lang; 255 | } 256 | return currentLanguage 257 | } 258 | 259 | // 获取多级内容的字符串 260 | export function getString(keys: string[], useDefault: boolean = false): string { 261 | let currentLanguage = "en"; 262 | if (!useDefault) { 263 | currentLanguage = setLanguage(window.localStorage.getItem("language")); 264 | } 265 | let obj = languages[currentLanguage]; 266 | let fail = false; 267 | for (let key of keys) { 268 | if (!(key in obj)) { 269 | fail = true; 270 | break 271 | } 272 | obj = obj[key]; 273 | } 274 | if (fail) { 275 | return getString(keys, true); 276 | } else { 277 | return obj; 278 | } 279 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Text Format 2 | 3 | <div align="center"> 4 | 5 | ![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22obsidian-text-format%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) ![GitHub stars](https://img.shields.io/github/stars/Benature/obsidian-text-format?style=flat) ![latest download](https://img.shields.io/github/downloads/Benature/obsidian-text-format/latest/total?style=plastic) 6 | [![Github release](https://img.shields.io/github/manifest-json/v/Benature/obsidian-text-format?color=blue)](https://github.com/Benature/obsidian-text-format/releases/latest) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/Benature/obsidian-text-format?include_prereleases&label=BRAT%20beta) 7 | 8 | [ [中文](https://github.com/Benature/obsidian-text-format/blob/master/README_ZH.md) | English ] 9 | 10 | </div> 11 | 12 | 13 | >When I'm taking notes, sometimes I encounter some issues like 14 | >1. I copied some text from pdf or some other source, but the copied content is out of format. For example, there are more than one space between words or one paragraph brokes into several lines. 15 | >2. Lowercase letters are required while they are all uppercase, etc. 16 | >3. blahblahblah... 17 | 18 | This plugin is created to format selected text lowercase/uppercase/capitalize/titlecase or remove redundant spaces/newline characters, and other features listed below. 19 | 20 | [Click to install this plugin right now](https://obsidian.md/plugins?id=obsidian-text-format) 21 | 22 | ## Features 23 | 24 | Experimental features: 25 | - Format on paste: see [#86](https://github.com/Benature/obsidian-text-format/issues/86) 26 | 27 | ### Commands 28 | 29 | Press <kbd>cmd/ctrl+P</kbd> to enter the command. 👇 30 | 31 | Or you can consider to bind custom hotkeys to those commands according to [#29](https://github.com/Benature/obsidian-text-format/issues/29#issuecomment-1279246640). 32 | 33 | --- 34 | 35 | - [Text Format](#text-format) 36 | - [Features](#features) 37 | - [Commands](#commands) 38 | - [Basic](#basic) 39 | - [Markdown Grammar](#markdown-grammar) 40 | - [List](#list) 41 | - [Links](#links) 42 | - [Copy / OCR issues](#copy--ocr-issues) 43 | - [Academic / Study](#academic--study) 44 | - [Advanced custom](#advanced-custom) 45 | - [Others](#others) 46 | - [Support](#support) 47 | - [Some Examples](#some-examples) 48 | - [Zotero format](#zotero-format) 49 | - [Replacements](#replacements) 50 | - [Wrapper](#wrapper) 51 | - [Convert citation index to the file name of paper note](#convert-citation-index-to-the-file-name-of-paper-note) 52 | - [Basic](#basic-1) 53 | 54 | 55 | ⚙️: There is setting of this command. 56 | 57 | #### Basic 58 | 59 | | Command | Description | 60 | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 61 | | **Lowercase** | Lowercase all letters in selection or the whole title (when cursor focus inline tile) | 62 | | **Uppercase** | Uppercase all letters in selection or the whole title (when cursor focus inline tile) | 63 | | **Capitalize** all **words** ⚙️ | Capitalize all words in selection or the whole title (when cursor focus inline tile) | 64 | | **Capitalize** only first word of **sentence** ⚙️ | Capitalize only first word of sentence(s) in selection or the whole title (when cursor focus inline tile) | 65 | | **Title case** ⚙️ | Capitalize words but leave certain words in lower case in selection or the whole title (when cursor focus inline tile) [#1](https://github.com/Benature/obsidian-text-format/issues/1) | 66 | | **Cycle case** ⚙️ | A custom loop to format the selection or the whole title (when cursor focus inline tile) | 67 | | **Slugify** selected text | convert any input text into a URL-friendly slug | 68 | | **Snakify** selected text | Lowercase all letters in selection *and* replace spaces with underscores | 69 | 70 | #### Markdown Grammar 71 | 72 | | Command | Description | 73 | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | 74 | | Heading upper | e.g.: `# Heading` -> `## Heading` (default shortcut: `Ctrl+Shift+]`) | 75 | | Heading lower | e.g.: `## Heading` -> `# Heading` (default shortcut: `Ctrl+Shift+[`) [[discussions](https://github.com/Benature/obsidian-text-format/issues/83)] | 76 | | Callout format | [#80](https://github.com/Benature/obsidian-text-format/issues/80) | 77 | 78 | 79 | ##### List 80 | | Command | Description | 81 | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 82 | | Detect and format **bullet** list ⚙️ | Change `•` into bullet list, i.e. `- `; split every bullet point into single line; and remove blank lines. ([test cases](https://github.com/Benature/obsidian-text-format/issues/66)) | 83 | | Detect and format **ordered** list | Change `*)`(star could be any letter) into ordered list (e.g. `1. `, `2. `); split every ordered point into single line; and remove blank lines. (#4) | 84 | | Convert table to bullet list | The first volume is 1st list, other volumes are sub-list | 85 | | Convert table to bullet list with header | Sub-list begins with `${header}: ` | 86 | | **Sort to-do** list | [#37](https://github.com/Benature/obsidian-text-format/issues/37), [#46](https://github.com/Benature/obsidian-text-format/issues/46) | 87 | 88 | ##### Links 89 | 90 | | Command | Description | 91 | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | 92 | | Remove WikiLinks format in selection | e.g. Convert `[[WikiLinks]]` to `WikiLinks` ([#28](https://github.com/Benature/obsidian-text-format/issues/28)) | 93 | | Remove URL links format in selection | e.g. Convert `[Google](www.google.com)` to `Google` | 94 | | Convert URL links to WikiLinks in selection | e.g. Convert `[Google](www.google.com)` to `[[Google]]` | 95 | | Convert WikiLinks to plain markdown links in selection ⚙️ | e.g. Convert `[[Google]]` to `[Google](Google.md)` ([#40](https://github.com/Benature/obsidian-text-format/issues/40)) | 96 | 97 | #### Copy / OCR issues 98 | 99 | | Command | Description | 100 | | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 101 | | Remove **redundant spaces** in selection | Ensure only one space between words | 102 | | Remove **all spaces** in selection | Remove all spaces | 103 | | Remove **trailing spaces** in selection | Remove trailing spaces ([#61](https://github.com/Benature/obsidian-text-format/issues/61)) | 104 | | Remove **blank line(s)** | replace `\n\n` with `\n` | 105 | | Merge **broken paragraph(s)** in selection ⚙️ | Change selected lines into single-line, except lines are separated by blank line(s). *At the same time, blank lines will be merged into one blank line(optional, default enable), and redundant spaces will be removed(optional, default enable).* | 106 | | **Split** line(s) by **blanks** | Replace ` ` with `\n` for OCR use case. | 107 | | Convert to **Chinese punctuation** marks (,;:!?) ⚙️ | For OCR use case. (For who require more custom setting, I would recommend <https://p.gantrol.com/>) | 108 | | Convert to **English punctuation** marks | Similar to `Convert to Chinese punctuation marks (,;:!?)` | 109 | | Remove **hyphens** | Remove hyphens (like when pasting text from pdf) [#15](https://github.com/Benature/obsidian-text-format/issues/15) | 110 | | Replace **ligature** | Replace [ligature](https://en.wikipedia.org/wiki/Ligature_(writing)) to Non-ligature [#24](https://github.com/Benature/obsidian-text-format/issues/24) | 111 | 112 | 113 | #### Academic / Study 114 | 115 | | Command | Description | 116 | | ------------------------------------------------------ | ---------------------------------------------------------------------------- | 117 | | Convert selection into **Anki** card format | [#32](https://github.com/Benature/obsidian-text-format/pull/32) | 118 | | Remove citation index | e.g., `A research [12] about notes` => `A research about notes` | 119 | | Get **Zotero** note from clipboard and paste ⚙️ | See [below ⬇️](#zotero-format) | 120 | | Detect and convert characters to **math mode** (LaTeX) | e.g. convert `P` into `$P$` (latex), apply for all single letter except `a`. | 121 | | Convert **Mathpix** array to markdown table | Convert latex array generated by Mathpix to markdown table format | 122 | 123 | 124 | 125 | #### Advanced custom 126 | 127 | | Command | Description | 128 | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 129 | | Custom **Replace** ⚙️ | Custom replace `<search>` to `<replace>`. See below for more examples [⬇️](#replacements) | 130 | | Custom **Wrapper** ⚙️ | Add any arbitrary wrapping element in Setting. (https://github.com/Benature/obsidian-underline/issues/5) See below for more examples [⬇️](#wrapper) | 131 | | Custom **API** Request ⚙️ | Replace Selection with the return of custom API request. The selection will be sent to custom API URL with `POST` method. (No user data is collected!) *There is an [example](#convert-citation-index-to-the-file-name-of-paper-note) of my use case.* | 132 | 133 | 134 | 135 | #### Others 136 | | Command | Description | 137 | | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | 138 | | Decode **URL** | Decode URL for better reading and shorter url. | 139 | | Add extra double spaces per paragraph for whole file | Add double spaces at the end of every paragraph [#8](https://github.com/Benature/obsidian-text-format/issues/8) | 140 | | Add extra line break to paragraph | replace `\n` with `\n\n` | 141 | | Format space between word and symbol | add space between words and `(` | 142 | 143 | 144 | ## Support 145 | 146 | If you find this plugin useful and would like to support its development, you can sponsor me via [Buy Me a Coffee ☕️](https://www.buymeacoffee.com/benature), WeChat, Alipay or [AiFaDian](https://afdian.net/a/Benature-K). Any amount is welcome, thank you! 147 | 148 | <p align="center"> 149 | <img src="https://s2.loli.net/2024/04/01/VtX3vYLobdF6MBc.png" width="500px"> 150 | </p> 151 | 152 | 153 | ## Some Examples 154 | 155 | ### Zotero format 156 | The format template can refer to https://www.zotero.org/support/note_templates 157 | - default 158 | - zotero: `<p>{{highlight quotes='true'}} {{citation}} {{comment}}</p>` 159 | - plugin config: `“(?<text>.*)” \((?<item>.*?)\) \(\[pdf\]\((?<pdf_url>.*?)\)\)` 160 | - result: `{text} [🔖]({pdf_url})` 161 | 162 | 163 | ### Replacements 164 | [use cases](https://github.com/Benature/obsidian-text-format/milestone/1?closed=1): 165 | - Split paragraph into sentences: [#78](https://github.com/Benature/obsidian-text-format/issues/78#issuecomment-1999198698) 166 | 167 | ### Wrapper 168 | [use cases](https://github.com/Benature/obsidian-text-format/milestone/2?closed=1): 169 | - Underline: prefix=`<u>`, suffix=`</u>`, then selected text will turn into `<u>text</u>` 170 | - Font color: [#30](https://github.com/Benature/obsidian-text-format/issues/30#issuecomment-1229835540) 171 | 172 | ### Convert citation index to the file name of paper note 173 | With [bib-cacher](https://github.com/Benature/bib-catcher), I can connect to Zotero database by python, building a simple Flask server. 174 | 175 | Example of command `Custom API Request`: 176 | 177 | ```diff 178 | - A survey concludes that obsidian is a good app [12]. Furthermore, The note taking... 179 | + A survey concludes that obsidian is a good app ([[File Name of the Reference]]). Furthermore, The note taking... 180 | ``` 181 | 182 | ### Basic 183 | 184 | ![demo](https://user-images.githubusercontent.com/35028647/121776728-149ea500-cbc1-11eb-89ee-f4afcb0816ed.gif) 185 | 186 | - lowercase 187 | ```diff 188 | - Hello, I am using Obsidian. 189 | + hello, i am using obsidian. 190 | ``` 191 | - uppercase 192 | ```diff 193 | - Hello, I am using Obsidian. 194 | + HELLO, I AM USING OBSIDIAN. 195 | ``` 196 | - capitalize word 197 | ```diff 198 | - Hello, I am using Obsidian. 199 | + Hello, I Am Using Obsidian. 200 | ``` 201 | - capitalize sentence 202 | ```diff 203 | - hello, I am using Obsidian. 204 | + Hello, I am using Obsidian. 205 | ^ 206 | ``` 207 | - title case 208 | ```diff 209 | - Obsidian is a good app. 210 | + Obsidian Is a Good App. 211 | ^ 212 | ``` 213 | - slugify 214 | ```diff 215 | - Obsidian - a good app. 216 | + obsidian-a-good-app 217 | ``` 218 | - snakify 219 | ```diff 220 | - Obsidian is a good app 221 | + obsidian_is_a_good_app 222 | ``` 223 | - redundant spaces 224 | ```diff 225 | - There are so many redundant blanks 226 | + There are so many redundant blanks 227 | ``` 228 | - merge broken paragraph 229 | ```diff 230 | - This paragraph is broken 231 | - into several lines. I want 232 | - those lines merged! 233 | - 234 | - And this is second paragraph. There is a blank line between 235 | - two paragraph, indicating that they should not be merged into 236 | - one paragraph! 237 | 238 | + This paragraph is broken into several lines. I want those lines merged! 239 | + 240 | + And this is second paragraph. There is a blank line between two paragraph, indicating that they should not be merged into one paragraph! 241 | ``` 242 | - bullet list 243 | ```diff 244 | - • first, blahblah • second, blahblah • third, blahblah 245 | 246 | + - first, blahblah 247 | + - second, blahblah 248 | + - third, blahblah 249 | ``` 250 | - ordered list 251 | ```diff 252 | - a) first, blahblah b) second, blahblah c) third, blahblah 253 | - i) first, blahblah ii) second, blahblah iii) third, blahblah 254 | 255 | + 1. first, blahblah 256 | + 2. second, blahblah 257 | + 3. third, blahblah 258 | ``` 259 | 260 | -------------------------------------------------------------------------------- /src/format.ts: -------------------------------------------------------------------------------- 1 | import { Editor, MarkdownView, EditorPosition, App, requestUrl, TFile, Notice, EditorRangeOrCaret, EditorChange, EditorSelection, EditorSelectionOrCaret } from "obsidian"; 2 | import { FormatSettings, customReplaceSetting } from "./settings/types"; 3 | import { compile as compileTemplate, TemplateDelegate as Template } from 'handlebars'; 4 | 5 | import { Ligatures, GreekLetters } from "./presets"; 6 | 7 | export function stringFormat(str: string, values: Record<string, string>) { 8 | return str.replace(/\{(\w+)\}/g, (match, key) => values[key] === undefined ? match : values[key]); 9 | } 10 | 11 | const LC = "[\\w\\u0400-\\u04FFåäöÅÄÖ]"; // Latin and Cyrillic and Swedish characters 12 | 13 | export function capitalizeWord(str: string): string { 14 | var rx = new RegExp(LC + "\\S*", "g"); 15 | return str.replace(rx, function (t) { 16 | return t.charAt(0).toUpperCase() + t.substr(1); 17 | }); 18 | } 19 | 20 | export function capitalizeSentence(s: string): string { 21 | let lcp = "(" + LC + "+)"; // LC plus 22 | var rx = new RegExp( 23 | String.raw`(?:^|[\n"“]|[\.\!\?\~#]\s+|\s*- \s*)` + lcp, 24 | "g" 25 | ); 26 | return s.replace(rx, function (t0, t) { 27 | if (/^(ve|t|m|d|ll|s|re)$/.test(t)) { 28 | return t0; 29 | } else { 30 | return t0.replace(t, t.charAt(0).toUpperCase() + t.substr(1)); 31 | } 32 | }); 33 | } 34 | 35 | export function headingLevel(s: string, upper: boolean = true, minLevel: number, isMultiLine?: boolean): { text: string, offset: number } { 36 | let ignorePlain = minLevel > 0; 37 | let offset = 0; 38 | if (upper) { 39 | let prefix = `#`; 40 | if (!/^#+\s/.test(s)) { // plain text (not a heading) 41 | if (isMultiLine) { return { text: s, offset: offset }; } 42 | prefix = `# `; 43 | } 44 | s = prefix + s; 45 | offset = prefix.length; 46 | } else { //: LOWER 47 | if (/^# /.test(s)) { //: heading level 1 48 | if (ignorePlain) { 49 | console.log("ignore plain text") 50 | return { text: s, offset: offset }; 51 | } 52 | s = s.slice(2); 53 | offset = -2; 54 | } else if (/^#+ /.test(s)) { 55 | s = s.slice(1); 56 | offset = -1; 57 | } 58 | } 59 | return { text: s, offset: offset }; 60 | } 61 | 62 | 63 | export function ankiSelection(str: string): string { 64 | let sections = str.split(/\r?\n/); 65 | var seclen = sections.length; 66 | let returned = ""; 67 | 68 | if (sections[0] == "") { 69 | sections.shift(); 70 | returned += "\n"; 71 | } 72 | 73 | if (seclen > 1) { 74 | returned += "START\nCloze\n"; 75 | let i = 1; 76 | let gap = 0; 77 | sections.forEach(function (entry) { 78 | if (entry != "" && gap > 0) { 79 | returned += "\nBack Extra:\nTags:\nEND\n"; 80 | for (let n = 0; n < gap; n++) { 81 | returned += "\n"; 82 | } 83 | returned += "START\nCloze\n"; 84 | gap = 0; 85 | i = 1; 86 | } 87 | 88 | if (entry != "") { 89 | returned += "{{c" + i + "::" + entry + "}} "; 90 | i++; 91 | } else { 92 | gap++; 93 | } 94 | }); 95 | returned += "\nBack Extra:\nTags:\nEND"; 96 | for (let n = 0; n < gap; n++) { 97 | returned += "\n"; 98 | } 99 | return returned; 100 | } else { 101 | return str; 102 | } 103 | } 104 | 105 | export function removeAllSpaces(s: string): string { 106 | return s.replace(/(?:[^\)\]\:#\-]) +| +$/g, (t) => t.replace(/ +/g, "")); 107 | } 108 | 109 | export function zoteroNote( 110 | text: string, 111 | regexp: string, 112 | template: string 113 | ): string { 114 | let template_regexp = new RegExp(regexp); 115 | let result = template_regexp.exec(text); 116 | 117 | if (result) { 118 | let z = result.groups; 119 | let text = result.groups.text.replace(/\\\[\d+\\\]/g, (t) => 120 | t.replace("\\[", "[").replace("\\]", "]") 121 | ); 122 | // console.log(template); 123 | // @ts-ignore 124 | return template.format({ 125 | text: text, 126 | item: z.item, 127 | pdf_url: z.pdf_url, 128 | }); 129 | } else { 130 | return text; 131 | } 132 | } 133 | 134 | export function table2bullet(content: string, withHeader: boolean = false): string { 135 | let header_str = ""; 136 | let output = ""; 137 | // remove header from `content` but record the header string 138 | content = content.replace(/[\S\s]+\n[:\-\| ]+\|\n/g, (t) => { 139 | header_str = t 140 | // .match(/^[\S ]+/)[0] 141 | .replace(/ *\| *$|^ *\| */g, "") 142 | .replace(/ *\| */g, "|"); 143 | return ""; 144 | }); 145 | let headers = header_str.split("|"); 146 | for (let i = 0; i < headers.length; i++) { 147 | headers[i] = withHeader ? `${headers[i]}: ` : ""; 148 | } 149 | content.split("\n").forEach((line) => { 150 | if (line.trim().startsWith('|')) { 151 | let items = line.replace(/\| *$|^ *\|/g, "").split("|"); 152 | output += `- ${items[0].trim()}\n`; 153 | for (let i = 1; i < items.length; i++) { 154 | output += ` - ${headers[i]}${items[i].trim()}\n`; 155 | } 156 | } else { 157 | output += line + "\n" 158 | } 159 | }); 160 | 161 | return output; 162 | } 163 | 164 | export function array2markdown(content: string): string { 165 | let volume = content.match(/\{([clr\|]+)\}/)[1].match(/[clr]/g).length; 166 | 167 | // remove `\test{}` 168 | content = content 169 | .replace(/\$|\n/g, ``) 170 | .replace(/\\text *\{.*?\}/g, (t) => { 171 | return t.match(/\{((.*?))\}/g)[0].replace(/^ +| +$|[\{\}]/g, ``) 172 | } 173 | ); 174 | // return content 175 | 176 | // convert array to single line 177 | content = content.replace( 178 | /\\begin\{array\}\{[clr]\}.*?\\end\{array\}/g, 179 | (t) => { 180 | // console.log(t) 181 | return t 182 | .replace(/\\{1,2}begin\{array\}\{[clr]\}/g, "") 183 | .replace("\\end{array}", "") 184 | .replace(/\\\\ */g, "") 185 | } 186 | ); 187 | 188 | // add `\n` 189 | content = content.replace(/\\\\ ?\\hline|\\\\ */g, (t) => t + `\n`); 190 | 191 | // convert to table 192 | let markdown = ( 193 | "|" + 194 | content 195 | .replace(/\\begin\{array\}\{[clr\|]+\}|\\end\{array\}|\\hline/g, "") 196 | .replace(/&/g, "|") 197 | .replace(/\n[ ]*$/, "") 198 | .replace(/\\\\[ ]*?\n/g, "|\n|") 199 | .replace("\\\\", "|") 200 | ).replace("\n", "\n" + "|:-:".repeat(volume) + "|\n"); 201 | 202 | let beautify_markdown = markdown 203 | .replace(/\[[\d,]+?\]/g, "") 204 | .replace(/\\[\w\{\}\d]+/g, (t) => `$${t}$`); 205 | 206 | return beautify_markdown; 207 | } 208 | 209 | export function toTitleCase(text: string, settings: FormatSettings | null = null): string { 210 | // reference: https://github.com/gouch/to-title-case 211 | var properNouns = RegExp(`^(` + settings?.ProperNoun.split(",").map((w) => w.trim()).join("|") + `)$`); 212 | var smallWords = 213 | /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|v.?|vs.?|via)$/i; 214 | var alphanumericPattern = /([A-Za-z0-9\u00C0-\u00FF])/; 215 | var wordSeparators = /([\s\:\–\—\-\(\)])/; 216 | 217 | return text.split(wordSeparators) 218 | .map(function (current: string, index: number, array: string[]): string { 219 | 220 | if (settings && current.search(properNouns) > -1) { /* Check for proper nouns */ 221 | return current; 222 | } else { 223 | if (settings && settings.LowercaseFirst) { 224 | current = current.toLowerCase(); 225 | } 226 | } 227 | 228 | if (/* Check for small words */ 229 | current.search(smallWords) > -1 && 230 | /* Skip first and last word */ 231 | index !== 0 && 232 | index !== array.length - 1 && 233 | /* Ignore title end and subtitle start */ 234 | array[index - 3] !== ":" && 235 | array[index + 1] !== ":" && 236 | /* Ignore small words that start a hyphenated phrase */ 237 | (array[index + 1] !== "-" || 238 | (array[index - 1] === "-" && array[index + 1] === "-")) 239 | ) { 240 | return current.toLowerCase(); 241 | } 242 | 243 | /* Ignore intentional capitalization */ 244 | if (current.substr(1).search(/[A-Z]|\../) > -1) { 245 | return current; 246 | } 247 | 248 | /* Ignore URLs */ 249 | if (array[index + 1] === ":" && array[index + 2] !== "") { 250 | return current; 251 | } 252 | 253 | /* Capitalize the first letter */ 254 | return current.replace(alphanumericPattern, function (match) { 255 | return match.toUpperCase(); 256 | }); 257 | }) 258 | .join(""); 259 | }; 260 | 261 | String.prototype.format = function (args: any) { 262 | var result = this; 263 | if (arguments.length > 0) { 264 | if (arguments.length == 1 && typeof args == "object") { 265 | for (var key in args) { 266 | if (args[key] != undefined) { 267 | var reg = new RegExp("({" + key + "})", "g"); 268 | result = result.replace(reg, args[key]); 269 | } 270 | } 271 | } else { 272 | for (var i = 0; i < arguments.length; i++) { 273 | if (arguments[i] != undefined) { 274 | var reg = new RegExp("({)" + i + "(})", "g"); 275 | result = result.replace(reg, arguments[i]); 276 | } 277 | } 278 | } 279 | } 280 | return result; 281 | }; 282 | 283 | export function textWrapper(selectedText: string, context: any): { editorChange: EditorChange, selectedText: string, resetSelectionOffset: { anchor: number, head: number } } { 284 | const editor: Editor = context.editor; 285 | const prefix_setting: string = context.prefix; 286 | const suffix_setting: string = context.suffix; 287 | const adjustRange: EditorRangeOrCaret = context.adjustRange; 288 | // let resetSelection; 289 | let resetSelectionOffset; 290 | let editorChange: EditorChange; 291 | 292 | let meta: Record<string, any> = {}; 293 | const metaProperties = context.view.metadataEditor?.properties; 294 | if (metaProperties) { 295 | for (const m of metaProperties) { meta[m.key] = m.value; } 296 | } 297 | 298 | let prefix_template = compileTemplate(prefix_setting.replace(/\\n/g, "\n"), { noEscape: true }) 299 | let suffix_template = compileTemplate(suffix_setting.replace(/\\n/g, "\n"), { noEscape: true }) 300 | 301 | const prefix = prefix_template(meta); 302 | const suffix = suffix_template(meta); 303 | const PL = prefix.length; // Prefix Length 304 | const SL = suffix.length; // Suffix Length 305 | 306 | 307 | function Cursor(offset: number): EditorPosition { 308 | const last_cursor = { line: editor.lastLine(), ch: editor.getLine(editor.lastLine()).length } 309 | const last_offset = editor.posToOffset(last_cursor); 310 | if (offset > last_offset) { 311 | return last_cursor; 312 | } 313 | offset = offset < 0 ? 0 : offset; 314 | return editor.offsetToPos(offset); 315 | } 316 | 317 | const fos = editor.posToOffset(adjustRange.from); // from offset 318 | const tos = editor.posToOffset(adjustRange.to); // to offset 319 | const len = selectedText.length; 320 | 321 | const outPrefix = editor.getRange(Cursor(fos - PL), Cursor(tos - len)); 322 | const outSuffix = editor.getRange(Cursor(fos + len), Cursor(tos + SL)); 323 | const inPrefix = editor.getRange(Cursor(fos), Cursor(fos + PL)); 324 | const inSuffix = editor.getRange(Cursor(tos - SL), Cursor(tos)); 325 | 326 | if (outPrefix === prefix && outSuffix === suffix) { 327 | //: selection outside match prefix and suffix => undo underline (inside selection) 328 | editorChange = { text: selectedText, from: Cursor(fos - PL), to: Cursor(tos + SL) }; 329 | // resetSelection = { anchor: Cursor(fos - PL), head: Cursor(tos - PL) }; 330 | resetSelectionOffset = { anchor: fos - PL, head: tos - PL }; 331 | selectedText = prefix + selectedText + suffix; 332 | } else if (inPrefix === prefix && inSuffix === suffix) { 333 | //: selection inside match prefix and suffix => undo underline (outside selection) 334 | editorChange = { text: editor.getRange(Cursor(fos + PL), Cursor(tos - SL)), ...adjustRange }; 335 | // resetSelection = { anchor: Cursor(fos), head: Cursor(tos - PL - SL) } 336 | resetSelectionOffset = { anchor: fos, head: tos - PL - SL } 337 | } else { 338 | //: Add prefix and suffix to selection 339 | editorChange = { text: prefix + selectedText + suffix, ...adjustRange }; 340 | // resetSelection = { anchor: editor.offsetToPos(fos + PL), head: editor.offsetToPos(tos + PL) } 341 | resetSelectionOffset = { anchor: fos + PL, head: tos + PL } 342 | } 343 | return { 344 | editorChange: editorChange, 345 | selectedText: selectedText, 346 | // resetSelection: resetSelection, 347 | resetSelectionOffset: resetSelectionOffset, 348 | }; 349 | } 350 | 351 | export function replaceLigature(s: string): string { 352 | Object.entries(Ligatures).forEach(([key, value]) => { 353 | var rx = new RegExp(key, "g"); 354 | s = s.replace(rx, value); 355 | }); 356 | return s; 357 | } 358 | 359 | /** 360 | * @param [text] The text to sort 361 | * @param [context] The context of the sort, including the editor and the settings 362 | * @param [fromOffset=0] - the offset of the first line of the text to sort 363 | */ 364 | export function sortTodo(text: string, context: any, fromLine: number | null = null): string { 365 | const lines = text.split("\n"); 366 | // console.log("lines", lines) 367 | 368 | 369 | let prefix_text_index = -1, 370 | suffix_text_index = -1; 371 | let todos: { [key: string]: any[] } = {}; 372 | let todo_detected = false, sort_prefix = false; 373 | let indent = 0; 374 | let last_flag: string, // flag of last line that count in as a new todo of level `indent` 375 | flag: string; 376 | for (const [i, line] of lines.entries()) { 377 | let flags = /- \[([ \w])\] /g.exec(line); 378 | // console.log(i, flags, line); 379 | if (flags) { // it is a todo line 380 | let head = line.match(/^[ \t]*/g)[0]; 381 | if (!todo_detected) { 382 | // first time to detect todo checkbox 383 | indent = head.length; 384 | todo_detected = true; 385 | } else { 386 | if (head.length < indent) { 387 | // the level of this line is higher than before, 388 | // reset the index and consider above lines as prefix text 389 | prefix_text_index = i - 1; 390 | indent = head.length; 391 | todos = {}; // reset 392 | sort_prefix = true; 393 | } 394 | } 395 | 396 | if (head.length > indent) { 397 | let last_idx = todos[last_flag].length - 1; 398 | todos[last_flag][last_idx] += "\n" + line; 399 | } else { 400 | flag = flags[1]; 401 | if (!(flag in todos)) { 402 | todos[flag] = []; 403 | } 404 | todos[flag].push(line); 405 | last_flag = flag; 406 | } 407 | } else { 408 | // console.log("else", flags, todo_detected, line) 409 | if (todo_detected) { 410 | suffix_text_index = i; 411 | break; 412 | } else { 413 | prefix_text_index = i; 414 | } 415 | } 416 | } 417 | // console.log("todos", todos) 418 | // console.log("prefix_text_line", prefix_text_index, "suffix_text_line", suffix_text_index) 419 | const todoBlockRangeLine = { 420 | from: prefix_text_index != -1 ? fromLine + prefix_text_index : fromLine, 421 | to: suffix_text_index != -1 ? fromLine + suffix_text_index : fromLine + lines.length 422 | } 423 | // console.log(context.originRange.from.line, context.originRange.to.line);//, fromLine, fromLine + prefix_text_index, fromLine + suffix_text_index) 424 | // console.log(todoBlockRangeLine) 425 | let body: string; 426 | if (fromLine === null 427 | || ( 428 | (context.originRange.from.line >= todoBlockRangeLine.from && context.originRange.from.line <= todoBlockRangeLine.to) 429 | || (context.originRange.from.line <= todoBlockRangeLine.from && context.originRange.to.line >= todoBlockRangeLine.to) 430 | || (context.originRange.to.line >= todoBlockRangeLine.from && context.originRange.to.line <= todoBlockRangeLine.to) 431 | )) { 432 | 433 | body = ""; 434 | for (const [i, flag] of Object.keys(todos).sort().entries()) { 435 | todos[flag].forEach((line, j) => { 436 | // console.log("body line", line) 437 | if (line.match(/\n/g)) { 438 | let sub_lines = line.split("\n"); 439 | line = sub_lines[0] + "\n" + sortTodo(sub_lines.slice(1, sub_lines.length).join("\n"), context, null); 440 | } 441 | body += line + "\n"; 442 | }) 443 | } 444 | body = body.slice(0, body.length - 1); // remove the last "\n" 445 | 446 | } else { 447 | // console.log("else: Do not sort") 448 | // body = lines.slice(todoBlockRangeLine.from, todoBlockRangeLine.to).join("\n"); 449 | // body = lines.slice(prefix_text_line + 1, suffix_text_line + 1).join("\n"); 450 | body = lines.slice(prefix_text_index === -1 ? 0 : prefix_text_index + 1, 451 | suffix_text_index === -1 ? lines.length : suffix_text_index).join("\n"); 452 | // return text; 453 | } 454 | // return text; 455 | // console.log("input text") 456 | // console.log(text) 457 | // console.log("body", body) 458 | 459 | let prefix_text = prefix_text_index === -1 ? null : lines.slice(0, prefix_text_index + 1).join('\n'); 460 | // prefix_text = lines.slice(0, prefix_text_line + 1).join('\n'); 461 | // let suffix_text = suffix_text_index === -1 ? null : ( 462 | // suffix_text_index + 1 == lines.length ? null : lines.slice(suffix_text_index + 1, lines.length).join('\n')); 463 | // console.log("suffix_text", suffix_text_index + 1 == lines.length) 464 | if (sort_prefix) { 465 | prefix_text = sortTodo(prefix_text, context, fromLine); 466 | } 467 | let suffix_text = suffix_text_index === -1 ? null : lines.slice(suffix_text_index, lines.length + 1).join("\n"); 468 | if (!(suffix_text_index == -1 || (suffix_text_index + 1 == lines.length))) { 469 | // suffix_text = lines.slice(suffix_text_index + 1, lines.length + 1).join("\n"); 470 | suffix_text = sortTodo(suffix_text, context, suffix_text_index == -1 ? null : fromLine + suffix_text_index); 471 | } 472 | let whole = [prefix_text, body, suffix_text]; 473 | // console.log(prefix_text_index, suffix_text_index) 474 | // console.log("text", text) 475 | // console.log("whole", whole); 476 | whole = whole.filter(item => item != null) // remove empty lines 477 | return whole.join('\n'); 478 | } 479 | 480 | 481 | export async function requestAPI(s: string, file: TFile, url: string): Promise<string> { 482 | try { 483 | const data = { 484 | text: s, 485 | path: file.path, 486 | } 487 | 488 | const response = await requestUrl({ 489 | url: url, 490 | method: "POST", 491 | contentType: "application/json", 492 | body: JSON.stringify(data), 493 | }) 494 | 495 | const res = response.json; 496 | if (res.notification) { 497 | new Notice(res.notification); 498 | } 499 | 500 | if (res.text) { 501 | return res.text; 502 | } else { 503 | return s; 504 | } 505 | } 506 | catch (e) { 507 | new Notice(`Fail to request API.\n${e}`); 508 | return s; 509 | } 510 | } 511 | 512 | 513 | export function slugify(text: string, maxLength: number = 76): string { 514 | // Convert to Lowercase 515 | text = text.toLowerCase(); 516 | // Remove Special Characters, preserve Latin and Cyrillic and Swedish characters 517 | text = text.replace(/[^\w\s\u0400-\u04FFåäöÅÄÖ]|_/g, "").replace(/\s+/g, " ").trim(); 518 | // Replace Spaces with Dashes 519 | text = text.replace(/\s+/g, "-"); 520 | // Remove Accents and Diacritics 521 | text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 522 | // Handle Multiple Dashes 523 | text = text.replace(/-{2,}/g, "-"); 524 | // Handle Numerals 525 | if (/^\d+$/.test(text)) { 526 | // If the slug is numeric only, add a suffix to make it unique and descriptive 527 | text = `item-${text}`; 528 | } 529 | 530 | // Truncate Length if required 531 | if (text.length > maxLength) { 532 | text = text.substr(0, maxLength); 533 | // Handle case where the last character is a hyphen 534 | if (text.endsWith("-")) { 535 | text = text.substr(0, text.lastIndexOf("-")); 536 | } 537 | } 538 | 539 | // Handle Hyphens and Dashes 540 | text = text.replace(/^-+|-+$/g, ""); 541 | return text; 542 | } 543 | 544 | export function snakify(text: string): string { 545 | text = text.toLowerCase(); 546 | text = text.replace(/\s+/g, "_"); 547 | return text; 548 | } 549 | 550 | export function camelCase(text: string, lowerFirst = false): string { 551 | text = toTitleCase(text.toLowerCase()); 552 | text = text.replace(/\s+/g, ""); 553 | if (lowerFirst) { 554 | text = text.charAt(0).toLowerCase() + text.slice(1); 555 | } 556 | return text; 557 | } 558 | 559 | export function extraDoubleSpaces(editor: Editor, view: MarkdownView): void { 560 | if (!view) { 561 | return; 562 | } 563 | let content = editor.getValue(); 564 | content = content.replace( 565 | /^(?:---\n[\s\S]*?\n---\n|)([\s\S]+)$/g, // exclude meta table 566 | (whole_content: string, body: string) => { 567 | return whole_content.replace(body, () => { 568 | return body.replace(/(?:\n)(.*[^-\n]+.*)(?=\n)/g, 569 | (t0, t) => t0.replace(t, `${t.replace(/ +$/g, '')} `) 570 | ) 571 | }); 572 | } 573 | ); 574 | editor.setValue(content); 575 | } 576 | 577 | export function customReplace(text: string, s: customReplaceSetting): string { 578 | s.data.forEach(data => { 579 | const re = new RegExp(data.search, "g"); 580 | text = text.replace(re, JSON.parse(`"${data.replace}"`)) 581 | }) 582 | return text; 583 | } 584 | 585 | export function convertLatex(editor: Editor, selectedText: string): string { 586 | //: If selectedText is surrounded by `$`, convert unicode Greek letters to latex commands 587 | if (editor) { 588 | const fos = editor.posToOffset(editor.getCursor("from")); // from offset 589 | const tos = editor.posToOffset(editor.getCursor("to")); // to offset 590 | const beforeText = editor.getRange(editor.offsetToPos(fos - 1), editor.offsetToPos(fos)); 591 | const afterText = editor.getRange(editor.offsetToPos(tos), editor.offsetToPos(tos + 1)); 592 | if (beforeText === "$" && afterText === "$") { 593 | let result = ""; 594 | let lastGreek = false; 595 | for (let i = 0; i < selectedText.length; i++) { 596 | let char = GreekLetters[selectedText[i]]; 597 | if (char) { 598 | result += char; 599 | lastGreek = true; 600 | } else { 601 | if (lastGreek && !/\d/.test(selectedText[i])) { 602 | result += " "; 603 | } 604 | result += selectedText[i]; 605 | lastGreek = false; 606 | } 607 | } 608 | return result.replace(/\s*$/g, ""); 609 | } 610 | } 611 | 612 | function G(str: string): string { 613 | return GreekLetters[str] || str; 614 | } 615 | // const reGreek = /[\u03B1-\u03C9\u0391-\u03A9]/g; 616 | 617 | //: Or, find math text and surround it with `$` 618 | const pre = String.raw`([\s:()。,、;—\(\)]|^)`; 619 | const suf = String.raw`(?=[\s\,\:\.\?\!,。、();—\(\)]|$)`; 620 | 621 | const patternChar2 = String.raw`([\u03B1-\u03C9\u0391-\u03A9a-zA-Z])([\u03B1-\u03C9\u0391-\u03A9a-zA-Z0-9])`; 622 | 623 | let replacedText = selectedText 624 | // single character 625 | .replace( 626 | RegExp(pre + String.raw`([a-zA-Z\u03B1-\u03C9\u0391-\u03A9])` + suf, "g"), 627 | (t, pre, t1) => { 628 | if (/[aA]/.test(t1)) { return t; } 629 | return pre + `$${G(t1)}$`; 630 | }) 631 | // two characters 632 | .replace( 633 | RegExp(pre + patternChar2 + suf, "g"), 634 | (t, pre, t1, t2) => { 635 | // ignore cases 636 | if (/is|or|as|to|am|an|at|by|do|go|ha|he|hi|ho|if|in|it|my|no|of|on|so|up|us|we|be/g.test(t1 + t2)) { return t; } 637 | return pre + `$${G(t1)}_${G(t2)}$`; 638 | }) 639 | .replace( 640 | RegExp(pre + String.raw`([a-z\u03B1-\u03C9\u0391-\u03A9])([\*])` + suf, "g"), 641 | (t, pre, t1, t2) => { 642 | return pre + `$${t1}^${t2}$`; 643 | }) 644 | // calculator 645 | .replace( 646 | RegExp(pre + String.raw`([\w\u03B1-\u03C9\u0391-\u03A9]{1,3}[\+\-\*\/<>=][\w\u03B1-\u03C9\u0391-\u03A9]{1,3})` + suf, "g"), 647 | (t, pre, t1) => { 648 | // let content = t1.replace(/([a-z])([a-zA-Z0-9])/g, `$1_$2`) 649 | let content = t1.replace(RegExp(patternChar2, "g"), 650 | (t: string, t1: string, t2: string) => `${G(t1)}_${G(t2)}`) 651 | return pre + `$${content}$` 652 | }) 653 | ; 654 | return replacedText; 655 | } -------------------------------------------------------------------------------- /src/settings/settingTab.ts: -------------------------------------------------------------------------------- 1 | import { Setting, PluginSettingTab, App, ButtonComponent, setIcon } from "obsidian"; 2 | import TextFormat from "../main"; 3 | import { Wikilink2mdPathMode, CalloutTypeDecider } from './types'; 4 | import { CustomReplacementBuiltInCommands } from "../commands" 5 | import { getString } from "../langs/langs"; 6 | import { addDonationElement } from "./donation" 7 | import { v4 as uuidv4 } from "uuid"; 8 | 9 | export class TextFormatSettingTab extends PluginSettingTab { 10 | plugin: TextFormat; 11 | contentEl: HTMLElement; 12 | collapseMemory: { [key: string]: boolean }; 13 | 14 | constructor(app: App, plugin: TextFormat) { 15 | super(app, plugin); 16 | this.plugin = plugin; 17 | this.collapseMemory = {} 18 | // this.builtInCustomReplacement(); 19 | } 20 | 21 | // async builtInCustomReplacement() { 22 | // for (let command of CustomReplacementBuiltInCommands) { 23 | // if (this.plugin.settings.customReplaceBuiltInLog[command.id] == null) { 24 | // this.plugin.settings.customReplaceList.push({ name: getString(["command", command.id]), data: command.data }); 25 | // this.plugin.settings.customReplaceBuiltInLog[command.id] = { id: command.id, modified: false }; 26 | // } 27 | // } 28 | // await this.plugin.saveSettings(); 29 | // } 30 | 31 | display(): void { 32 | let { containerEl } = this; 33 | 34 | let headerEl; 35 | 36 | containerEl.empty(); 37 | containerEl.addClass("plugin-text-format"); 38 | containerEl 39 | .createEl("p", { text: getString(["setting", "more-details"]) }) 40 | .createEl("a", { 41 | text: "text-format", 42 | href: "https://github.com/Benature/obsidian-text-format", 43 | }); 44 | 45 | this.addSettingsAboutWordCase(containerEl); 46 | this.addSettingsAboutLink(containerEl); 47 | this.addSettingsAboutList(containerEl); 48 | this.addSettingsAboutMarkdownQuicker(containerEl); 49 | this.addSettingsAboutWrapper(containerEl); 50 | this.addSettingsAboutApiRequest(containerEl); 51 | this.addSettingsAboutReplacement(containerEl); 52 | 53 | 54 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 55 | headerEl = headerDiv.createEl("h3", { text: getString(["setting", "others"]) }); 56 | 57 | this.contentEl = containerEl.createDiv(); 58 | this.makeCollapsible(headerEl, this.contentEl, true); 59 | 60 | 61 | new Setting(this.contentEl) 62 | .setName(getString(["setting", "format-on-paste", "name"])) 63 | .setDesc(getString(["setting", "format-on-paste", "desc"])) 64 | .addToggle((toggle) => { 65 | toggle 66 | .setValue(this.plugin.settings.formatOnSaveSettings.enabled) 67 | .onChange(async (value) => { 68 | this.plugin.settings.formatOnSaveSettings.enabled = value; 69 | await this.plugin.saveSettings(); 70 | }); 71 | }) 72 | .addTextArea((text) => 73 | text 74 | // .setPlaceholder(getString(["setting", "format-on-paste", "placeholder"])) 75 | .setValue(this.plugin.settings.formatOnSaveSettings.commandsString) 76 | .onChange(async (value) => { 77 | this.plugin.settings.formatOnSaveSettings.commandsString = value; 78 | await this.plugin.saveSettings(); 79 | }) 80 | ); 81 | 82 | 83 | new Setting(this.contentEl) 84 | .setName(getString(["setting", "remove-spaces-when-converting"])) 85 | .setDesc(getString(["setting", "remove-spaces-when-converting-desc"])) 86 | .addToggle((toggle) => { 87 | toggle 88 | .setValue(this.plugin.settings.RemoveBlanksWhenChinese) 89 | .onChange(async (value) => { 90 | this.plugin.settings.RemoveBlanksWhenChinese = value; 91 | await this.plugin.saveSettings(); 92 | }); 93 | }); 94 | 95 | new Setting(this.contentEl) 96 | .setName(getString(["setting", "debug-logging"])) 97 | .setDesc(getString(["setting", "debug-logging-desc"])) 98 | .addToggle((toggle) => { 99 | toggle 100 | .setValue(this.plugin.settings.debugMode) 101 | .onChange(async (value) => { 102 | this.plugin.settings.debugMode = value; 103 | await this.plugin.saveSettings(); 104 | }); 105 | }); 106 | this.addSettingsAboutParagraph(this.contentEl); 107 | 108 | this.addSettingsAboutZotero(this.contentEl); 109 | 110 | addDonationElement(containerEl); 111 | } 112 | 113 | // refer from https://github.com/Mocca101/obsidian-plugin-groups/tree/main 114 | makeCollapsible(foldClickElement: HTMLElement, content: HTMLElement, startOpened?: boolean) { 115 | if (!content.hasClass('tf-collapsible-content')) { 116 | content.addClass('tf-collapsible-content'); 117 | } 118 | 119 | if (!foldClickElement.hasClass('tf-collapsible-header')) { 120 | foldClickElement.addClass('tf-collapsible-header'); 121 | } 122 | 123 | toggleCollapsibleIcon(foldClickElement); 124 | 125 | let text = "<unknown>"; 126 | // settings headers are H3 127 | if (["H3", "H4"].includes(foldClickElement.tagName)) { 128 | text = foldClickElement.textContent; 129 | if (!(text in this.collapseMemory)) { 130 | this.collapseMemory[text] = false; 131 | } 132 | startOpened = startOpened ? true : this.collapseMemory[text]; 133 | } 134 | 135 | if (startOpened) { 136 | content.addClass('is-active'); 137 | toggleCollapsibleIcon(foldClickElement); 138 | } 139 | 140 | foldClickElement.onclick = () => { 141 | if (content.hasClass('is-active')) { 142 | content.removeClass('is-active'); 143 | this.collapseMemory[text] = false; 144 | } else { 145 | content.addClass('is-active'); 146 | this.collapseMemory[text] = true; 147 | } 148 | 149 | toggleCollapsibleIcon(foldClickElement); 150 | }; 151 | } 152 | 153 | 154 | addSettingsAboutWordCase(containerEl: HTMLElement) { 155 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 156 | let headerEl = headerDiv.createEl("h3", { text: getString(["setting", "word-cases"]) }) 157 | headerDiv.createEl("div", { text: getString(["setting", "word-cases-desc"]), cls: "setting-item-description heading-description" }); 158 | this.contentEl = containerEl.createDiv(); 159 | this.makeCollapsible(headerEl, this.contentEl); 160 | new Setting(this.contentEl) 161 | .setName(getString(["setting", "lowercase-before-capitalize"])) 162 | .setDesc(getString(["setting", "lowercase-before-capitalize-desc"])) 163 | .addToggle((toggle) => { 164 | toggle 165 | .setValue(this.plugin.settings.LowercaseFirst) 166 | .onChange(async (value) => { 167 | this.plugin.settings.LowercaseFirst = value; 168 | await this.plugin.saveSettings(); 169 | }); 170 | }); 171 | new Setting(this.contentEl) 172 | .setName(getString(["setting", "cycle-case-sequence"])) 173 | .setDesc(getString(["setting", "cycle-case-sequence-desc"])) 174 | .addTextArea((text) => 175 | text 176 | .setPlaceholder("lowerCase\nupperCase") 177 | .setValue(this.plugin.settings.ToggleSequence) 178 | .onChange(async (value) => { 179 | this.plugin.settings.ToggleSequence = value; 180 | await this.plugin.saveSettings(); 181 | }) 182 | ); 183 | new Setting(this.contentEl) 184 | .setName(getString(["setting", "proper-noun"])) 185 | .setDesc(getString(["setting", "proper-noun-desc"])) 186 | .addTextArea((text) => 187 | text 188 | .setPlaceholder("USA, UFO") 189 | .setValue(this.plugin.settings.ProperNoun) 190 | .onChange(async (value) => { 191 | this.plugin.settings.ProperNoun = value; 192 | await this.plugin.saveSettings(); 193 | }) 194 | ); 195 | } 196 | addSettingsAboutParagraph(containerEl: HTMLElement) { 197 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 198 | let headerEl = headerDiv.createEl("h4", { text: getString(["setting", "paragraph", "header"]) }); 199 | 200 | let contentEl = containerEl.createDiv(); 201 | this.makeCollapsible(headerEl, contentEl); 202 | new Setting(contentEl) 203 | .setName(getString(["setting", "paragraph", "remove-redundant-blank-lines"])) 204 | .setDesc(getString(["setting", "paragraph", "remove-redundant-blank-lines-desc"])) 205 | .addToggle((toggle) => { 206 | toggle 207 | .setValue(this.plugin.settings.MergeParagraph_Newlines) 208 | .onChange(async (value) => { 209 | this.plugin.settings.MergeParagraph_Newlines = value; 210 | await this.plugin.saveSettings(); 211 | }); 212 | }); 213 | 214 | new Setting(contentEl) 215 | .setName(getString(["setting", "remove-redundant-blank-spaces"])) 216 | .setDesc(getString(["setting", "remove-redundant-blank-spaces-desc"])) 217 | .addToggle((toggle) => { 218 | toggle 219 | .setValue(this.plugin.settings.MergeParagraph_Spaces) 220 | .onChange(async (value) => { 221 | this.plugin.settings.MergeParagraph_Spaces = value; 222 | await this.plugin.saveSettings(); 223 | }); 224 | }); 225 | } 226 | 227 | addSettingsAboutLink(containerEl: HTMLElement) { 228 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 229 | let headerEl = headerDiv.createEl("h3", { text: getString(["setting", "link-format"]) }); 230 | headerDiv.createEl("div", { text: getString(["setting", "link-format-desc"]), cls: "setting-item-description heading-description" }); 231 | 232 | this.contentEl = containerEl.createDiv(); 233 | this.makeCollapsible(headerEl, this.contentEl); 234 | new Setting(this.contentEl) 235 | .setName(getString(["setting", "path-mode"])) 236 | // .setDesc("Or will use absolute path instead.") 237 | .addDropdown(dropDown => 238 | dropDown 239 | .addOption(Wikilink2mdPathMode.relativeObsidian, getString(["setting", "Wikilink2mdPathMode-relative-obsidian"])) 240 | .addOption(Wikilink2mdPathMode.relativeFile, getString(["setting", "Wikilink2mdPathModerelative-file"])) 241 | .addOption(Wikilink2mdPathMode.absolute, getString(["setting", "Wikilink2mdPathMode-absolute"])) 242 | .setValue(this.plugin.settings.Wikilink2mdRelativePath || Wikilink2mdPathMode.relativeObsidian) 243 | .onChange(async (value) => { 244 | this.plugin.settings.Wikilink2mdRelativePath = value as Wikilink2mdPathMode; 245 | await this.plugin.saveSettings(); 246 | })); 247 | new Setting(this.contentEl) 248 | .setName(getString(["setting", "result-format"])) 249 | .setDesc(getString(["setting", "result-format-desc"])) 250 | .addTextArea((text) => 251 | text 252 | .setPlaceholder("{url}") 253 | .setValue(this.plugin.settings.UrlLinkFormat) 254 | .onChange(async (value) => { 255 | this.plugin.settings.UrlLinkFormat = value; 256 | await this.plugin.saveSettings(); 257 | }) 258 | ); 259 | new Setting(this.contentEl) 260 | .setName(getString(["setting", "remove-wikilink-url"])) 261 | .addToggle((toggle) => { 262 | toggle 263 | .setValue(this.plugin.settings.RemoveWikiURL2) 264 | .onChange(async (value) => { 265 | this.plugin.settings.RemoveWikiURL2 = value; 266 | await this.plugin.saveSettings(); 267 | }); 268 | }); 269 | this.contentEl.createEl("h4", { text: "Format when removing wikiLink" }); 270 | // containerEl.createEl("p", { text: "Define the result of calling `Remove WikiLink format in selection`" }); 271 | new Setting(this.contentEl) 272 | .setName(getString(["setting", "wiki-link-format-heading"])) 273 | .setDesc(getString(["setting", "wiki-link-format-heading-desc"])) 274 | .addTextArea((text) => 275 | text 276 | .setPlaceholder("{title} (> {heading})") 277 | .setValue(this.plugin.settings.WikiLinkFormat.headingOnly) 278 | .onChange(async (value) => { 279 | this.plugin.settings.WikiLinkFormat.headingOnly = value; 280 | await this.plugin.saveSettings(); 281 | }) 282 | ); 283 | new Setting(this.contentEl) 284 | .setName(getString(["setting", "wiki-link-format-alias"])) 285 | .setDesc(getString(["setting", "wiki-link-format-alias-desc"])) 286 | .addTextArea((text) => 287 | text 288 | .setPlaceholder("{alias} ({title})") 289 | .setValue(this.plugin.settings.WikiLinkFormat.aliasOnly) 290 | .onChange(async (value) => { 291 | this.plugin.settings.WikiLinkFormat.aliasOnly = value; 292 | await this.plugin.saveSettings(); 293 | }) 294 | ); 295 | new Setting(this.contentEl) 296 | .setName(getString(["setting", "wiki-link-format-both"])) 297 | .setDesc(getString(["setting", "wiki-link-format-both-desc"])) 298 | .addTextArea((text) => 299 | text 300 | .setPlaceholder("{alias} ({title})") 301 | .setValue(this.plugin.settings.WikiLinkFormat.both) 302 | .onChange(async (value) => { 303 | this.plugin.settings.WikiLinkFormat.both = value; 304 | await this.plugin.saveSettings(); 305 | }) 306 | ); 307 | } 308 | addSettingsAboutList(containerEl: HTMLElement) { 309 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 310 | let headerEl = headerDiv.createEl("h3", { text: getString(["setting", "list-format"]) }); 311 | headerDiv.createEl("div", { text: getString(["setting", "list-format-desc"]), cls: "setting-item-description heading-description" }); 312 | this.contentEl = containerEl.createDiv(); 313 | this.makeCollapsible(headerEl, this.contentEl); 314 | new Setting(this.contentEl) 315 | .setName(getString(["setting", "bullet-point-characters"])) 316 | .setDesc(getString(["setting", "bullet-point-characters-desc"])) 317 | .addTextArea((text) => 318 | text 319 | .setPlaceholder("•–") 320 | .setValue(this.plugin.settings.BulletPoints) 321 | .onChange(async (value) => { 322 | this.plugin.settings.BulletPoints = value; 323 | await this.plugin.saveSettings(); 324 | }) 325 | ); 326 | new Setting(this.contentEl) 327 | .setName(getString(["setting", "ordered-list-custom-separator"])) 328 | .setDesc(getString(["setting", "ordered-list-custom-separator-desc"])) 329 | .addTextArea((text) => 330 | text 331 | .setPlaceholder( 332 | String.raw`` 333 | ) 334 | .setValue(this.plugin.settings.OrderedListOtherSeparator) 335 | .onChange(async (value) => { 336 | this.plugin.settings.OrderedListOtherSeparator = value; 337 | await this.plugin.saveSettings(); 338 | }) 339 | ); 340 | } 341 | addSettingsAboutWrapper(containerEl: HTMLElement) { 342 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 343 | let headerEl = headerDiv.createEl("h3", { text: getString(["setting", "wrapper", "header"]) }); 344 | headerDiv.createEl("div", { text: getString(["setting", "wrapper", "desc"]), cls: "setting-item-description heading-description" }); 345 | 346 | this.contentEl = containerEl.createDiv(); 347 | this.makeCollapsible(headerEl, this.contentEl); 348 | const wrapperRuleDesc = document.createDocumentFragment(); 349 | wrapperRuleDesc.append( 350 | getString(["setting", "wrapper", "rule-desc1"]), 351 | document.createDocumentFragment().createEl("br"), 352 | getString(["setting","wrapper", "rule-desc2"]) 353 | ); 354 | new Setting(this.contentEl) 355 | .setName(getString(["setting", "wrapper", "add-new-wrapper"])) 356 | .setDesc(wrapperRuleDesc) 357 | .addButton((button: ButtonComponent) => { 358 | button 359 | .setTooltip(getString(["setting","wrapper", "new-wrapper-rule-tooltip"])) 360 | .setButtonText("+") 361 | .setCta() 362 | .onClick(async () => { 363 | this.plugin.settings.WrapperList.push({ 364 | id: uuidv4(), 365 | name: "", 366 | prefix: "", 367 | suffix: "", 368 | }); 369 | await this.plugin.saveSettings(); 370 | this.display(); 371 | }); 372 | }); 373 | this.plugin.settings.WrapperList.forEach((wrapperSetting, index) => { 374 | const s = new Setting(this.contentEl) 375 | .addText((cb) => { 376 | cb.setPlaceholder(getString(["setting", "wrapper", "name-placeholder"])) 377 | .setValue(wrapperSetting.name) 378 | .onChange(async (newValue) => { 379 | this.plugin.settings.WrapperList[index].name = newValue; 380 | await this.plugin.saveSettings(); 381 | this.plugin.debounceUpdateCommandWrapper(); 382 | }); 383 | }) 384 | .addText((cb) => { 385 | cb.setPlaceholder(getString(["setting","wrapper", "prefix-placeholder"])) 386 | .setValue(wrapperSetting.prefix) 387 | .onChange(async (newValue) => { 388 | this.plugin.settings.WrapperList[index].prefix = newValue; 389 | await this.plugin.saveSettings(); 390 | this.plugin.debounceUpdateCommandWrapper(); 391 | }); 392 | }) 393 | .addText((cb) => { 394 | cb.setPlaceholder(getString(["setting","wrapper", "suffix-placeholder"])) 395 | .setValue(wrapperSetting.suffix) 396 | .onChange(async (newValue) => { 397 | this.plugin.settings.WrapperList[index].suffix = newValue; 398 | await this.plugin.saveSettings(); 399 | this.plugin.debounceUpdateCommandWrapper(); 400 | }); 401 | }) 402 | .addExtraButton((cb) => { 403 | cb.setIcon("cross") 404 | .setTooltip(getString(["setting", "delete-tooltip"])) 405 | .onClick(async () => { 406 | this.plugin.settings.WrapperList.splice(index, 1); 407 | await this.plugin.saveSettings(); 408 | this.display(); 409 | }); 410 | }); 411 | s.infoEl.remove(); 412 | s.settingEl.addClass("wrapper"); 413 | }); 414 | } 415 | addSettingsAboutApiRequest(containerEl: HTMLElement) { 416 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 417 | let headerEl = headerDiv.createEl("h3", { text: getString(["setting", "api-request"]) }); 418 | headerDiv.createEl("div", { text: getString(["setting", "api-request-desc"]), cls: "setting-item-description heading-description" }); 419 | 420 | this.contentEl = containerEl.createDiv(); 421 | this.makeCollapsible(headerEl, this.contentEl); 422 | new Setting(this.contentEl) 423 | .setName(getString(["setting", "api-request-url"])) 424 | .setDesc(getString(["setting", "api-request-url-desc"])) 425 | .addButton((button: ButtonComponent) => { 426 | button.setTooltip(getString(["setting", "new-request-tooltip"])) 427 | .setButtonText("+") 428 | .setCta().onClick(async () => { 429 | this.plugin.settings.RequestList.push({ 430 | id: uuidv4(), 431 | name: "", 432 | url: "", 433 | }); 434 | await this.plugin.saveSettings(); 435 | this.display(); 436 | }); 437 | }) 438 | this.plugin.settings.RequestList.forEach((requestSetting, index) => { 439 | const s = new Setting(this.contentEl) 440 | .addText((cb) => { 441 | cb.setPlaceholder(getString(["setting", "request-name-placeholder"])) 442 | .setValue(requestSetting.name) 443 | .onChange(async (newValue) => { 444 | this.plugin.settings.RequestList[index].name = newValue; 445 | await this.plugin.saveSettings(); 446 | this.plugin.debounceUpdateCommandRequest(); 447 | }); 448 | }) 449 | .addText((cb) => { 450 | cb.setPlaceholder(getString(["setting", "request-url-placeholder"])) 451 | .setValue(requestSetting.url) 452 | .onChange(async (newValue) => { 453 | this.plugin.settings.RequestList[index].url = newValue; 454 | await this.plugin.saveSettings(); 455 | this.plugin.debounceUpdateCommandRequest(); 456 | }); 457 | }) 458 | .addExtraButton((cb) => { 459 | cb.setIcon("cross") 460 | .setTooltip(getString(["setting", "delete-tooltip"])) 461 | .onClick(async () => { 462 | this.plugin.settings.RequestList.splice(index, 1); 463 | await this.plugin.saveSettings(); 464 | this.display(); 465 | }); 466 | }); 467 | s.infoEl.remove(); 468 | s.settingEl.addClass("api-request"); 469 | }); 470 | } 471 | addSettingsAboutReplacement(containerEl: HTMLElement) { 472 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 473 | let headerEl = headerDiv.createEl("h3", { text: getString(["setting", "custom-replacement"]) }); 474 | headerDiv.createEl("div", { text: getString(["setting", "custom-replacement-desc"]), cls: "setting-item-description heading-description" }); 475 | 476 | 477 | this.contentEl = containerEl.createDiv(); 478 | this.makeCollapsible(headerEl, this.contentEl); 479 | new Setting(this.contentEl) 480 | .setName(getString(["setting", "add-custom-replacement"])) 481 | .setDesc(getString(["setting", "add-custom-replacement-desc"])) 482 | .addButton((button: ButtonComponent) => { 483 | button.setTooltip(getString(["setting", "add-new-replacement-tooltip"])) 484 | .setButtonText("+") 485 | .setCta().onClick(async () => { 486 | this.plugin.settings.customReplaceList.push({ 487 | id: uuidv4(), 488 | name: "", 489 | data: [{ 490 | search: "", 491 | replace: "", 492 | }] 493 | }); 494 | await this.plugin.saveSettings(); 495 | this.display(); 496 | }); 497 | }) 498 | this.plugin.settings.customReplaceList.forEach((replaceSetting, index) => { 499 | const checkIsBuiltIn = () => { 500 | if (this.plugin.settings.customReplaceBuiltInLog[replaceSetting.id]) { 501 | let logData = this.plugin.settings.customReplaceBuiltInLog[replaceSetting.id].data; 502 | let nowData = replaceSetting.data; 503 | if (logData.length != nowData.length) { 504 | this.plugin.settings.customReplaceBuiltInLog[replaceSetting.id].modified = true; 505 | return; 506 | } 507 | for (let i = 0; i < logData.length; i++) { 508 | console.log(logData[i], nowData[i]) 509 | if (logData[i].search !== nowData[i].search || logData[i].replace !== nowData[i].replace) { 510 | this.plugin.settings.customReplaceBuiltInLog[replaceSetting.id].modified = true; 511 | return; 512 | } 513 | } 514 | this.plugin.settings.customReplaceBuiltInLog[replaceSetting.id].modified = false; 515 | } 516 | }; 517 | const s = new Setting(this.contentEl) 518 | .addText((cb) => { 519 | cb.setPlaceholder(getString(["setting", "replacement-command-name-placeholder"])) 520 | .setValue(replaceSetting.name) 521 | .onChange(async (newValue) => { 522 | this.plugin.settings.customReplaceList[index].name = newValue; 523 | await this.plugin.saveSettings(); 524 | this.plugin.debounceUpdateCommandCustomReplace(); 525 | }); 526 | }) 527 | .addText((cb) => { 528 | cb.setPlaceholder(getString(["setting", "replacement-search-placeholder"])) 529 | .setValue(replaceSetting.data[0].search) 530 | .onChange(async (newValue) => { 531 | this.plugin.settings.customReplaceList[index].data[0].search = newValue; 532 | checkIsBuiltIn(); 533 | await this.plugin.saveSettings(); 534 | this.plugin.debounceUpdateCommandCustomReplace(); 535 | }); 536 | }) 537 | .addText((cb) => { 538 | cb.setPlaceholder(getString(["setting", "replacement-replace-placeholder"])) 539 | .setValue(replaceSetting.data[0].replace) 540 | .onChange(async (newValue) => { 541 | this.plugin.settings.customReplaceList[index].data[0].replace = newValue; 542 | checkIsBuiltIn(); 543 | await this.plugin.saveSettings(); 544 | this.plugin.debounceUpdateCommandCustomReplace(); 545 | }); 546 | }) 547 | .addExtraButton((cb) => { 548 | cb.setIcon("cross") 549 | .setTooltip(getString(["setting", "delete-tooltip"])) 550 | .onClick(async () => { 551 | this.plugin.settings.customReplaceList.splice(index, 1); 552 | await this.plugin.saveSettings(); 553 | this.display(); 554 | }); 555 | }); 556 | s.settingEl.addClass("custom-replace"); 557 | s.infoEl.remove(); 558 | }); 559 | } 560 | addSettingsAboutZotero(containerEl: HTMLElement) { 561 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 562 | let headerEl = headerDiv.createEl("h4", { text: getString(["setting", "zotero-pdf-note-format"]) }); 563 | 564 | let contentEl = containerEl.createDiv(); 565 | this.makeCollapsible(headerEl, contentEl); 566 | const zoteroEl = new Setting(contentEl) 567 | .setName(getString(["setting", "zotero-input-regexp"])) 568 | .addTextArea((text) => { 569 | text 570 | .setPlaceholder( 571 | String.raw`“(?<text>.*)” \((?<item>.*?)\) \(\[pdf\]\((?<pdf_url>.*?)\)\)` 572 | ) 573 | .setValue(this.plugin.settings.ZoteroNoteRegExp) 574 | .onChange(async (value) => { 575 | this.plugin.settings.ZoteroNoteRegExp = value; 576 | await this.plugin.saveSettings(); 577 | }) 578 | text.inputEl.setCssProps({ "height": "5rem" }); 579 | } 580 | ); 581 | zoteroEl.descEl.innerHTML = `<div>The format of note template can configured refer to <a href="https://github.com/Benature/obsidian-text-format?tab=readme-ov-file#zotero-format">document</a>.</div> 582 | <ul> 583 | <li><code>text</code>: highlight</li> 584 | <li><code>pdf_url</code>: comment</li> 585 | <li><code>item</code>: citation</li> 586 | </ul>`; 587 | new Setting(contentEl) 588 | .setName(getString(["setting", "zotero-output-format"])) 589 | .setDesc(getString(["setting", "zotero-output-format-desc"]) 590 | 591 | ) 592 | .addTextArea((text) => { 593 | text 594 | .setPlaceholder("{text} [🔖]({pdf_url})") 595 | .setValue(this.plugin.settings.ZoteroNoteTemplate) 596 | .onChange(async (value) => { 597 | this.plugin.settings.ZoteroNoteTemplate = value; 598 | await this.plugin.saveSettings(); 599 | }); 600 | 601 | } 602 | ); 603 | } 604 | addSettingsAboutMarkdownQuicker(containerEl: HTMLElement) { 605 | let headerDiv = containerEl.createDiv({ cls: "header-div" }); 606 | let headerEl = headerDiv.createEl("h3", { text: getString(["setting", "markdown-quicker"]) }); 607 | headerDiv.createEl("div", { text: getString(["setting", "markdown-quicker-desc"]), cls: "setting-item-description heading-description" }); 608 | this.contentEl = containerEl.createDiv(); 609 | this.makeCollapsible(headerEl, this.contentEl); 610 | 611 | new Setting(this.contentEl) 612 | .setName(getString(["setting", "heading-lower-to-plain"])) 613 | .setDesc(getString(["setting", "heading-lower-to-plain-desc"])) 614 | .addToggle((toggle) => { 615 | toggle 616 | .setValue(this.plugin.settings.headingLevelMin === 0) 617 | .onChange(async (value) => { 618 | this.plugin.settings.headingLevelMin = value ? 0 : 1; 619 | await this.plugin.saveSettings(); 620 | }); 621 | }); 622 | new Setting(this.contentEl) 623 | .setName(getString(["setting", "method-decide-callout-type"])) 624 | .setDesc(getString(["setting", "method-decide-callout-type-desc"])) 625 | .addDropdown(dropDown => 626 | dropDown 627 | .addOption(CalloutTypeDecider.preContent, 'Last callout type before the cursor') 628 | .addOption(CalloutTypeDecider.wholeFile, 'Last callout type in the whole file') 629 | .addOption(CalloutTypeDecider.fix, 'Fixed callout type') 630 | .setValue(this.plugin.settings.calloutTypeDecider || CalloutTypeDecider.preContent) 631 | .onChange(async (value) => { 632 | this.plugin.settings.calloutTypeDecider = value as CalloutTypeDecider; 633 | await this.plugin.saveSettings(); 634 | })); 635 | new Setting(this.contentEl) 636 | .setName(getString(["setting", "default-callout-type"])) 637 | .setDesc(getString(["setting", "default-callout-type-desc"])) 638 | .addText((text) => 639 | text 640 | .setPlaceholder("Callout type") 641 | .setValue(this.plugin.settings.calloutType) 642 | .onChange(async (value) => { 643 | this.plugin.settings.calloutType = value; 644 | await this.plugin.saveSettings(); 645 | }) 646 | ); 647 | } 648 | } 649 | 650 | 651 | 652 | function toggleCollapsibleIcon(parentEl: HTMLElement) { 653 | let foldable: HTMLElement | null = parentEl.querySelector( 654 | ':scope > .tf-collapsible-icon' 655 | ); 656 | if (!foldable) { 657 | foldable = parentEl.createSpan({ cls: 'tf-collapsible-icon' }); 658 | } 659 | if (foldable.dataset.togglestate === 'up') { 660 | setIcon(foldable, 'chevron-down'); 661 | foldable.dataset.togglestate = 'down'; 662 | } else { 663 | setIcon(foldable, 'chevron-up'); 664 | foldable.dataset.togglestate = 'up'; 665 | } 666 | } 667 | -------------------------------------------------------------------------------- /src/settings/donation.ts: -------------------------------------------------------------------------------- 1 | export function addDonationElement(containerEl: HTMLElement): void { 2 | const lang = window.localStorage.getItem("language"); 3 | let text: string[] = []; 4 | switch (lang) { 5 | case "zh": 6 | case "zh-tw": 7 | text = [ 8 | `如果插件对您有帮助,欢迎打赏!🤩 可以通过`, 9 | `<a>微信、支付宝</a>、`, 10 | `<a href="https://afdian.net/a/Benature-K">⚡️ 爱发电</a>、<a href="https://www.buymeacoffee.com/benature">☕️ Buy Me a Coffee</a>请木一喝杯咖啡。不胜感激!🙇`, 11 | ]; 12 | break; 13 | default: 14 | text = [`If you find this plugin useful and would like to support its development, you can sponsor me via 15 | <a href="https://www.buymeacoffee.com/benature">☕️ Buy Me a Coffee</a>, 16 | <a href="https://afdian.net/a/Benature-K">⚡️ AiFaDian</a>, `, 17 | `<a>WeChat or Alipay</a>. `, 18 | `Any amount is welcome, thank you!`] 19 | break; 20 | } 21 | addDonationElementContent(containerEl, text); 22 | } 23 | 24 | function addDonationElementContent(containerEl: HTMLElement, text: string[]): void { 25 | const donateELdiv = containerEl.createEl("div"); 26 | donateELdiv.setAttribute("style", "text-align: center; margin-top: 5rem; border-top: 0.2px solid grey"); 27 | const textContainerEl = document.createElement('div'); 28 | textContainerEl.setCssProps({ "font-size": "10px", color: "gray", "margin-bottom": "10px", "margin-top": "10px" }); 29 | let textEl1 = textContainerEl.createEl("span"); 30 | let textEl2 = textContainerEl.createEl("span"); 31 | let textEl3 = textContainerEl.createEl("span"); 32 | textEl1.innerHTML = text[0]; 33 | textEl2.innerHTML = text[1]; 34 | textEl3.innerHTML = text[2]; 35 | // donateELa1.appendText("If you find this plugin useful and would like to support its development, you can sponsor me by the button below."); 36 | donateELdiv.appendChild(textContainerEl); 37 | 38 | let centerEl = donateELdiv.createEl("div"); 39 | centerEl.setCssProps({ "display": "flex", "justify-content": "center" }); 40 | // centerEl.createEl("div", { text: "dfsfdsd" }); 41 | let qrcodeEl = centerEl.createEl("img"); 42 | qrcodeEl.setAttribute("src", "https://s2.loli.net/2024/04/01/VtX3vYLobdF6MBc.png"); 43 | qrcodeEl.setCssProps({ display: "none", width: "300px", "margin-bottom": "1rem" }); 44 | 45 | textEl2.addEventListener("click", () => { 46 | qrcodeEl.setCssStyles({ "display": "block", }); 47 | }) 48 | 49 | const parser = new DOMParser(); 50 | const donateELa2 = document.createElement('a'); 51 | donateELa2.setAttribute('href', "https://www.buymeacoffee.com/benature"); 52 | // donateELa2.addClass('advanced-tables-donate-button'); 53 | donateELa2.appendChild(parser.parseFromString(buyMeACoffee, 'text/xml').documentElement); 54 | donateELdiv.appendChild(donateELa2); 55 | } 56 | 57 | 58 | const buyMeACoffee = ` 59 | <svg width="150" height="42" viewBox="0 0 260 73" fill="none" xmlns="http://www.w3.org/2000/svg"> 60 | <path d="M0 11.68C0 5.22932 5.22931 0 11.68 0H248.2C254.651 0 259.88 5.22931 259.88 11.68V61.32C259.88 67.7707 254.651 73 248.2 73H11.68C5.22931 73 0 67.7707 0 61.32V11.68Z" fill="#FFDD00"/> 61 | <path d="M52.2566 24.0078L52.2246 23.9889L52.1504 23.9663C52.1802 23.9915 52.2176 24.0061 52.2566 24.0078Z" fill="#0D0C22"/> 62 | <path d="M52.7248 27.3457L52.6895 27.3556L52.7248 27.3457Z" fill="#0D0C22"/> 63 | <path d="M52.2701 24.0024C52.266 24.0019 52.2619 24.0009 52.258 23.9995C52.2578 24.0022 52.2578 24.0049 52.258 24.0076C52.2624 24.007 52.2666 24.0052 52.2701 24.0024Z" fill="#0D0C22"/> 64 | <path d="M52.2578 24.0094H52.2643V24.0054L52.2578 24.0094Z" fill="#0D0C22"/> 65 | <path d="M52.6973 27.3394L52.7513 27.3086L52.7714 27.2973L52.7897 27.2778C52.7554 27.2926 52.7241 27.3135 52.6973 27.3394Z" fill="#0D0C22"/> 66 | <path d="M52.3484 24.0812L52.2956 24.031L52.2598 24.0115C52.279 24.0454 52.3108 24.0705 52.3484 24.0812Z" fill="#0D0C22"/> 67 | <path d="M39.0684 56.469C39.0262 56.4872 38.9893 56.5158 38.9609 56.552L38.9943 56.5306C39.0169 56.5098 39.0489 56.4853 39.0684 56.469Z" fill="#0D0C22"/> 68 | <path d="M46.7802 54.9518C46.7802 54.9041 46.7569 54.9129 46.7626 55.0826C46.7626 55.0687 46.7683 55.0549 46.7708 55.0417C46.7739 55.0115 46.7764 54.982 46.7802 54.9518Z" fill="#0D0C22"/> 69 | <path d="M45.9844 56.469C45.9422 56.4872 45.9053 56.5158 45.877 56.552L45.9103 56.5306C45.9329 56.5098 45.9649 56.4853 45.9844 56.469Z" fill="#0D0C22"/> 70 | <path d="M33.6307 56.8301C33.5987 56.8023 33.5595 56.784 33.5176 56.7773C33.5515 56.7937 33.5855 56.81 33.6081 56.8226L33.6307 56.8301Z" fill="#0D0C22"/> 71 | <path d="M32.4118 55.6598C32.4068 55.6103 32.3916 55.5624 32.3672 55.519C32.3845 55.5642 32.399 55.6104 32.4106 55.6573L32.4118 55.6598Z" fill="#0D0C22"/> 72 | <path d="M40.623 34.7221C38.9449 35.4405 37.0404 36.2551 34.5722 36.2551C33.5397 36.2531 32.5122 36.1114 31.5176 35.834L33.2247 53.3605C33.2851 54.093 33.6188 54.7761 34.1595 55.2739C34.7003 55.7718 35.4085 56.0482 36.1435 56.048C36.1435 56.048 38.564 56.1737 39.3716 56.1737C40.2409 56.1737 42.8474 56.048 42.8474 56.048C43.5823 56.048 44.2904 55.7716 44.831 55.2737C45.3716 54.7759 45.7052 54.0929 45.7656 53.3605L47.594 33.993C46.7769 33.714 45.9523 33.5286 45.0227 33.5286C43.415 33.5279 42.1196 34.0817 40.623 34.7221Z" fill="white"/> 73 | <path d="M26.2344 27.2449L26.2633 27.2719L26.2821 27.2832C26.2676 27.2688 26.2516 27.2559 26.2344 27.2449Z" fill="#0D0C22"/> 74 | <path d="M55.4906 25.6274L55.2336 24.3307C55.0029 23.1673 54.4793 22.068 53.2851 21.6475C52.9024 21.513 52.468 21.4552 52.1745 21.1768C51.881 20.8983 51.7943 20.4659 51.7264 20.0649C51.6007 19.3289 51.4825 18.5923 51.3537 17.8575C51.2424 17.2259 51.1544 16.5163 50.8647 15.9368C50.4876 15.1586 49.705 14.7036 48.9269 14.4025C48.5282 14.2537 48.1213 14.1278 47.7082 14.0254C45.7642 13.5125 43.7202 13.324 41.7202 13.2165C39.3197 13.084 36.9128 13.1239 34.518 13.3359C32.7355 13.4981 30.8581 13.6942 29.1642 14.3108C28.5451 14.5364 27.9071 14.8073 27.4364 15.2856C26.8587 15.8733 26.6702 16.7821 27.0919 17.515C27.3917 18.0354 27.8996 18.4031 28.4382 18.6463C29.1398 18.9597 29.8726 19.1982 30.6242 19.3578C32.7172 19.8204 34.885 20.0021 37.0233 20.0794C39.3932 20.175 41.767 20.0975 44.1256 19.8474C44.7089 19.7833 45.2911 19.7064 45.8723 19.6168C46.5568 19.5118 46.9961 18.6168 46.7943 17.9933C46.553 17.2479 45.9044 16.9587 45.1709 17.0712C45.0628 17.0882 44.9553 17.1039 44.8472 17.1196L44.7692 17.131C44.5208 17.1624 44.2723 17.1917 44.0238 17.219C43.5105 17.2743 42.9959 17.3195 42.4801 17.3547C41.3249 17.4352 40.1665 17.4722 39.0088 17.4741C37.8712 17.4741 36.7329 17.4421 35.5978 17.3673C35.0799 17.3333 34.5632 17.2902 34.0478 17.2378C33.8134 17.2133 33.5796 17.1875 33.3458 17.1586L33.1233 17.1303L33.0749 17.1234L32.8442 17.0901C32.3728 17.0191 31.9014 16.9374 31.435 16.8387C31.388 16.8283 31.3459 16.8021 31.3157 16.7645C31.2856 16.7269 31.2691 16.6801 31.2691 16.6319C31.2691 16.5837 31.2856 16.5369 31.3157 16.4993C31.3459 16.4617 31.388 16.4356 31.435 16.4251H31.4438C31.848 16.339 32.2553 16.2655 32.6638 16.2014C32.8 16.18 32.9366 16.159 33.0736 16.1385H33.0774C33.3332 16.1215 33.5903 16.0757 33.8448 16.0455C36.0595 15.8151 38.2874 15.7366 40.5128 15.8104C41.5933 15.8419 42.6731 15.9053 43.7485 16.0147C43.9798 16.0386 44.2098 16.0637 44.4399 16.092C44.5279 16.1027 44.6165 16.1153 44.7051 16.1259L44.8836 16.1517C45.404 16.2292 45.9217 16.3233 46.4367 16.4339C47.1997 16.5999 48.1796 16.6539 48.519 17.4898C48.6271 17.7551 48.6761 18.0499 48.7359 18.3283L48.8119 18.6834C48.8139 18.6898 48.8154 18.6963 48.8163 18.7029C48.9961 19.5409 49.176 20.379 49.3562 21.217C49.3694 21.2789 49.3697 21.3429 49.3571 21.4049C49.3445 21.4669 49.3193 21.5257 49.2829 21.5776C49.2466 21.6294 49.2 21.6732 49.146 21.7062C49.092 21.7392 49.0317 21.7608 48.969 21.7695H48.964L48.854 21.7846L48.7453 21.799C48.4009 21.8439 48.056 21.8858 47.7107 21.9247C47.0307 22.0022 46.3496 22.0693 45.6674 22.1259C44.3119 22.2386 42.9536 22.3125 41.5927 22.3477C40.8992 22.3662 40.2059 22.3748 39.5129 22.3735C36.7543 22.3713 33.9981 22.211 31.2578 21.8933C30.9611 21.8581 30.6645 21.8204 30.3678 21.7821C30.5978 21.8116 30.2006 21.7594 30.1202 21.7481C29.9316 21.7217 29.7431 21.6943 29.5545 21.6658C28.9216 21.5709 28.2924 21.454 27.6607 21.3515C26.8971 21.2258 26.1667 21.2887 25.476 21.6658C24.909 21.976 24.4501 22.4518 24.1605 23.0297C23.8626 23.6456 23.7739 24.3163 23.6407 24.9781C23.5074 25.6399 23.3 26.3521 23.3786 27.0315C23.5477 28.4979 24.5728 29.6895 26.0473 29.956C27.4345 30.2074 28.8292 30.4111 30.2276 30.5846C35.7212 31.2574 41.2711 31.3379 46.7818 30.8247C47.2305 30.7828 47.6787 30.7371 48.1262 30.6876C48.266 30.6723 48.4074 30.6884 48.5401 30.7348C48.6729 30.7812 48.7936 30.8566 48.8934 30.9557C48.9932 31.0548 49.0695 31.1749 49.1169 31.3073C49.1642 31.4397 49.1814 31.5811 49.167 31.7209L49.0275 33.0773C48.7463 35.8181 48.4652 38.5587 48.184 41.299C47.8907 44.1769 47.5955 47.0545 47.2984 49.9319C47.2146 50.7422 47.1308 51.5524 47.047 52.3624C46.9666 53.16 46.9552 53.9827 46.8038 54.7709C46.5649 56.0103 45.7258 56.7715 44.5015 57.0499C43.3798 57.3052 42.2339 57.4392 41.0836 57.4497C39.8083 57.4566 38.5336 57.4 37.2583 57.4069C35.897 57.4145 34.2295 57.2887 33.1786 56.2756C32.2553 55.3856 32.1277 53.9921 32.002 52.7872C31.8344 51.192 31.6682 49.5971 31.5036 48.0023L30.5796 39.1344L29.9819 33.3966C29.9718 33.3017 29.9618 33.208 29.9524 33.1125C29.8807 32.428 29.3961 31.758 28.6324 31.7926C27.9788 31.8215 27.2359 32.3771 27.3125 33.1125L27.7557 37.3664L28.672 46.1657C28.9331 48.6652 29.1935 51.165 29.4533 53.6653C29.5036 54.1442 29.5507 54.6244 29.6035 55.1034C29.8908 57.7205 31.8895 59.131 34.3646 59.5282C35.8102 59.7607 37.291 59.8085 38.758 59.8324C40.6386 59.8626 42.538 59.9348 44.3877 59.5942C47.1287 59.0914 49.1853 57.2611 49.4788 54.422C49.5626 53.6024 49.6464 52.7826 49.7302 51.9626C50.0088 49.2507 50.2871 46.5386 50.5649 43.8263L51.4737 34.9641L51.8904 30.9026C51.9112 30.7012 51.9962 30.5118 52.133 30.3625C52.2697 30.2132 52.4509 30.1119 52.6497 30.0736C53.4335 29.9208 54.1827 29.66 54.7402 29.0635C55.6277 28.1138 55.8043 26.8756 55.4906 25.6274ZM26.0071 26.5035C26.019 26.4979 25.997 26.6003 25.9876 26.6481C25.9857 26.5758 25.9895 26.5117 26.0071 26.5035ZM26.0831 27.0918C26.0894 27.0874 26.1083 27.1126 26.1278 27.1428C26.0982 27.1151 26.0794 27.0944 26.0825 27.0918H26.0831ZM26.1579 27.1905C26.185 27.2364 26.1994 27.2653 26.1579 27.1905V27.1905ZM26.3082 27.3125H26.3119C26.3119 27.3169 26.3188 27.3213 26.3214 27.3257C26.3172 27.3208 26.3126 27.3164 26.3075 27.3125H26.3082ZM52.6132 27.1302C52.3317 27.3979 51.9074 27.5224 51.4882 27.5846C46.7868 28.2823 42.0169 28.6355 37.264 28.4796C33.8624 28.3633 30.4967 27.9856 27.129 27.5098C26.799 27.4633 26.4414 27.403 26.2145 27.1597C25.7871 26.7009 25.997 25.777 26.1083 25.2226C26.2101 24.7148 26.405 24.0378 27.009 23.9656C27.9518 23.8549 29.0466 24.2528 29.9794 24.3942C31.1023 24.5656 32.2295 24.7028 33.3609 24.8059C38.1892 25.2459 43.0986 25.1774 47.9056 24.5337C48.7817 24.416 49.6548 24.2792 50.5246 24.1233C51.2996 23.9844 52.1588 23.7236 52.6271 24.5262C52.9482 25.073 52.991 25.8046 52.9413 26.4225C52.926 26.6917 52.8084 26.9448 52.6126 27.1302H52.6132Z" fill="#0D0C22"/> 75 | <path fill-rule="evenodd" clip-rule="evenodd" d="M81.1302 40.1929C80.8556 40.7169 80.4781 41.1732 79.9978 41.5604C79.5175 41.9479 78.9571 42.2633 78.3166 42.5062C77.6761 42.7497 77.0315 42.9131 76.3835 42.9964C75.7352 43.0799 75.106 43.0727 74.4963 42.9735C73.8863 42.8749 73.3674 42.6737 72.9408 42.3695L73.4214 37.3779C73.8633 37.2261 74.4197 37.0703 75.0909 36.9107C75.7619 36.7513 76.452 36.6371 77.1613 36.5689C77.8705 36.5003 78.5412 36.5084 79.1744 36.5917C79.8068 36.6753 80.3065 36.8765 80.6725 37.1958C80.8707 37.378 81.0387 37.5754 81.176 37.7883C81.313 38.0011 81.3969 38.2214 81.4276 38.4493C81.5037 39.0875 81.4047 39.6687 81.1302 40.1929ZM74.153 29.5602C74.4734 29.3627 74.8585 29.1877 75.3083 29.0356C75.7581 28.8841 76.2195 28.7774 76.6923 28.7167C77.1648 28.6562 77.6262 28.6481 78.0763 28.6938C78.5258 28.7395 78.9228 28.8647 79.2659 29.0697C79.6089 29.2751 79.8643 29.5714 80.032 29.9586C80.1997 30.3464 80.2456 30.8365 80.1693 31.429C80.1083 31.9001 79.9211 32.2991 79.6089 32.6256C79.2963 32.9526 78.9147 33.2259 78.4652 33.4462C78.0154 33.6668 77.5388 33.8415 77.0356 33.9702C76.5321 34.0997 76.0477 34.1949 75.5828 34.2553C75.1176 34.3163 74.7137 34.3545 74.3706 34.3692C74.0273 34.3845 73.8021 34.3921 73.6956 34.3921L74.153 29.5602ZM83.6007 36.9676C83.3566 36.4361 83.0287 35.9689 82.6172 35.5658C82.2054 35.1633 81.717 34.8709 81.1531 34.6885C81.3969 34.491 81.6371 34.1795 81.8737 33.7539C82.1099 33.3288 82.3119 32.865 82.4796 32.3636C82.6474 31.8619 82.762 31.357 82.8229 30.8478C82.8836 30.3389 82.8607 29.902 82.7544 29.537C82.4947 28.6256 82.087 27.9114 81.5303 27.3946C80.9734 26.8782 80.3257 26.5211 79.586 26.3233C78.8462 26.1264 78.0304 26.0842 77.1383 26.1981C76.2462 26.312 75.3347 26.5361 74.4049 26.8704C74.4049 26.7946 74.4124 26.7148 74.4278 26.6312C74.4426 26.548 74.4504 26.4604 74.4504 26.369C74.4504 26.1411 74.3361 25.9439 74.1074 25.7765C73.8787 25.6093 73.6155 25.5107 73.3183 25.4801C73.0209 25.45 72.731 25.5142 72.4489 25.6738C72.1665 25.8334 71.9721 26.1264 71.8656 26.5511C71.7434 27.9189 71.6215 29.3398 71.4996 30.8134C71.3774 32.2875 71.248 33.7767 71.1107 35.2812C70.9735 36.7855 70.8362 38.2784 70.6989 39.7598C70.5616 41.2414 70.4244 42.6659 70.2871 44.0333C70.333 44.4436 70.4473 44.7629 70.6304 44.9907C70.8133 45.2189 71.0268 45.3556 71.2709 45.401C71.5147 45.4467 71.7704 45.4045 72.0371 45.2755C72.3038 45.1469 72.5365 44.9222 72.735 44.6032C73.3447 44.9375 74.0311 45.1541 74.7938 45.253C75.5561 45.3516 76.3298 45.3516 77.1157 45.253C77.9007 45.1541 78.6747 44.9682 79.4374 44.6943C80.1997 44.4211 80.8936 44.079 81.519 43.669C82.1441 43.2586 82.6703 42.7911 83.0975 42.2671C83.5244 41.7426 83.8065 41.1767 83.9437 40.5691C84.081 39.946 84.119 39.3231 84.0581 38.7C83.9971 38.0771 83.8445 37.5 83.6007 36.9676Z" fill="#0D0C23"/> 76 | <path fill-rule="evenodd" clip-rule="evenodd" d="M105.915 49.0017C105.832 49.5031 105.713 50.0311 105.561 50.586C105.408 51.1403 105.229 51.6458 105.023 52.1018C104.818 52.5575 104.589 52.9256 104.337 53.207C104.085 53.488 103.815 53.606 103.525 53.5606C103.296 53.5297 103.151 53.3854 103.091 53.1274C103.029 52.8686 103.029 52.5497 103.091 52.17C103.151 51.7901 103.269 51.3607 103.445 50.8821C103.62 50.4035 103.834 49.9284 104.085 49.4577C104.337 48.9864 104.623 48.5347 104.943 48.1015C105.264 47.6686 105.599 47.3075 105.95 47.0189C106.026 47.11 106.06 47.3378 106.053 47.7028C106.045 48.0674 105.999 48.5006 105.915 49.0017ZM113.67 39.1097C113.464 38.8819 113.213 38.7529 112.915 38.7223C112.618 38.6919 112.317 38.859 112.012 39.2237C111.813 39.5883 111.562 39.9379 111.257 40.2722C110.952 40.6067 110.635 40.9103 110.307 41.1839C109.98 41.4572 109.667 41.6931 109.37 41.8903C109.072 42.0881 108.84 42.2324 108.672 42.3235C108.611 41.8374 108.576 41.3132 108.569 40.7507C108.561 40.1886 108.573 39.619 108.603 39.0415C108.649 38.2209 108.744 37.393 108.889 36.557C109.034 35.7213 109.244 34.9007 109.518 34.0951C109.518 33.67 109.419 33.3242 109.221 33.0582C109.022 32.7924 108.782 32.625 108.5 32.5567C108.218 32.4885 107.929 32.5264 107.631 32.6707C107.334 32.8153 107.078 33.0775 106.865 33.4569C106.682 33.9586 106.472 34.5207 106.236 35.1436C105.999 35.7667 105.732 36.4012 105.435 37.0469C105.138 37.6931 104.806 38.3197 104.44 38.9273C104.074 39.5354 103.674 40.075 103.239 40.5457C102.804 41.0168 102.331 41.3854 101.821 41.6512C101.31 41.9172 100.757 42.0349 100.162 42.0045C99.8876 41.9285 99.6893 41.7235 99.5675 41.3889C99.4453 41.0549 99.373 40.6368 99.3504 40.1354C99.3275 39.634 99.3504 39.0831 99.4189 38.4828C99.4877 37.8828 99.5791 37.2863 99.6934 36.6938C99.8078 36.101 99.9337 35.5389 100.071 35.0071C100.208 34.4753 100.337 34.0268 100.46 33.6622C100.643 33.2218 100.643 32.8529 100.46 32.5567C100.277 32.2604 100.025 32.0631 99.705 31.964C99.3846 31.8654 99.0489 31.8694 98.6983 31.9755C98.3474 32.0819 98.0958 32.3173 97.9435 32.682C97.684 33.3054 97.4475 34.004 97.2342 34.779C97.0206 35.5539 96.8491 36.3558 96.7197 37.1836C96.5896 38.0121 96.5171 38.8327 96.502 39.6456C96.5011 39.6985 96.5037 39.7488 96.5034 39.8014C96.1709 40.6848 95.854 41.3525 95.553 41.7992C95.1641 42.377 94.7253 42.6277 94.2375 42.5513C94.0236 42.4603 93.8832 42.2477 93.8147 41.9132C93.7453 41.5792 93.7227 41.1689 93.7453 40.6822C93.7688 40.1964 93.826 39.6456 93.9171 39.0299C94.0091 38.4146 94.1229 37.7764 94.2601 37.1154C94.3977 36.4541 94.5425 35.7899 94.6949 35.121C94.8472 34.4525 94.9845 33.8218 95.107 33.2291C95.0916 32.6973 94.9352 32.291 94.6377 32.0097C94.3405 31.7289 93.9247 31.6187 93.3913 31.6791C93.0253 31.8312 92.7542 32.029 92.579 32.2719C92.4034 32.5148 92.2623 32.8265 92.1558 33.2062C92.0946 33.404 92.0032 33.799 91.8813 34.3918C91.7591 34.984 91.603 35.6644 91.4123 36.4315C91.2217 37.1992 90.9967 38.0005 90.7376 38.8362C90.4781 39.6719 90.1885 40.4283 89.8684 41.1041C89.548 41.7801 89.1972 42.3235 88.8161 42.7338C88.4348 43.1438 88.023 43.3113 87.5807 43.2352C87.3366 43.1895 87.1805 42.9388 87.112 42.4831C87.0432 42.0271 87.0319 41.4653 87.0775 40.7964C87.1233 40.1279 87.2148 39.3946 87.352 38.5971C87.4893 37.7993 87.63 37.0434 87.7752 36.3289C87.92 35.6149 88.0535 34.984 88.1756 34.4372C88.2975 33.8901 88.3814 33.5254 88.4272 33.3433C88.4272 32.9026 88.3277 32.5495 88.1298 32.2832C87.9313 32.0178 87.6913 31.8503 87.4092 31.7818C87.1268 31.7136 86.8372 31.7514 86.54 31.8957C86.2426 32.0403 85.9872 32.3026 85.7736 32.682C85.6973 33.0923 85.598 33.5674 85.4761 34.1067C85.3539 34.6459 85.2361 35.2006 85.1218 35.7705C85.0074 36.3404 84.9003 36.8988 84.8014 37.4459C84.7021 37.993 84.6299 38.4716 84.584 38.8819C84.5536 39.2008 84.519 39.5923 84.4813 40.0556C84.443 40.5194 84.4238 41.0092 84.4238 41.5257C84.4238 42.0427 84.4618 42.5554 84.5385 43.0643C84.6145 43.5735 84.7518 44.0408 84.95 44.4659C85.1482 44.8915 85.4265 45.2408 85.7852 45.5144C86.1433 45.7879 86.5972 45.9397 87.1463 45.9704C87.7101 46.0005 88.202 45.9591 88.6217 45.8449C89.041 45.731 89.4221 45.5523 89.7654 45.3091C90.1084 45.0665 90.421 44.7776 90.7033 44.443C90.9851 44.1091 91.2637 43.7444 91.5383 43.3491C91.7974 43.9269 92.1329 44.3748 92.5447 44.694C92.9565 45.013 93.3913 45.2032 93.8486 45.2637C94.306 45.3241 94.7715 45.2602 95.2442 45.0699C95.7167 44.8803 96.1436 44.5573 96.5252 44.1012C96.7762 43.8216 97.0131 43.5038 97.2354 43.1525C97.3297 43.317 97.4301 43.4758 97.543 43.6224C97.9168 44.1091 98.424 44.443 99.0645 44.6255C99.7506 44.808 100.421 44.8386 101.077 44.7169C101.733 44.5954 102.358 44.3748 102.953 44.0559C103.548 43.7366 104.101 43.3532 104.612 42.9047C105.122 42.4565 105.568 41.9895 105.95 41.5028C105.934 41.8524 105.927 42.1832 105.927 42.4944C105.927 42.8061 105.919 43.1438 105.904 43.5088C105.141 44.0408 104.421 44.679 103.742 45.4233C103.064 46.1676 102.469 46.9616 101.958 47.8051C101.447 48.6483 101.047 49.5031 100.757 50.3691C100.467 51.2357 100.326 52.0445 100.334 52.7969C100.341 53.549 100.521 54.206 100.871 54.7681C101.222 55.3306 101.794 55.7331 102.587 55.9763C103.411 56.2348 104.135 56.242 104.76 55.9991C105.386 55.7559 105.931 55.3531 106.396 54.791C106.861 54.2289 107.242 53.549 107.54 52.7512C107.837 51.9534 108.073 51.1215 108.249 50.2555C108.424 49.3894 108.535 48.5379 108.58 47.7028C108.626 46.8668 108.626 46.1219 108.58 45.4687C109.892 44.9219 110.967 44.2305 111.806 43.3945C112.645 42.5594 113.338 41.6778 113.887 40.7507C114.055 40.5229 114.112 40.2493 114.059 39.9304C114.006 39.6111 113.876 39.3376 113.67 39.1097Z" fill="#0D0C23"/> 77 | <path fill-rule="evenodd" clip-rule="evenodd" d="M142.53 37.6515C142.575 37.3022 142.644 36.9335 142.735 36.546C142.827 36.1585 142.941 35.7823 143.079 35.4177C143.216 35.0531 143.376 34.7379 143.559 34.4718C143.742 34.2061 143.937 34.0161 144.142 33.9019C144.348 33.7883 144.558 33.7995 144.771 33.936C145 34.0731 145.141 34.3617 145.195 34.8021C145.248 35.2433 145.195 35.7141 145.034 36.2155C144.874 36.7172 144.588 37.1879 144.177 37.6286C143.765 38.0696 143.208 38.3579 142.507 38.4947C142.476 38.2824 142.484 38.0011 142.53 37.6515ZM150.456 38.5857C150.204 38.5103 149.964 38.5025 149.735 38.5632C149.506 38.6239 149.361 38.7835 149.301 39.042C149.178 39.5281 148.984 40.0258 148.717 40.5347C148.45 41.0439 148.122 41.5262 147.734 41.9822C147.345 42.438 146.906 42.8408 146.418 43.1901C145.93 43.5397 145.419 43.7904 144.886 43.9422C144.351 44.1096 143.91 44.1284 143.559 43.9991C143.208 43.8705 142.93 43.6498 142.724 43.3384C142.518 43.027 142.369 42.6508 142.278 42.2101C142.186 41.7694 142.133 41.3137 142.118 40.8424C142.987 40.9034 143.761 40.7478 144.44 40.3751C145.118 40.0032 145.694 39.509 146.167 38.8937C146.639 38.2784 146.998 37.587 147.242 36.8195C147.485 36.0524 147.623 35.2887 147.653 34.5288C147.669 33.8146 147.562 33.2108 147.333 32.7169C147.105 32.2233 146.796 31.839 146.407 31.5658C146.018 31.2922 145.572 31.1326 145.069 31.0872C144.566 31.0415 144.054 31.11 143.536 31.2922C142.91 31.505 142.381 31.8506 141.946 32.3294C141.512 32.808 141.149 33.3629 140.86 33.9933C140.57 34.6239 140.341 35.3038 140.173 36.033C140.005 36.7626 139.883 37.4806 139.807 38.1873C139.739 38.8214 139.702 39.4278 139.689 40.013C139.657 40.0874 139.625 40.1588 139.59 40.2383C139.354 40.7782 139.079 41.3062 138.766 41.8226C138.454 42.3394 138.107 42.7725 137.726 43.1218C137.344 43.4714 136.948 43.5929 136.536 43.4865C136.292 43.426 136.159 43.1444 136.136 42.6433C136.113 42.1416 136.139 41.5187 136.216 40.7741C136.292 40.0298 136.38 39.2239 136.479 38.3579C136.578 37.4918 136.628 36.664 136.628 35.8737C136.628 35.1898 136.498 34.5329 136.239 33.9019C135.979 33.2718 135.625 32.7473 135.175 32.3294C134.725 31.9113 134.203 31.634 133.608 31.4975C133.013 31.3605 132.373 31.4518 131.687 31.7708C131 32.09 130.455 32.5382 130.051 33.1157C129.647 33.6934 129.277 34.3009 128.942 34.9391C128.819 34.4528 128.641 34.0011 128.404 33.583C128.167 33.1651 127.878 32.8005 127.535 32.4888C127.191 32.1776 126.806 31.9344 126.38 31.7595C125.953 31.5851 125.502 31.4975 125.03 31.4975C124.572 31.4975 124.149 31.5851 123.76 31.7595C123.371 31.9344 123.017 32.1583 122.696 32.4318C122.376 32.7056 122.087 33.013 121.827 33.3551C121.568 33.6969 121.339 34.0352 121.141 34.3692C121.11 33.9742 121.076 33.6286 121.038 33.332C121 33.0359 120.931 32.7852 120.832 32.5801C120.733 32.3748 120.592 32.2193 120.409 32.1129C120.226 32.0067 119.967 31.9532 119.632 31.9532C119.464 31.9532 119.296 31.9874 119.128 32.0556C118.96 32.1241 118.811 32.2193 118.682 32.3407C118.552 32.4627 118.453 32.6105 118.385 32.7852C118.316 32.9598 118.297 33.1614 118.327 33.3892C118.342 33.5566 118.385 33.7576 118.453 33.9933C118.522 34.2289 118.587 34.5369 118.648 34.9163C118.708 35.2962 118.758 35.756 118.796 36.2953C118.834 36.8349 118.846 37.4959 118.831 38.2784C118.815 39.0611 118.758 39.9763 118.659 41.0248C118.56 42.0733 118.403 43.289 118.19 44.6714C118.16 44.9907 118.282 45.2492 118.556 45.4467C118.831 45.6439 119.143 45.7578 119.494 45.7885C119.845 45.8188 120.177 45.7578 120.489 45.6063C120.802 45.4539 120.981 45.1882 121.027 44.8085C121.072 44.0943 121.16 43.3347 121.29 42.529C121.419 41.724 121.579 40.9262 121.77 40.1359C121.961 39.346 122.178 38.5938 122.422 37.8793C122.666 37.1651 122.937 36.5347 123.234 35.9876C123.532 35.4405 123.84 35.0039 124.161 34.6771C124.481 34.3504 124.816 34.187 125.167 34.187C125.594 34.187 125.926 34.3805 126.162 34.7679C126.398 35.1557 126.566 35.6536 126.666 36.2609C126.765 36.869 126.81 37.5341 126.803 38.2555C126.795 38.9773 126.765 39.6724 126.711 40.341C126.658 41.0098 126.597 41.606 126.528 42.1303C126.46 42.6545 126.41 43.0157 126.38 43.2129C126.38 43.5625 126.513 43.8395 126.78 44.0448C127.046 44.2498 127.344 44.3716 127.672 44.4095C128 44.4476 128.309 44.3866 128.598 44.227C128.888 44.0674 129.056 43.7982 129.102 43.4179C129.254 42.324 129.464 41.2264 129.731 40.1247C129.997 39.023 130.303 38.0355 130.646 37.1616C130.989 36.2878 131.37 35.5735 131.79 35.0189C132.209 34.4646 132.655 34.187 133.128 34.187C133.371 34.187 133.559 34.3544 133.688 34.6884C133.818 35.0227 133.883 35.4784 133.883 36.0559C133.883 36.4815 133.848 36.9184 133.78 37.3666C133.711 37.8148 133.631 38.2784 133.54 38.7569C133.448 39.2358 133.368 39.7256 133.299 40.227C133.231 40.7287 133.196 41.2527 133.196 41.7998C133.196 42.1797 133.235 42.6204 133.311 43.1218C133.387 43.6229 133.532 44.0983 133.745 44.5462C133.959 44.9947 134.252 45.3744 134.626 45.6858C135 45.9973 135.476 46.1531 136.056 46.1531C136.925 46.1531 137.695 45.9669 138.366 45.5947C139.037 45.2226 139.613 44.7365 140.093 44.1362C140.118 44.1047 140.141 44.0711 140.165 44.0399C140.202 44.1287 140.235 44.2227 140.276 44.3071C140.604 44.9756 141.05 45.4921 141.615 45.857C142.178 46.2216 142.842 46.4229 143.605 46.4611C144.367 46.4987 145.198 46.3581 146.098 46.0392C146.769 45.796 147.352 45.4921 147.848 45.1275C148.343 44.7628 148.789 44.3184 149.186 43.7941C149.583 43.2699 149.945 42.6658 150.273 41.9822C150.601 41.2981 150.932 40.5159 151.268 39.6342C151.329 39.3916 151.272 39.1751 151.097 38.9848C150.921 38.7951 150.708 38.6621 150.456 38.5857Z" fill="#0D0C23"/> 78 | <path fill-rule="evenodd" clip-rule="evenodd" d="M162.887 36.0434C162.81 36.4918 162.707 36.986 162.578 37.525C162.448 38.0646 162.284 38.623 162.086 39.2004C161.888 39.7779 161.644 40.2984 161.354 40.7616C161.064 41.2254 160.733 41.5935 160.359 41.8671C159.985 42.1406 159.555 42.2546 159.066 42.2089C158.822 42.1788 158.635 42.0117 158.506 41.7075C158.376 41.4038 158.308 41.0161 158.3 40.545C158.292 40.0743 158.334 39.5575 158.426 38.9951C158.517 38.4333 158.658 37.8821 158.849 37.3426C159.04 36.8036 159.272 36.3056 159.547 35.8496C159.821 35.3939 160.138 35.0405 160.496 34.7898C160.854 34.5391 161.247 34.4217 161.674 34.4365C162.101 34.4518 162.559 34.6643 163.047 35.0747C163.016 35.2725 162.963 35.5954 162.887 36.0434ZM171.019 37.787C170.782 37.6656 170.538 37.6392 170.287 37.7075C170.035 37.7757 169.856 38.0076 169.749 38.4026C169.688 38.8283 169.551 39.3294 169.338 39.9069C169.124 40.4843 168.861 41.0317 168.548 41.5478C168.236 42.0646 167.877 42.494 167.473 42.8358C167.069 43.1778 166.638 43.3337 166.181 43.3028C165.799 43.2727 165.532 43.079 165.38 42.7218C165.227 42.3647 165.147 41.9168 165.14 41.3769C165.132 40.838 165.186 40.2301 165.3 39.5538C165.414 38.8777 165.552 38.2054 165.712 37.5363C165.872 36.868 166.036 36.2258 166.204 35.6105C166.371 34.9951 166.508 34.4747 166.616 34.0493C166.738 33.6693 166.699 33.3466 166.501 33.0803C166.303 32.8149 166.055 32.6246 165.758 32.5107C165.46 32.3967 165.159 32.3664 164.854 32.4196C164.549 32.4728 164.351 32.6362 164.259 32.9094C163.359 32.1345 162.494 31.7166 161.663 31.6559C160.831 31.5952 160.065 31.7776 159.364 32.203C158.662 32.6284 158.041 33.2437 157.5 34.0493C156.958 34.8549 156.52 35.7322 156.184 36.6818C155.849 37.6314 155.639 38.6004 155.555 39.5879C155.471 40.5757 155.536 41.4761 155.75 42.289C155.963 43.1018 156.34 43.7669 156.882 44.283C157.423 44.7998 158.159 45.0583 159.089 45.0583C159.501 45.0583 159.898 44.9747 160.279 44.8076C160.66 44.6401 161.011 44.4426 161.331 44.2148C161.651 43.9869 161.933 43.7475 162.178 43.4968C162.421 43.2461 162.612 43.0373 162.749 42.8699C162.856 43.417 163.032 43.8808 163.276 44.2605C163.519 44.6401 163.798 44.9521 164.111 45.1948C164.423 45.4376 164.751 45.6164 165.094 45.7306C165.437 45.8445 165.769 45.9015 166.089 45.9015C166.806 45.9015 167.477 45.6583 168.102 45.1719C168.727 44.6861 169.288 44.0893 169.784 43.3829C170.279 42.6762 170.687 41.9319 171.007 41.1491C171.328 40.3666 171.541 39.6715 171.648 39.0634C171.755 38.8355 171.735 38.5964 171.591 38.3457C171.446 38.095 171.255 37.909 171.019 37.787Z" fill="#0D0C23"/> 79 | <path fill-rule="evenodd" clip-rule="evenodd" d="M212.194 50.3701C212.064 50.8866 211.862 51.3238 211.587 51.6806C211.313 52.0377 210.97 52.2239 210.558 52.2393C210.299 52.2543 210.101 52.1175 209.963 51.8289C209.826 51.5401 209.731 51.1679 209.678 50.7122C209.624 50.2562 209.601 49.747 209.609 49.1849C209.616 48.6227 209.639 48.0681 209.678 47.521C209.715 46.9742 209.761 46.4647 209.815 45.9939C209.868 45.5226 209.91 45.1586 209.94 44.9C210.459 44.9608 210.89 45.1846 211.233 45.5723C211.576 45.9598 211.839 46.4193 212.022 46.9514C212.205 47.4831 212.312 48.0568 212.343 48.6722C212.373 49.2875 212.323 49.8534 212.194 50.3701ZM203.913 50.3701C203.783 50.8866 203.581 51.3238 203.307 51.6806C203.032 52.0377 202.689 52.2239 202.277 52.2393C202.018 52.2543 201.82 52.1175 201.683 51.8289C201.545 51.5401 201.45 51.1679 201.397 50.7122C201.343 50.2562 201.32 49.747 201.328 49.1849C201.336 48.6227 201.358 48.0681 201.397 47.521C201.434 46.9742 201.48 46.4647 201.534 45.9939C201.587 45.5226 201.629 45.1586 201.66 44.9C202.178 44.9608 202.609 45.1846 202.952 45.5723C203.295 45.9598 203.558 46.4193 203.741 46.9514C203.924 47.4831 204.031 48.0568 204.062 48.6722C204.092 49.2875 204.042 49.8534 203.913 50.3701ZM195.415 37.4241C195.399 37.7884 195.365 38.1114 195.312 38.3925C195.258 38.6741 195.186 38.8522 195.095 38.9283C194.927 38.8369 194.721 38.6018 194.477 38.2216C194.233 37.8419 194.042 37.4122 193.905 36.9336C193.768 36.4551 193.725 35.9843 193.779 35.5205C193.832 35.0573 194.073 34.6967 194.5 34.4379C194.667 34.3468 194.812 34.3809 194.934 34.5405C195.056 34.7001 195.155 34.9318 195.232 35.2357C195.308 35.5399 195.361 35.8892 195.392 36.2842C195.422 36.6795 195.43 37.0591 195.415 37.4241ZM193.39 41.9711C193.154 42.2215 192.89 42.4381 192.601 42.6206C192.311 42.803 192.014 42.9398 191.709 43.0309C191.404 43.1223 191.129 43.1448 190.885 43.0991C190.199 42.9627 189.673 42.666 189.307 42.2103C188.941 41.7545 188.708 41.219 188.609 40.6037C188.51 39.9881 188.521 39.3308 188.644 38.6319C188.765 37.933 188.971 37.2835 189.261 36.6832C189.551 36.0829 189.902 35.5662 190.313 35.1333C190.725 34.7001 191.175 34.4306 191.663 34.3239C191.48 35.0989 191.419 35.9007 191.48 36.7286C191.541 37.5568 191.739 38.3355 192.075 39.0648C192.288 39.506 192.544 39.9082 192.841 40.2729C193.139 40.6378 193.501 40.9492 193.928 41.2075C193.806 41.466 193.626 41.7204 193.39 41.9711ZM218.702 37.6519C218.747 37.3026 218.816 36.9336 218.908 36.5462C218.999 36.159 219.114 35.7828 219.251 35.4181C219.388 35.0532 219.548 34.738 219.731 34.4723C219.914 34.2065 220.108 34.0163 220.314 33.9024C220.52 33.7884 220.73 33.7997 220.943 33.9365C221.172 34.0735 221.313 34.3621 221.367 34.8025C221.42 35.2435 221.367 35.7142 221.207 36.2159C221.046 36.7173 220.761 37.1884 220.349 37.6288C219.937 38.07 219.38 38.3583 218.679 38.4951C218.648 38.2826 218.656 38.0015 218.702 37.6519ZM227.921 37.6519C227.966 37.3026 228.035 36.9336 228.126 36.5462C228.218 36.159 228.332 35.7828 228.47 35.4181C228.607 35.0532 228.767 34.738 228.95 34.4723C229.133 34.2065 229.328 34.0163 229.533 33.9024C229.739 33.7884 229.949 33.7997 230.162 33.9365C230.391 34.0735 230.532 34.3621 230.586 34.8025C230.639 35.2435 230.586 35.7142 230.425 36.2159C230.265 36.7173 229.979 37.1884 229.568 37.6288C229.156 38.07 228.599 38.3583 227.898 38.4951C227.867 38.2826 227.875 38.0015 227.921 37.6519ZM236.488 38.9852C236.312 38.7955 236.099 38.6625 235.847 38.5862C235.595 38.5104 235.355 38.5029 235.126 38.5636C234.897 38.6244 234.752 38.784 234.692 39.0422C234.57 39.5286 234.375 40.0262 234.108 40.5349C233.841 41.0444 233.514 41.5267 233.125 41.9824C232.736 42.4381 232.297 42.8412 231.81 43.1905C231.321 43.5401 230.81 43.7908 230.277 43.9423C229.743 44.1101 229.301 44.1289 228.95 43.9996C228.599 43.8706 228.321 43.6503 228.115 43.3389C227.909 43.0271 227.761 42.6512 227.669 42.2103C227.578 41.7699 227.524 41.3142 227.509 40.8428C228.378 40.9038 229.152 40.7483 229.831 40.3755C230.509 40.0034 231.085 39.5092 231.558 38.8939C232.031 38.2788 232.389 37.5874 232.633 36.82C232.877 36.0526 233.014 35.2892 233.045 34.5293C233.06 33.815 232.953 33.211 232.724 32.7171C232.496 32.2235 232.187 31.8395 231.798 31.5662C231.409 31.2924 230.963 31.133 230.46 31.0874C229.957 31.0417 229.445 31.1105 228.927 31.2924C228.302 31.5055 227.772 31.851 227.338 32.3296C226.903 32.8085 226.54 33.3634 226.251 33.9934C225.961 34.6244 225.732 35.3039 225.564 36.0335C225.396 36.7627 225.274 37.481 225.199 38.1874C225.124 38.873 225.084 39.5292 225.075 40.1572C225.017 40.2824 224.956 40.4082 224.889 40.5349C224.622 41.0444 224.295 41.5267 223.906 41.9824C223.517 42.4381 223.078 42.8412 222.591 43.1905C222.102 43.5401 221.592 43.7908 221.058 43.9423C220.524 44.1101 220.082 44.1289 219.731 43.9996C219.38 43.8706 219.102 43.6503 218.896 43.3389C218.691 43.0271 218.542 42.6512 218.45 42.2103C218.359 41.7699 218.305 41.3142 218.29 40.8428C219.159 40.9038 219.933 40.7483 220.612 40.3755C221.29 40.0034 221.866 39.5092 222.339 38.8939C222.811 38.2788 223.17 37.5874 223.414 36.82C223.658 36.0526 223.795 35.2892 223.826 34.5293C223.841 33.815 223.734 33.211 223.506 32.7171C223.277 32.2235 222.968 31.8395 222.579 31.5662C222.19 31.2924 221.744 31.133 221.241 31.0874C220.738 31.0417 220.227 31.1105 219.708 31.2924C219.083 31.5055 218.553 31.851 218.119 32.3296C217.684 32.8085 217.321 33.3634 217.032 33.9934C216.742 34.6244 216.513 35.3039 216.346 36.0335C216.178 36.7627 216.056 37.481 215.98 38.1874C215.936 38.5859 215.907 38.9722 215.886 39.3516C215.739 39.4765 215.595 39.6023 215.442 39.7258C214.916 40.1514 214.363 40.5349 213.784 40.8769C213.204 41.219 212.601 41.5001 211.977 41.7204C211.351 41.9408 210.71 42.0738 210.055 42.1192L211.473 26.9847C211.565 26.6655 211.519 26.3847 211.336 26.1415C211.153 25.8983 210.916 25.7312 210.627 25.6401C210.337 25.5488 210.028 25.5566 209.7 25.6627C209.372 25.7694 209.102 26.0126 208.888 26.3919C208.781 26.9697 208.671 27.7597 208.557 28.7625C208.442 29.7653 208.328 30.8595 208.213 32.0448C208.099 33.23 207.985 34.4532 207.87 35.7142C207.756 36.9759 207.657 38.1533 207.573 39.2472C207.569 39.2958 207.566 39.3398 207.562 39.3878C207.429 39.5005 207.299 39.6142 207.161 39.7258C206.635 40.1514 206.082 40.5349 205.503 40.8769C204.923 41.219 204.321 41.5001 203.696 41.7204C203.07 41.9408 202.429 42.0738 201.774 42.1192L203.192 26.9847C203.284 26.6655 203.238 26.3847 203.055 26.1415C202.872 25.8983 202.635 25.7312 202.346 25.6401C202.056 25.5488 201.747 25.5566 201.419 25.6627C201.091 25.7694 200.821 26.0126 200.607 26.3919C200.501 26.9697 200.39 27.7597 200.276 28.7625C200.161 29.7653 200.047 30.8595 199.933 32.0448C199.818 33.23 199.704 34.4532 199.589 35.7142C199.475 36.9759 199.376 38.1533 199.292 39.2472C199.29 39.2692 199.289 39.2891 199.287 39.3111C199.048 39.4219 198.786 39.519 198.503 39.6006C198.213 39.6844 197.885 39.7339 197.519 39.7489C197.58 39.4751 197.63 39.1712 197.668 38.8369C197.706 38.5029 197.737 38.1533 197.76 37.7884C197.782 37.4241 197.79 37.0591 197.782 36.6945C197.774 36.3296 197.755 35.9956 197.725 35.6914C197.649 35.0385 197.508 34.4191 197.302 33.8338C197.096 33.2491 196.818 32.7593 196.467 32.3637C196.116 31.9687 195.678 31.7027 195.151 31.5662C194.626 31.4294 194.012 31.4748 193.31 31.7027C192.273 31.5662 191.339 31.6613 190.508 31.9878C189.677 32.3149 188.956 32.7894 188.346 33.4122C187.736 34.0357 187.237 34.7684 186.848 35.6119C186.459 36.4551 186.2 37.3214 186.07 38.21C186.015 38.5868 185.988 38.9618 185.98 39.336C185.744 39.8177 185.486 40.2388 185.201 40.5921C184.797 41.0935 184.377 41.5038 183.943 41.8228C183.508 42.142 183.077 42.3852 182.65 42.5523C182.223 42.7198 181.842 42.8337 181.507 42.8941C181.11 42.9702 180.729 42.978 180.363 42.917C179.997 42.8565 179.661 42.6816 179.357 42.3927C179.112 42.1802 178.925 41.8381 178.796 41.3671C178.666 40.896 178.59 40.3608 178.567 39.7602C178.544 39.1599 178.567 38.533 178.636 37.8798C178.705 37.2266 178.822 36.6072 178.99 36.0222C179.158 35.4372 179.371 34.913 179.631 34.4492C179.89 33.9862 180.195 33.6554 180.546 33.4579C180.744 33.4886 180.866 33.606 180.912 33.811C180.958 34.0163 180.969 34.2595 180.946 34.5405C180.923 34.8219 180.889 35.1105 180.843 35.4066C180.797 35.703 180.775 35.9502 180.775 36.1474C180.851 36.5577 180.999 36.877 181.221 37.1048C181.441 37.3327 181.69 37.466 181.964 37.5036C182.239 37.5417 182.509 37.4773 182.776 37.3098C183.043 37.143 183.26 36.877 183.428 36.512C183.443 36.5274 183.466 36.5349 183.497 36.5349L183.817 33.6404C183.909 33.2451 183.847 32.8958 183.634 32.5919C183.42 32.288 183.138 32.113 182.788 32.0676C182.345 31.4294 181.747 31.0914 180.992 31.0532C180.237 31.0154 179.463 31.2623 178.67 31.7941C178.182 32.144 177.751 32.626 177.378 33.2413C177.004 33.857 176.699 34.5405 176.463 35.2926C176.226 36.0448 176.058 36.8391 175.959 37.6748C175.86 38.5104 175.841 39.3236 175.902 40.1133C175.963 40.9038 176.104 41.6484 176.325 42.347C176.546 43.0462 176.855 43.6312 177.252 44.102C177.587 44.5123 177.968 44.8127 178.395 45.0027C178.822 45.1927 179.268 45.3101 179.734 45.3558C180.199 45.4012 180.66 45.3821 181.118 45.2988C181.575 45.2155 182.01 45.0978 182.421 44.9454C182.955 44.7482 183.505 44.4972 184.069 44.1933C184.633 43.8897 185.174 43.5248 185.693 43.0991C185.966 42.8753 186.228 42.6313 186.482 42.3696C186.598 42.6553 186.727 42.9317 186.882 43.1905C187.294 43.8741 187.85 44.429 188.552 44.8544C189.253 45.2797 190.115 45.4844 191.137 45.4697C192.235 45.4544 193.249 45.1774 194.18 44.6378C195.11 44.0988 195.872 43.3042 196.467 42.256C197.358 42.256 198.234 42.1096 199.096 41.819C199.089 41.911 199.081 42.0079 199.075 42.0966C199.014 42.9019 198.983 43.4487 198.983 43.7376C198.968 44.239 198.934 44.8581 198.88 45.5949C198.827 46.332 198.793 47.1069 198.778 47.9198C198.763 48.7326 198.793 49.5532 198.869 50.3817C198.945 51.2096 199.105 51.962 199.349 52.6383C199.593 53.3141 199.94 53.8878 200.39 54.3591C200.84 54.8299 201.431 55.1112 202.163 55.2023C202.941 55.3084 203.612 55.1717 204.176 54.792C204.74 54.412 205.198 53.8918 205.549 53.2308C205.899 52.5695 206.147 51.8061 206.292 50.9401C206.437 50.074 206.479 49.2039 206.418 48.3301C206.357 47.4562 206.196 46.6321 205.937 45.8575C205.678 45.0822 205.319 44.444 204.862 43.9423C205.137 43.8669 205.465 43.7226 205.846 43.5095C206.227 43.2969 206.62 43.0575 207.024 42.7915C207.123 42.7261 207.221 42.6573 207.32 42.5902C207.283 43.1286 207.264 43.5126 207.264 43.7376C207.249 44.239 207.215 44.8581 207.161 45.5949C207.108 46.332 207.073 47.1069 207.058 47.9198C207.043 48.7326 207.073 49.5532 207.15 50.3817C207.226 51.2096 207.386 51.962 207.63 52.6383C207.874 53.3141 208.221 53.8878 208.671 54.3591C209.121 54.8299 209.712 55.1112 210.444 55.2023C211.221 55.3084 211.892 55.1717 212.457 54.792C213.021 54.412 213.478 53.8918 213.83 53.2308C214.18 52.5695 214.428 51.8061 214.573 50.9401C214.718 50.074 214.759 49.2039 214.699 48.3301C214.637 47.4562 214.477 46.6321 214.218 45.8575C213.959 45.0822 213.601 44.444 213.143 43.9423C213.418 43.8669 213.745 43.7226 214.127 43.5095C214.508 43.2969 214.9 43.0575 215.305 42.7915C215.515 42.6533 215.724 42.5107 215.932 42.3641C216.01 43.1072 216.179 43.759 216.448 44.3073C216.776 44.9761 217.222 45.4925 217.787 45.8575C218.351 46.2218 219.014 46.4234 219.777 46.4612C220.539 46.4988 221.37 46.3586 222.271 46.0393C222.941 45.7965 223.525 45.4925 224.02 45.1279C224.516 44.763 224.962 44.3185 225.358 43.7946C225.381 43.7642 225.403 43.7313 225.425 43.7006C225.496 43.9134 225.574 44.1179 225.667 44.3073C225.995 44.9761 226.441 45.4925 227.006 45.8575C227.569 46.2218 228.233 46.4234 228.996 46.4612C229.758 46.4988 230.589 46.3586 231.489 46.0393C232.16 45.7965 232.744 45.4925 233.239 45.1279C233.735 44.763 234.181 44.3185 234.577 43.7946C234.974 43.27 235.336 42.666 235.664 41.9824C235.992 41.2985 236.323 40.5164 236.659 39.6347C236.72 39.3918 236.663 39.1752 236.488 38.9852Z" fill="#0D0C23"/> 80 | </svg>`; 81 | 82 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Editor, MarkdownView, Plugin, Notice, debounce, normalizePath, 3 | EditorSelectionOrCaret, EditorRangeOrCaret, EditorChange, EditorPosition, MarkdownFileInfo 4 | } from "obsidian"; 5 | import { removeWikiLink, removeUrlLink, url2WikiLink, convertWikiLinkToMarkdown } from "./link"; 6 | import { TextFormatSettingTab } from "./settings/settingTab"; 7 | import { FormatSettings, DEFAULT_SETTINGS, CalloutTypeDecider, CustomReplaceBuiltIn } from "./settings/types"; 8 | import { array2markdown, table2bullet, capitalizeWord, capitalizeSentence, removeAllSpaces, zoteroNote, textWrapper, replaceLigature, ankiSelection, sortTodo, requestAPI, headingLevel, slugify, snakify, extraDoubleSpaces, toTitleCase, customReplace, convertLatex, camelCase } from "./format"; 9 | import { CustomReplacementBuiltInCommands, GlobalCommands } from "./commands"; 10 | import { getString } from "./langs/langs"; 11 | import { selectionBehavior, FormatSelectionReturn } from "./types"; 12 | import { v4 as uuidv4 } from "uuid"; 13 | import { renew } from "./util"; 14 | 15 | function getLang() { 16 | let lang = window.localStorage.getItem('language'); 17 | if (["en", "zh", "zh-TW"].indexOf(lang) == -1) { lang = "en"; } 18 | return lang; 19 | } 20 | 21 | 22 | export default class TextFormat extends Plugin { 23 | settings: FormatSettings; 24 | debounceUpdateCommandWrapper = debounce(this.updateCommandWrapper, 1000, true); 25 | debounceUpdateCommandRequest = debounce(this.updateCommandRequest, 1000, true); 26 | // memory: TextFormatMemory; 27 | 28 | executeCommandById(cmd: string) { 29 | // @ts-ignore 30 | this.app.commands.executeCommandById(cmd); 31 | } 32 | 33 | async quickFormat(text: string, cmd: string) { 34 | let formatRes = await this.formatSelection(text, cmd); 35 | let res = formatRes.editorChange.text 36 | if (res) 37 | return res; 38 | return text; 39 | } 40 | 41 | async formatGlobal(cmd: string) { 42 | let activeElement = document.activeElement; 43 | const activeClasses = activeElement.classList; 44 | 45 | let where = "editor"; 46 | if (activeClasses.contains("inline-title") || activeClasses.contains("view-header-title")) { 47 | where = "title"; 48 | } else if (activeClasses.contains("metadata-input-longtext")) { 49 | where = "metadata-long-text" 50 | } else if (activeClasses.contains("metadata-property")) { 51 | let longtext = activeElement.querySelector(".metadata-input-longtext"); 52 | if (longtext) { 53 | activeElement = longtext; 54 | where = "metadata-long-text" 55 | } 56 | } 57 | const file = this.app.workspace.getActiveFile(); 58 | 59 | switch (where) { 60 | case "editor": 61 | this.executeCommandById(`obsidian-text-format::private:${cmd}`); 62 | break; 63 | case "title": 64 | const formatResult = await this.formatSelection(file.basename, cmd); 65 | const newName = formatResult.editorChange.text; 66 | const newPath = normalizePath(file.parent.path + "/" + newName + "." + file.extension) 67 | this.app.vault.adapter.rename(file.path, newPath); 68 | break; 69 | case "metadata-long-text": 70 | const activePPElement = activeElement.parentElement.parentElement; 71 | let metadataKey = activePPElement.getAttribute("data-property-key"); 72 | // focus on parent element, so that the new frontmatter can be updated 73 | activePPElement.focus(); 74 | if (!file) 75 | break; 76 | const text = activeElement.textContent; 77 | const replacedText = await this.quickFormat(text, cmd); 78 | await this.app.fileManager.processFrontMatter(file, (fm) => { 79 | fm[metadataKey] = replacedText; 80 | }); 81 | // let keyboardEvent = new KeyboardEvent('keydown', { 82 | // keyCode: 13, code: 'KeyEnter', key: 'Enter' 83 | // }) 84 | // document.activeElement.dispatchEvent(keyboardEvent); 85 | break; 86 | } 87 | } 88 | 89 | async onload() { 90 | await this.loadSettings(); 91 | await this.initCustomSettings(); 92 | this.addSettingTab(new TextFormatSettingTab(this.app, this)); 93 | 94 | const lang = getLang(); 95 | 96 | this.registerEvent(this.app.workspace.on('editor-paste', async (evt: ClipboardEvent, editor: Editor, info: MarkdownView | MarkdownFileInfo) => { 97 | this.log(evt, editor, info) 98 | // refer: https://github.com/kxxt/obsidian-advanced-paste/blob/cfb04918298f14ffa7f04aefa49beaef9a2e8a76/src/main.ts#L220 99 | const isManuallyTriggered = evt == null; // Not triggered by Ctrl+V 100 | if (!isManuallyTriggered && (evt.clipboardData?.getData("application/x-textformat-tag") == "tag")) { 101 | //: Event was triggered by this plugin, don't handle it again 102 | return; 103 | } 104 | // @ts-ignore 105 | let formatOnPasteCmdList = info.metadataEditor.properties.find(m => m.key === "tfFormatOnPaste")?.value; 106 | // console.log(formatOnPasteCmdList) 107 | if (formatOnPasteCmdList === undefined) { 108 | if (this.settings.formatOnSaveSettings.enabled) { 109 | formatOnPasteCmdList = this.settings.formatOnSaveSettings.commandsString.split("\n").map((c) => c.trim().replace(/^[ -]*/g, "")); 110 | } else return; 111 | } 112 | if (formatOnPasteCmdList?.length == 0) return; 113 | 114 | let clipboard = evt.clipboardData.getData('text/html') || evt.clipboardData.getData('text/plain'); 115 | if (!clipboard) { return; } 116 | 117 | evt?.preventDefault(); 118 | 119 | for (let cmd of formatOnPasteCmdList) { 120 | const formatText = (await this.formatSelection(clipboard, cmd)).editorChange.text; 121 | if (formatText) { clipboard = formatText; } 122 | } 123 | // await navigator.clipboard.writeText('Some text to paste'); 124 | 125 | const dat = new DataTransfer(); 126 | dat.setData('text/html', `<pre>${clipboard}</pre>`); 127 | // dat.setData('text/html', clipboard); 128 | dat.setData("application/x-textformat-tag", "tag"); 129 | const e = new ClipboardEvent("paste", { 130 | clipboardData: dat, 131 | }); 132 | // @ts-ignore 133 | await info.currentMode.clipboardManager.handlePaste(e, editor, info); 134 | 135 | if (formatOnPasteCmdList.includes("easy-typing-format")) { 136 | // @ts-ignore 137 | const activePlugins = this.app.plugins.plugins; 138 | if (activePlugins["easy-typing-obsidian"]) { 139 | try { 140 | const pluginEasyTyping = activePlugins["easy-typing-obsidian"] 141 | // console.log(.formatSelectionOrCurLine); 142 | const cursorTo = editor.getCursor("to"); 143 | editor.setSelection(editor.offsetToPos(editor.posToOffset(cursorTo) - clipboard.length), cursorTo); 144 | pluginEasyTyping.formatSelectionOrCurLine(editor, info); 145 | editor.setCursor(editor.getCursor("to")); 146 | } catch (e) { console.error(e) } 147 | } 148 | } 149 | })) 150 | 151 | this.addCommand({ 152 | id: "open-settings", 153 | name: getString(["command", "open-settings"]), 154 | icon: "bolt", 155 | callback: () => { 156 | // @ts-ignore 157 | const settings = this.app.setting; 158 | settings.open(); 159 | settings.openTabById(`obsidian-text-format`); 160 | }, 161 | }); 162 | 163 | GlobalCommands.forEach(command => { 164 | this.addCommand({ 165 | id: command.id, 166 | name: getString(["command", command.id]), 167 | icon: "case-sensitive", 168 | callback: async () => { 169 | await this.formatGlobal(command.id); 170 | }, 171 | }); 172 | this.addCommand({ 173 | id: `:private:${command.id}`, 174 | name: getString(["command", command.id]) + " in editor", 175 | editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => { 176 | if (!checking) { this.editorTextFormat(editor, view, command.id); } 177 | return !checking; 178 | }, 179 | }); 180 | }) 181 | this.addCommand({ 182 | id: "slugify", 183 | name: { en: "Slugify selected text (`-` for space)", zh: "使用 Slugify 格式化选中文本(`-`连字符)", "zh-TW": "使用 Slugify 格式化選取文字(`-`連字符)" }[lang], 184 | icon: "case-sensitive", 185 | editorCallback: (editor: Editor, view: MarkdownView) => { 186 | this.editorTextFormat(editor, view, "slugify"); 187 | }, 188 | }); 189 | this.addCommand({ 190 | id: "snakify", 191 | name: { en: "Snakify selected text (`_` for space)", zh: "使用 Snakify 格式化选中文本(`_`连字符)", "zh-TW": "使用 Snakify 格式化選取文字(`_`連字符)" }[lang], 192 | icon: "case-sensitive", 193 | editorCallback: (editor: Editor, view: MarkdownView) => { 194 | this.editorTextFormat(editor, view, "snakify"); 195 | }, 196 | }); 197 | this.addCommand({ 198 | id: "camel-case-lower", 199 | name: { en: "camelCase selected text", zh: "使用小驼峰格式化选中文本", "zh-TW": "使用小駝峰格式化選取文字" }[lang], 200 | icon: "case-sensitive", 201 | editorCallback: (editor: Editor, view: MarkdownView) => { 202 | this.editorTextFormat(editor, view, "camel-case", { lowerFirst: true }); 203 | }, 204 | }); 205 | this.addCommand({ 206 | id: "camel-case-upper", 207 | name: { en: "CamelCase selected text", zh: "使用大驼峰格式化选中文本", "zh-TW": "使用大駝峰格式化選取文字" }[lang], 208 | icon: "case-sensitive", 209 | editorCallback: (editor: Editor, view: MarkdownView) => { 210 | this.editorTextFormat(editor, view, "camel-case", { lowerFirst: false }); 211 | }, 212 | }); 213 | 214 | this.addCommand({ 215 | id: "heading-upper", 216 | name: getString(["command", "heading-upper"]), 217 | icon: "indent-increase", 218 | editorCallback: (editor: Editor, view: MarkdownView) => { 219 | this.editorTextFormat(editor, view, "heading", { upper: true }); 220 | }, 221 | repeatable: false, 222 | hotkeys: [ 223 | { 224 | modifiers: ["Ctrl", "Shift"], 225 | key: "]", 226 | }], 227 | }); 228 | this.addCommand({ 229 | id: "heading-lower", 230 | name: getString(["command", "heading-lower"]), 231 | icon: "indent-decrease", 232 | editorCallback: (editor: Editor, view: MarkdownView) => { 233 | this.editorTextFormat(editor, view, "heading", { upper: false }); 234 | }, 235 | repeatable: false, 236 | hotkeys: [ 237 | { 238 | modifiers: ["Ctrl", "Shift"], 239 | key: "[", 240 | }], 241 | }); 242 | 243 | this.addCommand({ 244 | id: "convert-bullet-list", 245 | name: { en: "Detect and format bullet list", zh: "识别并格式化无序列表", "zh-TW": "識別並格式化無序清單" }[lang], 246 | icon: "list", 247 | editorCallback: (editor: Editor, view: MarkdownView) => { 248 | this.editorTextFormat(editor, view, "convert-bullet-list"); 249 | }, 250 | }); 251 | this.addCommand({ 252 | id: "convert-ordered-list", 253 | name: { en: "Detect and format ordered list", zh: "识别并格式化有序列表", "zh-TW": "識別並格式化有序清單" }[lang], 254 | icon: "list-ordered", 255 | editorCallback: (editor: Editor, view: MarkdownView) => { 256 | this.editorTextFormat(editor, view, "convert-ordered-list"); 257 | }, 258 | }); 259 | this.addCommand({ 260 | id: "table2bullet", 261 | name: { en: "Convert table to bullet list without header", zh: "将表格转换为无序列表(不含标题)", "zh-TW": "將表格轉換為無序清單(不含標題)" }[lang], 262 | icon: "list", 263 | editorCallback: (editor: Editor, view: MarkdownView) => { 264 | this.editorTextFormat(editor, view, "table2bullet"); 265 | }, 266 | }); 267 | this.addCommand({ 268 | id: "table2bullet-head", 269 | name: { en: "Convert table to bullet list with header", zh: "将表格转换为无序列表(含标题)", "zh-TW": "將表格轉換為無序清單(含標題)" }[lang], 270 | icon: "list", 271 | editorCallback: (editor: Editor, view: MarkdownView) => { 272 | this.editorTextFormat(editor, view, "table2bullet-header"); 273 | }, 274 | }); 275 | this.addCommand({ 276 | id: "sort-todo", 277 | name: { en: "Sort to-do list", zh: "将待办事项列表排序", "zh-TW": "將待辦事項列表排序" }[lang], 278 | icon: "arrow-down-narrow-wide", 279 | editorCallback: (editor: Editor, view: MarkdownView) => { 280 | this.editorTextFormat(editor, view, "sort-todo"); 281 | }, 282 | }); 283 | 284 | this.addCommand({ 285 | id: "remove-wiki-link", 286 | name: { "en": "Remove WikiLinks format in selection", "zh": "移除选中文本中的 WikiLinks 格式", "zh-TW": "移除選取文字中的 WikiLinks 格式" }[lang], 287 | icon: "link-2-off", 288 | editorCallback: (editor: Editor, view: MarkdownView) => { 289 | this.editorTextFormat(editor, view, "remove-wiki-link"); 290 | }, 291 | }); 292 | this.addCommand({ 293 | id: "remove-url-link", 294 | name: { "en": "Remove URL links format in selection", "zh": "移除选中文本中的 URL 链接格式", "zh-TW": "移除選取文字中的 URL 鏈接格式" }[lang], 295 | icon: "link-2-off", 296 | editorCallback: (editor: Editor, view: MarkdownView) => { 297 | this.editorTextFormat(editor, view, "remove-url-link"); 298 | }, 299 | }); 300 | this.addCommand({ 301 | id: "link-url2wiki", 302 | name: { en: "Convert URL links to WikiLinks in selection", zh: "将选中文本中的 URL 链接转换为 WikiLinks 格式", "zh-TW": "將選取文字中的 URL 鏈接轉換為 WikiLinks 格式" }[lang], 303 | icon: "link-2", 304 | editorCallback: (editor: Editor, view: MarkdownView) => { 305 | this.editorTextFormat(editor, view, "link-url2wiki"); 306 | }, 307 | }); 308 | this.addCommand({ 309 | id: "link-wiki2md", 310 | name: { en: "Convert wikiLinks to plain markdown links in selection", zh: "将选中文本中的 WikiLinks 转换为普通 Markdown 链接格式", "zh-TW": "將選取文字中的 WikiLinks 轉換為普通 Markdown 鏈接格式" }[lang], 311 | icon: "link-2", 312 | editorCallback: (editor: Editor, view: MarkdownView) => { 313 | this.editorTextFormat(editor, view, "link-wiki2md"); 314 | }, 315 | }); 316 | 317 | 318 | this.addCommand({ 319 | id: "remove-redundant-spaces", 320 | name: { en: "Remove redundant spaces in selection", zh: "将选中文本中的多余空格移除", "zh-TW": "將選取文字中的多餘空格移除" }[lang], 321 | icon: "space", 322 | editorCallback: (editor: Editor, view: MarkdownView) => { 323 | this.editorTextFormat(editor, view, "remove-redundant-spaces"); 324 | }, 325 | }); 326 | this.addCommand({ 327 | id: "remove-spaces-all", 328 | name: { en: "Remove all spaces in selection", zh: "将选中文本中的所有空格移除", "zh-TW": "將選取文字中的所有空格移除" }[lang], 329 | icon: "space", 330 | editorCallback: (editor: Editor, view: MarkdownView) => { 331 | this.editorTextFormat(editor, view, "spaces-all"); 332 | }, 333 | }); 334 | // this.addCommand({ 335 | // id: "remove-trailing-all", 336 | // name: { en: "Remove trailing spaces in selection", zh: "将选中文本中的所有行末空格移除", "zh-TW": "將選取文字中的所有行尾空格移除" }[lang], 337 | // editorCallback: (editor: Editor, view: MarkdownView) => { 338 | // this.editorTextFormat(editor, view, "trailing-spaces"); 339 | // }, 340 | // }); 341 | // this.addCommand({ 342 | // id: "remove-blank-line", 343 | // name: { en: "Remove blank line(s)", zh: "将选中文本中的空行移除", "zh-TW": "將選取文字中的空行移除" }[lang], 344 | // editorCallback: (editor: Editor, view: MarkdownView) => { 345 | // this.editorTextFormat(editor, view, "remove-blank-line"); 346 | // }, 347 | // }); 348 | this.addCommand({ 349 | id: "merge-line", 350 | name: { en: "Merge broken paragraph(s) in selection", zh: "将选中文本中的断行合并", "zh-TW": "將選取文字中的斷行合併" }[lang], 351 | icon: "wrap-text", 352 | editorCallback: (editor: Editor, view: MarkdownView) => { 353 | this.editorTextFormat(editor, view, "merge-line"); 354 | }, 355 | }); 356 | // this.addCommand({ 357 | // id: "split-blank", 358 | // name: { en: "Split line(s) by blanks", zh: "将选中文本按空格分行", "zh-TW": "將選取文字按空格分行" }[lang], 359 | // editorCallback: (editor: Editor, view: MarkdownView) => { 360 | // this.editorTextFormat(editor, view, "split-blank"); 361 | // }, 362 | // }); 363 | this.addCommand({ 364 | id: "chinese-punctuation", 365 | name: { en: "Convert to Chinese punctuation marks (,;:!?)", zh: "转换为中文标点符号(,;:!?)", "zh-TW": "轉換為中文標點符號(,;:!?)" }[lang], 366 | icon: "a-large-small", 367 | editorCallback: (editor: Editor, view: MarkdownView) => { 368 | this.editorTextFormat(editor, view, "Chinese-punctuation"); 369 | }, 370 | }); 371 | this.addCommand({ 372 | id: "english-punctuation", 373 | name: { en: "Convert to English punctuation marks", zh: "转换为英文标点符号", "zh-TW": "轉換為英文標點符號" }[lang], 374 | icon: "a-large-small", 375 | editorCallback: (editor: Editor, view: MarkdownView) => { 376 | this.editorTextFormat(editor, view, "English-punctuation"); 377 | }, 378 | }); 379 | this.addCommand({ 380 | id: "hyphen", 381 | name: { en: "Remove hyphens", zh: "移除连字符", "zh-TW": "移除連字符" }[lang], 382 | icon: "a-large-small", 383 | editorCallback: (editor: Editor, view: MarkdownView) => { 384 | this.editorTextFormat(editor, view, "hyphen"); 385 | }, 386 | }); 387 | this.addCommand({ 388 | id: "ligature", 389 | name: { "en": "Replace ligature", "zh": "替换连字", "zh-TW": "取代連字" }[lang], 390 | icon: "a-large-small", 391 | editorCallback: (editor: Editor, view: MarkdownView) => { 392 | this.editorTextFormat(editor, view, "ligature"); 393 | }, 394 | }); 395 | 396 | 397 | this.addCommand({ 398 | id: "anki-card", 399 | name: { "en": "Convert selection into Anki card format", "zh": "将选中内容转换为 Anki 卡片格式", "zh-TW": "將選取內容轉換為 Anki 卡片格式" }[lang], 400 | icon: "a-large-small", 401 | editorCallback: (editor: Editor, view: MarkdownView) => { 402 | this.editorTextFormat(editor, view, "anki"); 403 | }, 404 | }); 405 | this.addCommand({ 406 | id: "remove-citation-index", 407 | name: { en: "Remove citation index", zh: "移除引用索引编号", "zh-TW": "移除引用索引編號" }[lang], 408 | icon: "a-large-small", 409 | editorCallback: (editor: Editor, view: MarkdownView) => { 410 | this.editorTextFormat(editor, view, "remove-citation"); 411 | }, 412 | }); 413 | this.addCommand({ 414 | id: "zotero-note", 415 | name: { en: "Get Zotero note from clipboard and paste", zh: "从剪贴板获取 Zotero 笔记并粘贴", "zh-TW": "從剪貼板獲取 Zotero 筆記並粘貼" }[lang], 416 | icon: "clipboard-type", 417 | editorCallback: async (editor: Editor, view: MarkdownView) => { 418 | const clipboardText = await navigator.clipboard.readText(); 419 | let text = zoteroNote( 420 | clipboardText, 421 | this.settings.ZoteroNoteRegExp, 422 | this.settings.ZoteroNoteTemplate 423 | ); 424 | editor.replaceSelection(text); 425 | }, 426 | }); 427 | this.addCommand({ 428 | id: "latex-letter", 429 | name: { en: "Detect and convert characters to math mode (LaTeX)", zh: "识别并转换字符为数学模式(LaTeX)", "zh-TW": "識別並轉換字符為數學模式(LaTeX)" }[lang], 430 | icon: "square-sigma", 431 | editorCallback: (editor: Editor, view: MarkdownView) => { 432 | this.editorTextFormat(editor, view, "latex-letter"); 433 | }, 434 | }); 435 | this.addCommand({ 436 | id: "mathpix-array2table", 437 | name: { 438 | en: "Convert Mathpix's LaTeX array to markdown table", zh: "将 Mathpix 的 LaTeX 数组转换为 Markdown 表格", "zh-TW": "將 Mathpix 的 LaTeX 陣列轉換為 Markdown 表格" 439 | }[lang], 440 | icon: "square-sigma", 441 | editorCallback: (editor: Editor, view: MarkdownView) => { 442 | this.editorTextFormat(editor, view, "array2table"); 443 | }, 444 | }); 445 | this.addCommand({ 446 | id: "callout", 447 | name: { en: "Callout format", zh: "Callout 格式", "zh-TW": "Callout 格式" }[lang], 448 | icon: "a-large-small", 449 | editorCallback: (editor: Editor, view: MarkdownView) => { 450 | this.editorTextFormat(editor, view, "callout"); 451 | }, 452 | }); 453 | 454 | 455 | this.debounceUpdateCommandWrapper(); 456 | this.debounceUpdateCommandRequest(); 457 | this.debounceUpdateCommandCustomReplace(); 458 | 459 | // this.addCommand({ 460 | // id: "decodeURI", 461 | // name: { en: "Decode URL", zh: "解码 URL", "zh-TW": "解碼 URL" }[lang], 462 | // icon: "link", 463 | // callback: async () => { 464 | // const activeElement = document.activeElement; 465 | // if (activeElement.classList.contains("metadata-input-longtext")) { 466 | // let metadataKey = activeElement.parentElement.parentElement.getAttribute("data-property-key"); 467 | // // focus on parent element, so that the new frontmatter can be updated 468 | // activeElement.parentElement.parentElement.focus(); 469 | // const file = this.app.workspace.getActiveFile(); 470 | // const frontmatter = this.app.metadataCache.getCache(file?.path as string)?.frontmatter; 471 | // let formatRes = await this.formatSelection(frontmatter[metadataKey], "decodeURI"); 472 | // if (file) { 473 | // await this.app.fileManager.processFrontMatter(file, (fm) => { 474 | // fm[metadataKey] = formatRes.editorChange.text; 475 | // }); 476 | // // activeElement.parentElement.focus(); 477 | // } 478 | // } else { 479 | // this.executeCommandById(`obsidian-text-format::editor:decodeURI`); 480 | // } 481 | // }, 482 | // }); 483 | // this.addCommand({ 484 | // id: ":editor:decodeURI", 485 | // name: { en: "Decode URL", zh: "解码 URL", "zh-TW": "解碼 URL" }[lang] + " (Editor)", 486 | // icon: "link", 487 | // editorCallback: (editor: Editor, view: MarkdownView) => { 488 | // this.editorTextFormat(editor, view, "decodeURI"); 489 | // }, 490 | // }); 491 | 492 | this.addCommand({ 493 | id: "space-word-symbol", 494 | name: { en: "Format space between words and symbols", zh: "格式化单词与符号之间的空格", "zh-TW": "格式化單詞與符號之間的空格" }[lang], 495 | icon: "space", 496 | editorCallback: (editor: Editor, view: MarkdownView) => { 497 | this.editorTextFormat(editor, view, "space-word-symbol"); 498 | }, 499 | }); 500 | } 501 | 502 | updateCommandWrapper() { 503 | const lang = getLang(); 504 | this.settings.WrapperList.forEach((wrapper, index) => { 505 | this.addCommand({ 506 | id: `wrapper:${wrapper.id}`, 507 | name: { "en": "Wrapper", "zh": "包装器", "zh-TW": "包裝器" }[lang] + " - " + wrapper.name, 508 | icon: "a-large-small", 509 | editorCallback: (editor: Editor, view: MarkdownView) => { 510 | this.editorTextFormat(editor, view, "wrapper", wrapper); 511 | }, 512 | }); 513 | }); 514 | } 515 | updateCommandRequest() { 516 | const lang = getLang(); 517 | this.settings.RequestList.forEach((request, index) => { 518 | this.addCommand({ 519 | id: `request:${request.id}`, 520 | name: { "en": "API Request", "zh": "API 请求", "zh-TW": "API 請求" }[lang] + " - " + request.name, 521 | icon: "a-large-small", 522 | editorCallback: (editor: Editor, view: MarkdownView) => { 523 | this.editorTextFormat(editor, view, "api-request", { url: request.url }); 524 | }, 525 | }); 526 | }); 527 | } 528 | debounceUpdateCommandCustomReplace() { 529 | const lang = getLang(); 530 | this.settings.customReplaceList.forEach((customReplace, index) => { 531 | this.addCommand({ 532 | id: `custom-replace:${customReplace.id}`, 533 | name: { "en": "Custom Replace", "zh": "自定义替换", "zh-TW": "自定義取代" }[lang] + " - " + customReplace.name, 534 | icon: "a-large-small", 535 | editorCallback: (editor: Editor, view: MarkdownView) => { 536 | this.editorTextFormat(editor, view, "custom-replace", { settings: customReplace }); 537 | }, 538 | }); 539 | }); 540 | } 541 | 542 | async formatSelection(selectedText: string, cmd: string, context: any = {}): Promise<FormatSelectionReturn> { 543 | this.log("formatSelection", selectedText, cmd, context) 544 | let replacedText: string = selectedText; 545 | let ret: FormatSelectionReturn = { editorChange: {} as EditorChange }; 546 | try { 547 | switch (cmd) { 548 | case "anki": 549 | replacedText = ankiSelection(selectedText); 550 | break; 551 | case "lowercase": 552 | replacedText = selectedText.toLowerCase(); 553 | break; 554 | case "uppercase": 555 | replacedText = selectedText.toUpperCase(); 556 | break; 557 | case "capitalize-word": 558 | replacedText = capitalizeWord(this.settings.LowercaseFirst ? selectedText.toLowerCase() : selectedText); 559 | break; 560 | case "capitalize-sentence": 561 | replacedText = capitalizeSentence(this.settings.LowercaseFirst ? selectedText.toLowerCase() : selectedText); 562 | break; 563 | case "title-case": 564 | replacedText = toTitleCase(selectedText, this.settings); 565 | break; 566 | case "cycle-case": 567 | let lowerString = selectedText.toLowerCase(); 568 | const settings = this.settings; 569 | function getNewString(caseCommand: string): string { 570 | switch (caseCommand) { 571 | case "titleCase": return toTitleCase(selectedText, settings); 572 | case "lowerCase": return lowerString; 573 | case "upperCase": return selectedText.toUpperCase(); 574 | case "capitalizeWord": return capitalizeWord(lowerString) 575 | case "capitalizeSentence": return capitalizeSentence(lowerString) 576 | default: 577 | new Notice(`Unknown case ${caseCommand}. \nOnly lowerCase, upperCase, capitalizeWord, capitalizeSentence, titleCase supported.`); 578 | return null; 579 | } 580 | } 581 | let toggleSeq = this.settings.ToggleSequence.replace(/ /g, "").replace(/\n+/g, "\n").split('\n'); 582 | let textHistory = new Array<string>(); 583 | let i; 584 | const L = toggleSeq.length; 585 | for (i = 0; i < L; i++) { 586 | let resText = getNewString(toggleSeq[i]), duplicated = false; 587 | // console.log(resText, toggleSeq[i]) 588 | for (let j = 0; j < textHistory.length; j++) { 589 | if (textHistory[j] == resText) { 590 | duplicated = true; 591 | break; 592 | } 593 | } 594 | if (!duplicated) { //: if the converted text is the same as before cycle case, ignore it 595 | if (selectedText == resText) { break; } 596 | } 597 | textHistory.push(resText); 598 | } 599 | //: find the cycle case that is different from the original text 600 | for (i++; i < i + L; i++) { 601 | let resText = getNewString(toggleSeq[i % L]); 602 | if (selectedText != resText) { 603 | // console.log("!", toggleSeq[i % L]) 604 | replacedText = resText; 605 | break; 606 | } 607 | } 608 | if (!(replacedText)) { return; } 609 | break; 610 | case "remove-redundant-spaces": 611 | replacedText = selectedText 612 | .replace(/(\S) {2,}/g, "$1 ") 613 | .replace(/ $| (?=\n)/g, ""); 614 | // replacedText = replacedText.replace(/\n /g, "\n"); // when a single space left at the head of the line 615 | break; 616 | case "spaces-all": 617 | replacedText = removeAllSpaces(selectedText); 618 | break; 619 | case "merge-line": 620 | replacedText = selectedText.replace(/(?:[^\n])(\n)(?!\n)/g, (t, t1) => t.replace(t1, " ")); 621 | if (this.settings.MergeParagraph_Newlines) { 622 | replacedText = replacedText.replace(/\n\n+/g, "\n\n"); 623 | } 624 | if (this.settings.MergeParagraph_Spaces) { 625 | replacedText = replacedText.replace(/ +/g, " "); 626 | } 627 | break; 628 | case "space-word-symbol": 629 | replacedText = selectedText 630 | .replace(/([\u4e00-\u9fa5]+)([\(\[\{])/g, "$1 $2") 631 | .replace(/([\)\]\}])([a-zA-Z0-9\u4e00-\u9fa5]+)/g, "$1 $2") 632 | .replace(/([\u4e00-\u9fa5])([a-zA-Z])/g, "$1 $2") 633 | .replace(/([a-zA-Z])([\u4e00-\u9fa5])/g, "$1 $2"); 634 | break; 635 | case "remove-citation": 636 | replacedText = selectedText.replace(/\[\d+\]|【\d+】/g, "").replace(/ +/g, " "); 637 | break; 638 | case "convert-ordered-list": 639 | let orderedCount = 0; 640 | // var rx = new RegExp( 641 | // String.raw`(?:^|[\s,。])((?:[:;]?i{1,4}[)\)]|\d\.) *)` + 642 | // "|" + 643 | // String.raw`(?:^|\s| and )[^\s\(\[\]]\)`, 644 | // "g" 645 | // ); 646 | let sepCustom = ""; 647 | if (this.settings.OrderedListOtherSeparator.length > 0) { 648 | sepCustom = "|" + this.settings.OrderedListOtherSeparator; 649 | } 650 | const rx = new RegExp( 651 | String.raw`([\((]?(\b\d+|\b[a-zA-Z]|[ivx]{1,4})[.\))](\s|(?=[\u4e00-\u9fa5]))` + sepCustom + `)`, 652 | "g"); 653 | // const rx = /([\((]?(\b\d+|\b[a-zA-Z]|[ivx]{1,4})[.\))](\s|(?=[\u4e00-\u9fa5]))|\sand\s|\s?(以及和)\s?)/g; 654 | replacedText = selectedText.replace( 655 | rx, function (t, t1) { 656 | orderedCount++; 657 | let head = "\n"; // if single line, then add newline character. 658 | return t.replace(t1, `${head}${orderedCount}. `); 659 | } 660 | ); 661 | replacedText = replacedText.replace(/\n+/g, "\n").replace(/^\n/, ""); 662 | break; 663 | case "convert-bullet-list": 664 | let r = this.settings.BulletPoints;//.replace("-", ""); 665 | let bulletSymbolFound = false; 666 | replacedText = selectedText 667 | .replace(RegExp(`\s*([${r}] *)|(\n[~\/Vv-] +)`, "g"), (t, t1, t2) => { 668 | // console.log(t, t1, t2) 669 | bulletSymbolFound = true; 670 | return t.replace(t1 || t2, "\n- ") 671 | }) 672 | .replace(/\n+/g, "\n") 673 | .replace(/^\n/, ""); 674 | // if "-" in this.settings.BulletPoints 675 | // if (this.settings.BulletPoints.indexOf("-") > -1) { 676 | // replacedText = replacedText.replace(/^- /g, "\n- "); 677 | // } 678 | // if select multi-paragraphs, add `- ` to the beginning 679 | if (bulletSymbolFound && selectedText.indexOf("\n") > -1 && replacedText.slice(0, 2) != "- ") { 680 | replacedText = "- " + replacedText; 681 | } 682 | replacedText = replacedText.replace(/^\n*/, ""); 683 | break; 684 | // case "split-blank": 685 | // replacedText = selectedText.replace(/ /g, "\n"); 686 | // break; 687 | case "Chinese-punctuation": 688 | replacedText = selectedText; 689 | replacedText = replacedText 690 | .replace(/(?:[\u4e00-\u9fa5])( ?, ?)(?:[\u4e00-\u9fa5])/g, (t, t1) => t.replace(t1, ",")) 691 | .replace(/(?:[^\d])( ?\. ?)/g, (t, t1) => t.replace(t1, "。")) 692 | .replace(/ ?、 ?/g, "、") 693 | .replace(/;/g, ";") 694 | .replace(/--/g, "——") 695 | .replace(/[^a-zA-Z0-9](: ?)/g, (t, t1) => t.replace(t1, ":")) 696 | .replace(/\!(?=[^\[])/g, "!") 697 | .replace(/\?/g, "?") 698 | .replace(/[\((][^\)]*?[\u4e00-\u9fa5]+?[^\)]*?[\))]/g, (t) => `(${t.slice(1, t.length - 1)})`); 699 | // TODO: ignore `!` that is `[!note]` 700 | if (this.settings.RemoveBlanksWhenChinese) { 701 | replacedText = replacedText.replace( 702 | /([\u4e00-\u9fa5【】()「」《》:“?‘、;])( +)([\u4e00-\u9fa5【】()「」《》:“?‘、;])/g, "$1$3"); 703 | } 704 | break; 705 | case "English-punctuation": 706 | replacedText = selectedText 707 | .replace(/[(\(]([\w !\"#$%&'()*+,-./:;<=>?@\[\\\]^_`{\|}~]+)[)\)]/g, "($1)") 708 | .replace(/(?:[a-zA-Z])(, ?)(?:[a-zA-Z])/g, (t, t1) => t.replace(t1, ", ")); 709 | break; 710 | case "decodeURI": 711 | replacedText = selectedText.replace( 712 | /(\w+):\/\/[-\w+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g, 713 | function (t) { 714 | return decodeURI(t) 715 | .replace(/\s/g, "%20") 716 | .replace(/%2F/g, "/"); 717 | } 718 | ); 719 | console.log(replacedText); 720 | break; 721 | case "hyphen": 722 | replacedText = selectedText.replace(/(\w)-[ ]/g, "$1"); 723 | break; 724 | case "array2table": 725 | replacedText = array2markdown(selectedText); 726 | break; 727 | case "table2bullet": 728 | replacedText = table2bullet(selectedText, false); 729 | break; 730 | case "table2bullet-header": 731 | replacedText = table2bullet(selectedText, true); 732 | break; 733 | case "remove-wiki-link": 734 | replacedText = removeWikiLink(selectedText, this.settings.WikiLinkFormat) 735 | break; 736 | case "remove-url-link": 737 | replacedText = removeUrlLink(selectedText, this.settings.UrlLinkFormat); 738 | if (this.settings.RemoveWikiURL2) { 739 | replacedText = removeWikiLink(replacedText, this.settings.WikiLinkFormat); 740 | } 741 | break; 742 | case "link-url2wiki": 743 | replacedText = url2WikiLink(selectedText); 744 | break; 745 | case "link-wiki2md": 746 | replacedText = convertWikiLinkToMarkdown(selectedText, this); 747 | break; 748 | case "ligature": 749 | replacedText = replaceLigature(selectedText); 750 | break; 751 | case "sort-todo": 752 | // const blocks = selectedText.split(/\n\s*\n/g); 753 | // const results: string[] = []; 754 | // for (const block of blocks) { 755 | // results.push(sortTodo(block)); 756 | // } 757 | // replacedText = results.join("\n\n"); 758 | let fromLine = context.adjustRange.from.line; 759 | if ((context.originRange.from.line === context.originRange.to.line) && (!context.editor.getLine(context.originRange.from.line).match(/\s*- \[[ \w\?\!\-]\] /))) { 760 | fromLine = null; 761 | } 762 | replacedText = sortTodo(selectedText, context, fromLine); 763 | break; 764 | case "slugify": 765 | replacedText = slugify(selectedText); 766 | break; 767 | case "snakify": 768 | replacedText = snakify(selectedText); 769 | break; 770 | case "camel-case": 771 | replacedText = camelCase(selectedText, context.lowerFirst); 772 | break; 773 | case "custom-replace": 774 | replacedText = customReplace(selectedText, context.settings); 775 | break; 776 | case "heading": 777 | const adjustRange = context.adjustRange; 778 | if (adjustRange.from.line === adjustRange.to.line) { 779 | const headingRes = headingLevel(selectedText, context.upper, this.settings.headingLevelMin); 780 | replacedText = headingRes.text; 781 | } else { 782 | replacedText = ""; 783 | selectedText.split("\n").forEach((line, index) => { 784 | const headingRes = headingLevel(line, context.upper, this.settings.headingLevelMin, true); 785 | replacedText += headingRes.text + "\n"; 786 | }); 787 | replacedText = replacedText.slice(0, -1); // remove the last `\n` 788 | } 789 | break; 790 | case "api-request": 791 | replacedText = await requestAPI(selectedText, context.view.file, context.url); 792 | break; 793 | case "wrapper": 794 | const wrapperResult = textWrapper(selectedText, context); 795 | selectedText = wrapperResult.selectedText; 796 | replacedText = wrapperResult.editorChange.text; 797 | context.adjustRange = { from: wrapperResult.editorChange.from, to: wrapperResult.editorChange.to }; 798 | // ret.resetSelection = wrapperResult.resetSelection; 799 | ret.resetSelectionOffset = wrapperResult.resetSelectionOffset; 800 | break; 801 | case "callout": 802 | const reCalloutType = /(?:(^|\n)\>\s*\[\!)(\w+)(?:\])/gm; 803 | const lines = selectedText.replace(/^\n|\n$/g, "").split("\n"); 804 | if (lines[0].match(reCalloutType)) { //: detect callout grammar, delete callout prefix 805 | replacedText = lines[0].replace(reCalloutType, "").replace(/^\s*/g, ""); 806 | let i = 1; 807 | for (; i < lines.length; i++) { 808 | if (lines[i].match(/^>/g)) { 809 | replacedText += "\n" + lines[i].replace(/^>\s*/, ""); 810 | } else { break; } 811 | } 812 | //: add rest part of lines in original format 813 | replacedText = (replacedText + "\n" + lines.slice(i, lines.length).join("\n")).replace(/\n$/g, "") 814 | } else { //: To add callout prefix 815 | //: Get the previous callout types at first 816 | let wholeContent, type; 817 | switch (this.settings.calloutTypeDecider) { 818 | // case CalloutTypeDecider.lastUsed: 819 | // type = this.memory.lastCallout; 820 | // break; 821 | case CalloutTypeDecider.fix: 822 | type = this.settings.calloutType; 823 | break; 824 | case CalloutTypeDecider.wholeFile: 825 | wholeContent = context.editor.getValue(); 826 | break; 827 | case CalloutTypeDecider.preContent: 828 | wholeContent = context.editor.getRange({ line: 0, ch: 0 }, context.adjustRange.from); 829 | break; 830 | } 831 | const preCalloutList = wholeContent.match(reCalloutType); 832 | if (preCalloutList) { 833 | type = reCalloutType.exec(preCalloutList[preCalloutList.length - 1])[2]; 834 | } else { 835 | type = this.settings.calloutType; 836 | } 837 | if (type.startsWith("!")) { type = type.substring(1, type.length); } 838 | 839 | 840 | replacedText = `> [!${type}] ${lines[0]}` 841 | if (lines.length > 1) { 842 | for (let idx = 1; idx < lines.length; idx++) { 843 | replacedText += `\n> ` + lines[idx]; 844 | } 845 | } 846 | // this.memory.lastCallout = type; 847 | } 848 | break; 849 | case "latex-letter": 850 | replacedText = convertLatex(context.editor, selectedText); 851 | break; 852 | default: 853 | Error("Unknown command"); 854 | } 855 | } catch (e) { 856 | new Notice(e); 857 | console.error(e); 858 | } 859 | 860 | if (replacedText != selectedText) { 861 | ret.editorChange = { text: replacedText, ...context?.adjustRange }; 862 | } 863 | return ret; 864 | } 865 | 866 | log(...args: any[]): void { 867 | // TODO: add verbose log setting 868 | if (this.settings.debugMode) { 869 | console.log(...args); 870 | } 871 | } 872 | 873 | async editorTextFormat(editor: Editor, view: MarkdownView, cmd: string, context: any = {}): Promise<void> { 874 | 875 | const originSelectionList: EditorSelectionOrCaret[] = editor.listSelections(); 876 | const resetSelectionList: EditorSelectionOrCaret[] = []; 877 | 878 | for (let originSelection of originSelectionList) { 879 | const originRange = selection2range(editor, originSelection); 880 | const somethingSelected = !(originRange.from.ch == originRange.to.ch && originRange.from.line == originRange.to.line) 881 | let adjustRange: EditorRangeOrCaret = originRange; 882 | 883 | this.log(originSelection) 884 | this.log(originRange) 885 | 886 | //: Adjust Selection 887 | let adjustSelectionCmd: selectionBehavior; 888 | switch (cmd) { 889 | case "wrapper": 890 | //: Keep the selection as it is 891 | break; 892 | case "heading": 893 | adjustSelectionCmd = selectionBehavior.wholeLine; 894 | break; 895 | case "split-blank": 896 | case "convert-bullet-list": 897 | case "convert-ordered-list": 898 | case "callout": 899 | //: Force to select whole paragraph(s) 900 | adjustSelectionCmd = selectionBehavior.wholeLine; 901 | break; 902 | case "sort-todo": 903 | //: Select whole file if nothing selected 904 | if (originRange.from.line == originRange.to.line) { 905 | adjustRange = { 906 | from: { line: 0, ch: 0 }, 907 | // to: { line: editor.lastLine(), ch: editor.getLine(editor.lastLine()).length } 908 | to: { line: editor.lastLine() + 1, ch: 0 } 909 | }; 910 | } else { 911 | adjustSelectionCmd = selectionBehavior.wholeLine; 912 | } 913 | context.originRange = originRange; 914 | break; 915 | default: 916 | if (!somethingSelected) { 917 | // if nothing is selected, select the whole line. 918 | adjustSelectionCmd = selectionBehavior.wholeLine; 919 | } 920 | //: Except special process of adjusting selection, get all selected text (for now) 921 | break; 922 | } 923 | 924 | switch (adjustSelectionCmd) { 925 | case selectionBehavior.wholeLine: //: Force to select whole paragraph(s) 926 | adjustRange = { 927 | from: { line: originRange.from.line, ch: 0 }, 928 | to: { line: originRange.to.line, ch: editor.getLine(originRange.to.line).length } 929 | }; 930 | break; 931 | default: 932 | break; 933 | } 934 | 935 | const selectedText = editor.getRange(adjustRange.from, adjustRange.to); 936 | this.log("adjustRange", adjustRange) 937 | this.log("selectedText", selectedText) 938 | 939 | //: MODIFY SELECTION 940 | context.editor = editor; 941 | context.view = view; 942 | context.adjustRange = adjustRange; 943 | const formatResult = await this.formatSelection(selectedText, cmd, context); 944 | this.log("formatResult", formatResult) 945 | //: Make change immediately 946 | 947 | if (formatResult.editorChange.text == undefined) { 948 | this.log("nothing changed.") 949 | editor.setSelections(originSelectionList); 950 | return; 951 | } 952 | editor.transaction({ changes: [formatResult.editorChange] }); 953 | 954 | //: Set cursor selection 955 | let resetSelection: EditorSelectionOrCaret = { anchor: adjustRange.from, head: adjustRange.to }; 956 | const fos = editor.posToOffset(adjustRange.from); 957 | const replacedText = formatResult.editorChange?.text || selectedText; 958 | const textOffset = replacedText.length - selectedText.length; 959 | const cursorLast = editor.offsetToPos(fos + replacedText.length); 960 | const selections = { 961 | keepOriginSelection: { 962 | anchor: editor.offsetToPos(editor.posToOffset(originRange.from) + textOffset), 963 | head: editor.offsetToPos(editor.posToOffset(originRange.to) + textOffset) 964 | }, 965 | wholeReplacedText: { 966 | anchor: adjustRange.from, 967 | head: cursorLast 968 | } 969 | } 970 | switch (cmd) { 971 | case "sort-todo": 972 | resetSelection = originSelection; 973 | break; 974 | case "wrapper": 975 | // resetSelection = formatResult.resetSelection; 976 | resetSelection = { 977 | anchor: editor.offsetToPos(formatResult.resetSelectionOffset.anchor), 978 | head: editor.offsetToPos(formatResult.resetSelectionOffset.head) 979 | } 980 | // console.log(resetSelection) 981 | break; 982 | case "callout": 983 | case "heading": 984 | if (originRange.from.line === originRange.to.line) { 985 | resetSelection = selections.keepOriginSelection; 986 | } else { 987 | resetSelection = selections.wholeReplacedText; 988 | } 989 | break; 990 | default: 991 | resetSelection = selections.wholeReplacedText; 992 | } 993 | this.log("resetSelection", resetSelection) 994 | resetSelectionList.push(resetSelection); 995 | } 996 | 997 | this.log("resetSelectionList", resetSelectionList) 998 | editor.setSelections(resetSelectionList); 999 | } 1000 | 1001 | async loadSettings() { 1002 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 1003 | this.compatibleSettingsUpgrade(); 1004 | } 1005 | 1006 | async saveSettings() { 1007 | await this.saveData(this.settings); 1008 | 1009 | } 1010 | 1011 | async initCustomSettings() { 1012 | if (this.manifest.version === this.settings.manifest.version) { return; } // no need to upgrade 1013 | 1014 | this.compatibleSettingsUpgrade(); 1015 | for (let command of CustomReplacementBuiltInCommands) { 1016 | if (!this.settings.customReplaceBuiltInLog[command.id]) { 1017 | this.settings.customReplaceList.push({ 1018 | id: command.id, 1019 | name: getString(["command", command.id]), 1020 | data: renew(command.data) 1021 | }); 1022 | this.settings.customReplaceBuiltInLog[command.id] = { 1023 | id: command.id, 1024 | modified: false, 1025 | data: renew(command.data) 1026 | }; 1027 | } else { // build in command is loaded in older version, then check if modified 1028 | if (!this.settings.customReplaceBuiltInLog[command.id].modified) { 1029 | // upgrade replacement data 1030 | const index = this.settings.customReplaceList.findIndex(item => item.id === command.id); 1031 | // console.log(index); 1032 | if (index > -1) { 1033 | this.settings.customReplaceList[index].data = renew(command.data); 1034 | } 1035 | // this.settings.customReplaceBuiltInLog[command.id].data = command.data; 1036 | } 1037 | } 1038 | } 1039 | 1040 | await this.backupDataJson(); 1041 | 1042 | // update version AFTER backup data.json 1043 | this.settings.manifest.version = this.manifest.version; // update version 1044 | await this.saveSettings(); 1045 | } 1046 | 1047 | async backupDataJson() { 1048 | // save a backup data.json before overwriting data.json 1049 | const vault = this.app.vault; 1050 | // @ts-ignore 1051 | const originDataPath = normalizePath(this.manifest.dir + "/data.json"); 1052 | const newDataPath = normalizePath(this.manifest.dir + `/data-backup-v${this.settings.manifest.version}-to-v${this.manifest.version}.json`); 1053 | if (await vault.adapter.exists(originDataPath)) { 1054 | // exist data.json of old version 1055 | new Notice(`[INFO] Updated ${this.manifest.name} from ${this.settings.manifest.version} to v${this.manifest.version}, backup ongoing...`) 1056 | await vault.adapter.copy(originDataPath, newDataPath); 1057 | } 1058 | } 1059 | 1060 | compatibleSettingsUpgrade() { 1061 | // uuid init 1062 | for (let i in this.settings.customReplaceList) 1063 | if (!this.settings.customReplaceList[i].id) 1064 | this.settings.customReplaceList[i].id = uuidv4(); 1065 | for (let i in this.settings.WrapperList) 1066 | if (!this.settings.WrapperList[i].id) 1067 | this.settings.WrapperList[i].id = uuidv4(); 1068 | for (let i in this.settings.RequestList) 1069 | if (!this.settings.RequestList[i].id) 1070 | this.settings.RequestList[i].id = uuidv4(); 1071 | 1072 | // @ts-ignore 1073 | const oldCustomReplaceBuiltIn = this.settings.customReplaceBuiltIn; 1074 | if (oldCustomReplaceBuiltIn && 1075 | (!this.settings.customReplaceBuiltInLog || Object.keys(this.settings.customReplaceBuiltInLog).length == 0)) { 1076 | console.log("upgrade customReplaceBuiltInLog") 1077 | let newBuiltIn: { [id: string]: CustomReplaceBuiltIn } = {}; 1078 | for (const i in oldCustomReplaceBuiltIn) { 1079 | const id: string = oldCustomReplaceBuiltIn[i]; // string 1080 | newBuiltIn[id] = { id: id, modified: false, data: CustomReplacementBuiltInCommands.find(x => x.id === id).data }; 1081 | } 1082 | this.settings.customReplaceBuiltInLog = newBuiltIn; 1083 | } 1084 | } 1085 | } 1086 | 1087 | 1088 | 1089 | function selection2range(editor: Editor, selection: EditorSelectionOrCaret): { readonly from: EditorPosition, readonly to: EditorPosition } { 1090 | let anchorOffset = editor.posToOffset(selection.anchor), 1091 | headOffset = editor.posToOffset(selection.head); 1092 | const from = editor.offsetToPos(Math.min(anchorOffset, headOffset)); 1093 | const to = editor.offsetToPos(Math.max(anchorOffset, headOffset)); 1094 | return { from: from, to: to }; 1095 | } --------------------------------------------------------------------------------