├── .npmrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── closeIssue.yml │ ├── checkPR.yml │ ├── build.yml │ └── release.yml ├── exampleVault ├── Untitled.md ├── index.md ├── customThemes │ └── OneMonokai-color-theme.json └── customLanguages │ └── odin.json ├── .prettierignore ├── exampleImage.png ├── exampleImagePlain.png ├── exampleImageObsidian.png ├── bunfig.toml ├── .prettierrc.json ├── automation ├── config.json ├── utils │ ├── utils.ts │ ├── versionUtils.ts │ └── shellUtils.ts ├── tsconfig.json ├── build │ ├── esbuild.config.min.ts │ ├── esbuild.dev.config.ts │ ├── esbuild.config.ts │ └── buildBanner.ts ├── stats.ts └── release.ts ├── tests ├── test.test.ts ├── obsidianMock.ts ├── happydom.ts └── svelteLoader.ts ├── src ├── obsidian-ex.d.ts ├── general │ ├── README.md │ ├── LLogInOb.ts │ ├── LLog.ts │ ├── EditableCodeblock.zh.md │ ├── EditableCodeblock.md │ ├── EditableCodeblock.css │ └── EditableCodeblockInOb.ts ├── settings │ ├── Settings.ts │ ├── StringSelectModal.ts │ └── SettingsTab.ts ├── codemirror │ ├── Cm6_Util.ts │ └── Cm6_ViewPlugin.ts ├── CodeBlock.ts ├── themes │ ├── ThemeMapper.ts │ ├── ECTheme.ts │ └── ObsidianTheme.ts ├── PrismPlugin.ts ├── main.ts ├── main.min.ts ├── EditableEditor.ts └── Highlighter.ts ├── .editorconfig ├── manifest.json ├── manifest-beta.json ├── tsconfig.json ├── versions.json ├── version-bump.mjs ├── .gitignore ├── LICENSE ├── eslint.config.mjs ├── package.json ├── custom.css └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ['mProjectsCode'] 2 | -------------------------------------------------------------------------------- /exampleVault/Untitled.md: -------------------------------------------------------------------------------- 1 | ```yaml 2 | test: a 3 | ``` 4 | 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /lib 2 | node_modules 3 | exampleVault 4 | main.js 5 | -------------------------------------------------------------------------------- /exampleImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LincZero/obsidian-shiki-plugin/HEAD/exampleImage.png -------------------------------------------------------------------------------- /exampleImagePlain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LincZero/obsidian-shiki-plugin/HEAD/exampleImagePlain.png -------------------------------------------------------------------------------- /exampleImageObsidian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LincZero/obsidian-shiki-plugin/HEAD/exampleImageObsidian.png -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | preload = ["./tests/svelteLoader.ts", "./tests/happydom.ts", "./tests/obsidianMock.ts"] 3 | root = "./tests" 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 160, 3 | "useTabs": true, 4 | "tabWidth": 4, 5 | "semi": true, 6 | "singleQuote": true, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /automation/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "devBranch": "master", 3 | "releaseBranch": "release", 4 | "github": "https://github.com/mProjectsCode/obsidian-shiki-plugin" 5 | } 6 | -------------------------------------------------------------------------------- /automation/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export class UserError extends Error {} 2 | 3 | export interface ProjectConfig { 4 | corePackages: string[]; 5 | packages: string[]; 6 | } 7 | -------------------------------------------------------------------------------- /tests/test.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'bun:test'; 2 | 3 | describe('sample test group', () => { 4 | test('sample test', () => { 5 | expect(5).toBe(5); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/obsidian-ex.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare module 'obsidian' { 4 | interface App { 5 | // opens a file or folder with the default application 6 | openWithDefaultApp(path: string): void; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /tests/obsidianMock.ts: -------------------------------------------------------------------------------- 1 | import { mock } from 'bun:test'; 2 | import Moment from 'moment'; 3 | 4 | mock.module('obsidian', () => ({ 5 | setIcon(iconEl: HTMLElement, iconName: string): void { 6 | // do nothing 7 | }, 8 | moment: Moment, 9 | })); 10 | -------------------------------------------------------------------------------- /tests/happydom.ts: -------------------------------------------------------------------------------- 1 | import { GlobalRegistrator } from '@happy-dom/global-registrator'; 2 | import process from 'process'; 3 | 4 | GlobalRegistrator.register({ 5 | settings: {}, 6 | }); 7 | 8 | if (process.env.LOG_TESTS === 'false') { 9 | console.log = () => {}; 10 | console.debug = () => {}; 11 | } 12 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "shiki-highlighter", 3 | "name": "Shiki Highlighter", 4 | "version": "0.5.0", 5 | "minAppVersion": "1.5.0", 6 | "description": "Highlight code blocks with Shiki.", 7 | "author": "Moritz Jung", 8 | "authorUrl": "https://www.moritzjung.dev", 9 | "fundingUrl": "https://github.com/sponsors/mProjectsCode", 10 | "isDesktopOnly": false 11 | } 12 | -------------------------------------------------------------------------------- /manifest-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "shiki-highlighter", 3 | "name": "Shiki Highlighter", 4 | "version": "0.5.0", 5 | "minAppVersion": "1.5.0", 6 | "description": "Highlight code blocks with Shiki.", 7 | "author": "Moritz Jung", 8 | "authorUrl": "https://www.moritzjung.dev", 9 | "fundingUrl": "https://github.com/sponsors/mProjectsCode", 10 | "isDesktopOnly": false 11 | } 12 | -------------------------------------------------------------------------------- /tests/svelteLoader.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from 'bun'; 2 | 3 | // plugin( 4 | // // @ts-ignore 5 | // // esbuildSvelte({ 6 | // // compilerOptions: { css: 'injected' }, 7 | // // preprocess: sveltePreprocess(), 8 | // // filterWarnings: warning => { 9 | // // // we don't want warnings from node modules that we can do nothing about 10 | // // return !warning.filename?.includes('node_modules'); 11 | // // }, 12 | // // }), 13 | // ); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "ES6", 6 | "allowJs": true, 7 | "noImplicitAny": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "noImplicitReturns": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "isolatedModules": true, 14 | "lib": ["DOM", "ESNext", "DOM.Iterable"], 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["src/**/*.ts", "tests/**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.1": "0.15.0", 3 | "0.0.3": "1.4.0", 4 | "0.0.4": "1.4.0", 5 | "0.1.0": "1.5.0", 6 | "0.1.1": "1.5.0", 7 | "0.1.2": "1.5.0", 8 | "0.2.0": "1.5.0", 9 | "0.2.1": "1.5.0", 10 | "0.2.2": "1.5.0", 11 | "0.2.3": "1.5.0", 12 | "0.2.4": "1.5.0", 13 | "0.3.0": "1.5.0", 14 | "0.3.1": "1.5.0", 15 | "0.3.2": "1.5.0", 16 | "0.4.0": "1.5.0", 17 | "0.4.1": "1.5.0", 18 | "0.4.2": "1.5.0", 19 | "0.4.3": "1.5.0", 20 | "0.4.4": "1.5.0", 21 | "0.4.5": "1.5.0", 22 | "0.5.0": "1.5.0" 23 | } 24 | -------------------------------------------------------------------------------- /src/general/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | General, shared, and utils are all common parts, but here the specification is: 4 | - general: Must be unrelated to this project. The contents in this folder can be moved to other projects for reuse. 5 | - shared: Multiple components need to share. 6 | - utils: Small tools. Since they are all small tools, they can sometimes be reused, but they do not emphasize reusability. 7 | 8 | `InOb` version: 9 | 10 | - Convert the general module into a module with `Obsidian` dependencies and replace the general module. 11 | -------------------------------------------------------------------------------- /automation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "allowJs": true, 7 | "noImplicitAny": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "noImplicitReturns": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "isolatedModules": true, 14 | "lib": ["DOM", "ES5", "ES6", "ES7", "Es2021"], 15 | "types": ["bun-types"], 16 | "allowSyntheticDefaultImports": true, 17 | "resolveJsonModule": true 18 | }, 19 | "include": ["**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/workflows/closeIssue.yml: -------------------------------------------------------------------------------- 1 | name: Close Invalid Issue 2 | on: 3 | issues: 4 | types: 5 | - labeled 6 | jobs: 7 | closeIssue: 8 | if: github.event.label.name == 'invalid' 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - name: Close Issue 14 | uses: peter-evans/close-issue@v2 15 | with: 16 | comment: This issue is invalid. Please conform to the issue templates. 17 | close-reason: not_planned 18 | -------------------------------------------------------------------------------- /src/general/LLogInOb.ts: -------------------------------------------------------------------------------- 1 | import { LLog, type LogLevel2 } from 'src/general/LLog' 2 | import { 3 | Notice, 4 | } from 'obsidian'; 5 | 6 | class LLogInOb extends LLog { 7 | logCore(level: LogLevel2, ...args: unknown[]): void { 8 | super.logCore(level, ...args) 9 | 10 | // If error, it is necessary to clearly inform the users 11 | if (level != "error") return 12 | if (args[0] && typeof args[0] === 'string') { 13 | new Notice(args[0], 3000) 14 | } 15 | } 16 | } 17 | // Provide an object that is ready to use out of the box. 18 | export const LLOG = new LLogInOb() 19 | -------------------------------------------------------------------------------- /.github/workflows/checkPR.yml: -------------------------------------------------------------------------------- 1 | name: Run Checks and Tests on PR 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Install Bun 14 | uses: oven-sh/setup-bun@v1 15 | with: 16 | bun-version: latest 17 | 18 | - name: Install Dependencies 19 | id: build 20 | run: | 21 | bun install 22 | 23 | - name: Run Checks 24 | run: | 25 | bun run check 26 | -------------------------------------------------------------------------------- /src/settings/Settings.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | disabledLanguages: string[]; 3 | customThemeFolder: string; 4 | customLanguageFolder: string; 5 | theme: string; 6 | renderMode: 'textarea'|'pre'|'editablePre'|'codemirror'; 7 | renderEngine: 'shiki'|'prismjs'; 8 | saveMode: 'onchange'|'oninput', 9 | preferThemeColors: boolean; 10 | inlineHighlighting: boolean; 11 | } 12 | 13 | export const DEFAULT_SETTINGS: Settings = { 14 | disabledLanguages: [], 15 | customThemeFolder: '', 16 | customLanguageFolder: '', 17 | theme: 'obsidian-theme', 18 | renderMode: 'textarea', 19 | renderEngine: 'shiki', 20 | saveMode: 'onchange', 21 | preferThemeColors: true, 22 | inlineHighlighting: true, 23 | }; 24 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs'; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync('manifest.json', 'utf8')); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t')); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync('versions.json', 'utf8')); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync('versions.json', JSON.stringify(versions, null, '\t')); 15 | -------------------------------------------------------------------------------- /src/settings/StringSelectModal.ts: -------------------------------------------------------------------------------- 1 | import { FuzzySuggestModal } from 'obsidian'; 2 | import type ShikiPlugin from 'src/main'; 3 | 4 | export class StringSelectModal extends FuzzySuggestModal { 5 | items: string[]; 6 | onSelect: (item: string) => void; 7 | 8 | constructor(plugin: ShikiPlugin, items: string[], onSelect: (item: string) => void) { 9 | super(plugin.app); 10 | 11 | this.items = items; 12 | this.onSelect = onSelect; 13 | } 14 | 15 | public getItemText(item: string): string { 16 | return item; 17 | } 18 | 19 | public getItems(): string[] { 20 | return this.items; 21 | } 22 | 23 | public onChooseItem(item: string, _evt: MouseEvent | KeyboardEvent): void { 24 | this.onSelect(item); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | 24 | 25 | obsidian.css 26 | meta.txt 27 | 28 | !exampleVault/.obsidian 29 | 30 | exampleVault/.obsidian/* 31 | !exampleVault/.obsidian/plugins 32 | 33 | exampleVault/.obsidian/plugins/* 34 | exampleVault/.obsidian/plugins/lemons-plugin-template/* 35 | !exampleVault/.obsidian/plugins/lemons-plugin-template/.hotreload 36 | 37 | /main.css 38 | /styles.css 39 | /tmp 40 | /temp 41 | /backup 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | - [ ] The Plugin is up to date 10 | - [ ] Obsidian is up to date 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Occurs on** 30 | 31 | - [ ] Windows 32 | - [ ] macOS 33 | - [ ] Linux 34 | - [ ] Android 35 | - [ ] iOS 36 | 37 | **Plugin version** 38 | x.x.x 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Moritz Jung 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. 22 | -------------------------------------------------------------------------------- /automation/build/esbuild.config.min.ts: -------------------------------------------------------------------------------- 1 | import builtins from 'builtin-modules'; 2 | import esbuild from 'esbuild'; 3 | import { getBuildBanner } from 'build/buildBanner'; 4 | import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill'; 5 | 6 | const banner = getBuildBanner('Release Build', version => version); 7 | 8 | const build = await esbuild.build({ 9 | banner: { 10 | js: banner, 11 | }, 12 | entryPoints: ['src/main.min.ts'], 13 | bundle: true, 14 | external: [ 15 | 'obsidian', 16 | 'electron', 17 | '@codemirror/autocomplete', 18 | '@codemirror/collab', 19 | '@codemirror/commands', 20 | '@codemirror/language', 21 | '@codemirror/lint', 22 | '@codemirror/search', 23 | '@codemirror/state', 24 | '@codemirror/view', 25 | '@lezer/common', 26 | '@lezer/highlight', 27 | '@lezer/lr', 28 | ...builtins, 29 | 'shiki', // [!code hl] 30 | ], 31 | format: 'cjs', 32 | target: 'es2018', 33 | logLevel: 'info', 34 | sourcemap: false, 35 | treeShaking: true, 36 | outfile: 'dist-min/main.js', // [!code hl] 37 | minify: true, 38 | metafile: true, 39 | define: { 40 | MB_GLOBAL_CONFIG_DEV_BUILD: 'false', 41 | }, 42 | plugins: [ 43 | nodeModulesPolyfillPlugin({ 44 | modules: { 45 | fs: true, 46 | path: true, 47 | url: true, 48 | }, 49 | }), 50 | ], 51 | }); 52 | 53 | const file = Bun.file('meta.txt'); 54 | await Bun.write(file, JSON.stringify(build.metafile, null, '\t')); 55 | 56 | process.exit(0); 57 | -------------------------------------------------------------------------------- /automation/build/esbuild.dev.config.ts: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import copy from 'esbuild-plugin-copy-watch'; 3 | import manifest from '../../manifest.json' assert { type: 'json' }; 4 | import { getBuildBanner } from 'build/buildBanner'; 5 | import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill'; 6 | 7 | const banner = getBuildBanner('Dev Build', _ => 'Dev Build'); 8 | 9 | const context = await esbuild.context({ 10 | banner: { 11 | js: banner, 12 | }, 13 | entryPoints: ['src/main.ts'], 14 | bundle: true, 15 | external: [ 16 | 'obsidian', 17 | 'electron', 18 | '@codemirror/autocomplete', 19 | '@codemirror/collab', 20 | '@codemirror/commands', 21 | '@codemirror/language', 22 | '@codemirror/lint', 23 | '@codemirror/search', 24 | '@codemirror/state', 25 | '@codemirror/view', 26 | '@lezer/common', 27 | '@lezer/highlight', 28 | '@lezer/lr', 29 | ], 30 | format: 'cjs', 31 | target: 'es2018', 32 | logLevel: 'info', 33 | sourcemap: 'inline', 34 | treeShaking: true, 35 | outdir: `exampleVault/.obsidian/plugins/${manifest.id}/`, 36 | outbase: 'src', 37 | define: { 38 | MB_GLOBAL_CONFIG_DEV_BUILD: 'true', 39 | }, 40 | plugins: [ 41 | copy({ 42 | paths: [ 43 | { 44 | from: './styles.css', 45 | to: '', 46 | }, 47 | { 48 | from: './manifest.json', 49 | to: '', 50 | }, 51 | ], 52 | }), 53 | nodeModulesPolyfillPlugin({ 54 | modules: { 55 | fs: true, 56 | path: true, 57 | url: true, 58 | }, 59 | }), 60 | ], 61 | }); 62 | 63 | await context.watch(); 64 | -------------------------------------------------------------------------------- /src/codemirror/Cm6_Util.ts: -------------------------------------------------------------------------------- 1 | import { type EditorState, type EditorSelection } from '@codemirror/state'; 2 | import { type DecorationSet } from '@codemirror/view'; 3 | 4 | export class Cm6_Util { 5 | /** 6 | * Checks if two ranges overlap. 7 | * 8 | * @param fromA 9 | * @param toA 10 | * @param fromB 11 | * @param toB 12 | */ 13 | static checkRangeOverlap(fromA: number, toA: number, fromB: number, toB: number): boolean { 14 | return fromA <= toB && fromB <= toA; 15 | } 16 | 17 | /** 18 | * Checks if editor selection and the given range overlap. 19 | * 20 | * @param selection 21 | * @param from 22 | * @param to 23 | */ 24 | static checkSelectionAndRangeOverlap(selection: EditorSelection, from: number, to: number): boolean { 25 | for (const range of selection.ranges) { 26 | if (Cm6_Util.checkRangeOverlap(range.from, range.to, from, to)) { 27 | return true; 28 | } 29 | } 30 | return false; 31 | } 32 | 33 | /** 34 | * Gets the editor content of a given range. 35 | * 36 | * @param state 37 | * @param from 38 | * @param to 39 | */ 40 | static getContent(state: EditorState, from: number, to: number): string { 41 | return state.sliceDoc(from, to); 42 | } 43 | 44 | /** 45 | * Checks if a decoration exists in a given range. 46 | * 47 | * @param decorations 48 | * @param from 49 | * @param to 50 | */ 51 | static existsDecorationBetween(decorations: DecorationSet, from: number, to: number): boolean { 52 | let exists = false; 53 | decorations.between(from, to, () => { 54 | exists = true; 55 | }); 56 | return exists; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /automation/build/esbuild.config.ts: -------------------------------------------------------------------------------- 1 | import builtins from 'builtin-modules'; 2 | import esbuild from 'esbuild'; 3 | import { getBuildBanner } from 'build/buildBanner'; 4 | import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill'; 5 | 6 | const banner = getBuildBanner('Release Build', version => version); 7 | 8 | const build = await esbuild.build({ 9 | banner: { 10 | js: banner, 11 | }, 12 | entryPoints: ['src/main.ts'], 13 | bundle: true, 14 | external: [ 15 | 'obsidian', 16 | 'electron', 17 | '@codemirror/autocomplete', 18 | '@codemirror/collab', 19 | '@codemirror/commands', 20 | '@codemirror/language', 21 | '@codemirror/lint', 22 | '@codemirror/search', 23 | '@codemirror/state', 24 | '@codemirror/view', 25 | '@lezer/common', 26 | '@lezer/highlight', 27 | '@lezer/lr', 28 | ...builtins, 29 | ], 30 | format: 'cjs', 31 | target: 'es2018', 32 | logLevel: 'info', 33 | sourcemap: false, 34 | treeShaking: true, 35 | outfile: 'main.js', 36 | minify: true, 37 | metafile: true, 38 | define: { 39 | MB_GLOBAL_CONFIG_DEV_BUILD: 'false', 40 | }, 41 | plugins: [ 42 | nodeModulesPolyfillPlugin({ 43 | modules: { 44 | fs: true, 45 | path: true, 46 | url: true, 47 | }, 48 | }), 49 | ], 50 | }); 51 | 52 | const file = Bun.file('meta.txt'); 53 | await Bun.write(file, JSON.stringify(build.metafile, null, '\t')); 54 | 55 | // css 56 | await esbuild.build({ 57 | entryPoints: ["custom.css"], 58 | outfile: "styles.css", 59 | // watch: !prod, // 似乎若升级esbuild后不再支持 60 | bundle: true, 61 | allowOverwrite: true, 62 | minify: false, 63 | }); 64 | 65 | process.exit(0); 66 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build-obsidian: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout repo 15 | uses: actions/checkout@v4 16 | - name: env use node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '22' 20 | - name: Install Bun 21 | uses: oven-sh/setup-bun@v1 22 | with: 23 | bun-version: latest 24 | - name: build 25 | run: | 26 | bun install 27 | bun run build 28 | - name: upload build artifact 29 | if: always() 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: build-artifact 33 | path: | 34 | manifest.json 35 | main.js 36 | styles.css 37 | 38 | build-obsidian-min: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: checkout repo 42 | uses: actions/checkout@v4 43 | - name: env use node.js 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: '22' 47 | - name: Install Bun 48 | uses: oven-sh/setup-bun@v1 49 | with: 50 | bun-version: latest 51 | - name: build 52 | run: | 53 | bun install 54 | bun run build-min 55 | mv dist-min/main.js main.js 56 | - name: upload build artifact 57 | if: always() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: build-artifact-min 61 | path: | 62 | manifest.json 63 | main.js 64 | styles.css 65 | -------------------------------------------------------------------------------- /automation/build/buildBanner.ts: -------------------------------------------------------------------------------- 1 | import manifest from '../../manifest.json' assert { type: 'json' }; 2 | 3 | export function getBuildBanner(buildType: string, getVersion: (version: string) => string) { 4 | return `/* 5 | ------------------------------------------- 6 | ${manifest.name} - ${buildType} 7 | ------------------------------------------- 8 | By: ${manifest.author} (${manifest.authorUrl}) 9 | Time: ${new Date().toUTCString()} 10 | Version: ${getVersion(manifest.version)} 11 | ------------------------------------------- 12 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 13 | if you want to view the source, please visit the github repository of this plugin 14 | ------------------------------------------- 15 | MIT License 16 | 17 | Copyright (c) ${new Date().getFullYear()} ${manifest.author} 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in all 27 | copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | */ 37 | `; 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Plugin Release 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: shiki-highlighter # Change this to the name of your plugin-id folder 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Determine prerelease status 21 | id: status 22 | run: | 23 | if [[ "${{ github.ref }}" == *"canary"* ]]; then 24 | echo "prerelease=true" >> $GITHUB_OUTPUT 25 | else 26 | echo "prerelease=false" >> $GITHUB_OUTPUT 27 | fi 28 | 29 | - name: Install Bun 30 | uses: oven-sh/setup-bun@v1 31 | with: 32 | bun-version: latest 33 | 34 | - name: Build 35 | id: build 36 | run: | 37 | bun install 38 | bun run build 39 | mkdir ${{ env.PLUGIN_NAME }} 40 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} 41 | zip -r ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip ${{ env.PLUGIN_NAME }} 42 | ls 43 | 44 | - name: Release 45 | id: release 46 | uses: softprops/action-gh-release@v2 47 | with: 48 | prerelease: ${{ steps.status.outputs.prerelease }} 49 | token: ${{ secrets.GITHUB_TOKEN }} 50 | files: | 51 | ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip 52 | main.js 53 | manifest.json 54 | styles.css 55 | -------------------------------------------------------------------------------- /src/general/LLog.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple, general, log tool 3 | * 4 | * feat: 5 | * - level 6 | * - log & notice & file 7 | */ 8 | 9 | export type LogLevel = "debug" | "info" | "warn" | "error" | "none"; 10 | export type LogLevel2 = "debug" | "info" | "warn" | "error"; 11 | 12 | interface LLog_Config { 13 | level?: LogLevel; // min output level 14 | enableTimestamp?: boolean; // is output timestamp 15 | tag?: string; // tag prefix 16 | } 17 | 18 | const levelOrder: LogLevel[] = ["debug", "info", "warn", "error", "none"]; 19 | 20 | export class LLog { 21 | config: Required = { 22 | level: "debug", 23 | enableTimestamp: true, 24 | tag: "", 25 | } 26 | 27 | set_config(cfg: LLog_Config): void { 28 | this.config = { ...this.config, ...cfg } 29 | } 30 | 31 | debug(...args: unknown[]): void { 32 | this.logCore("debug", ...args) 33 | } 34 | 35 | info(...args: unknown[]): void { 36 | this.logCore("info", ...args) 37 | } 38 | 39 | warn(...args: unknown[]): void { 40 | this.logCore("warn", ...args) 41 | } 42 | 43 | error(...args: unknown[]): void { 44 | this.logCore("error", ...args) 45 | } 46 | 47 | static consoleMap = { 48 | debug: console.log, 49 | info: console.info, 50 | warn: console.warn, 51 | error: console.error, 52 | } as const; 53 | 54 | // can override 55 | /// // @return 返回打印内容。可以通过这种方式链式调用添加Notice等操作,进行多重输出 56 | logCore(level: LogLevel2, ...args: unknown[]): void { 57 | if (levelOrder.indexOf(level) < levelOrder.indexOf(this.config.level)) return 58 | 59 | const now = this.config.enableTimestamp ? `[${new Date().toISOString()}]` : "" 60 | const tag = this.config.tag ? `[${this.config.tag}]` : "" 61 | const prefix = [now, tag, `[${level.toUpperCase()}]`].filter(Boolean).join(" ") 62 | 63 | LLog.consoleMap[level]?.(prefix, ...args) 64 | } 65 | } 66 | // Provide an object that is ready to use out of the box. 67 | export const LLOG = new LLog() 68 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import only_warn from 'eslint-plugin-only-warn'; 6 | import no_relative_import_paths from 'eslint-plugin-no-relative-import-paths'; 7 | 8 | export default tseslint.config( 9 | { 10 | ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.svelte'], 11 | }, 12 | { 13 | files: ['src/**/*.ts'], 14 | extends: [ 15 | eslint.configs.recommended, 16 | ...tseslint.configs.recommended, 17 | ...tseslint.configs.recommendedTypeChecked, 18 | ...tseslint.configs.stylisticTypeChecked, 19 | ], 20 | languageOptions: { 21 | parser: tseslint.parser, 22 | parserOptions: { 23 | project: true, 24 | }, 25 | }, 26 | plugins: { 27 | // @ts-ignore 28 | 'only-warn': only_warn, 29 | 'no-relative-import-paths': no_relative_import_paths, 30 | }, 31 | rules: { 32 | // `any` about 33 | '@typescript-eslint/no-explicit-any': 'off', // ['warn'], 34 | '@typescript-eslint/no-unsafe-call': 'off', 35 | '@typescript-eslint/no-unsafe-assignment': 'off', 36 | '@typescript-eslint/no-unsafe-member-access': 'off', 37 | '@typescript-eslint/no-unsafe-return': 'off', 38 | '@typescript-eslint/no-unsafe-argument': 'off', 39 | 40 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }], 41 | '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', fixStyle: 'inline-type-imports' }], 42 | 43 | '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], 44 | '@typescript-eslint/restrict-template-expressions': 'off', 45 | 46 | 'no-relative-import-paths/no-relative-import-paths': ['warn', { allowSameFolder: false }], 47 | 48 | '@typescript-eslint/ban-ts-comment': 'off', 49 | '@typescript-eslint/no-empty-function': 'off', 50 | '@typescript-eslint/no-inferrable-types': 'off', 51 | '@typescript-eslint/explicit-function-return-type': ['warn'], 52 | '@typescript-eslint/require-await': 'off', 53 | }, 54 | }, 55 | ); 56 | -------------------------------------------------------------------------------- /src/general/EditableCodeblock.zh.md: -------------------------------------------------------------------------------- 1 | # 更多文档 2 | 3 | version: v0.5.1 4 | 5 | ## 设置面板文档 6 | 7 | ### 渲染引擎 8 | 9 | Shiki, PrismJS,CodeMirror 10 | 11 | - Shiki: 一个强大的代码高亮引擎。 12 | - 功能更加强大,更多主题和插件 13 | - 插件: meta标注、注释型标注。行高亮、单词高亮、差异化标注、警告/错误标注 14 | - 主题:近80种配色方案:你可以在 https://textmate-grammars-themes.netlify.app 中可视化选择 15 | - *min版不包含该库,无法选用该引擎* 16 | - PrismJS: Obsidian默认在阅读模式中使用的渲染引擎。 17 | - 当选择这个的时候,你也可以选用min版本的本插件,拥有更小的插件体积和更快的加载速度 18 | - 可以与使用obsidian主题的代码配色,可以与一些其他的obsidian风格化插件配合 19 | - CodeMirror: Obsidian默认在实时模式中使用的渲染引擎。当前插件不支持 20 | - 适合实时渲染,性能尚可 21 | - 但代码分析比较粗糙,高亮层数少,效果较差 22 | 23 | ### 渲染方式 24 | 25 | - textarea (默认) 26 | - 优点: 27 | 允许实时编辑,typora般的所见即所得的体验 28 | 支持编辑注释型高亮 29 | 同为块内编辑的obsidian新版本md表格,采用的是这种方式 (但ob表格编辑时不触发重渲染) 30 | - 缺点: 31 | 原理上是将textarea和pre完美重叠在一起,但容易受主题和样式影响导致不完全重叠 32 | textarea的横向滚动无法与pre的同步 33 | - pre 34 | - 缺点: 35 | 不允许实时编辑 36 | - editable pre 37 | - 优点: 38 | 允许实时编辑,typora般的所见即所得的体验 39 | 原理上是 `code[contenteditable='true']` 40 | - 缺点: 41 | 程序上需要手动处理光标位置 42 | *不支持实时编辑注释型高亮* 43 | - codemirror 44 | - 缺点: 45 | V0.5.0及之前唯一支持的方式,不允许实时编辑 46 | 47 | > [!warning] 48 | > 49 | > 如果选用了可实时编辑的方案,最好能在仓库定期备份的情况下使用,避免意外 50 | 51 | ### 自动保存方式 52 | 53 | - onchange 54 | - 优点: 55 | 更好的性能 56 | 程序实现简单更简单,无需手动管理光标位置 57 | - 缺点: 58 | 延时保存,特殊场景可能不会保存修改: 程序突然崩溃。当光标在代码块中时,直接切换到阅读模式,或关闭当前窗口/标签页 59 | - oninput 60 | - 优点: 61 | 实时保存,数据更安全 62 | 同为块内编辑的obsidian新版本md表格,采用的是这种方式 63 | - 缺点: 64 | 性能略差? 每次修改都要重新创建代码块 65 | 程序需要手动管理光标位置,手动防抖。 66 | 需要注意输入法问题,输入候选阶段也会触发 `oninput` 67 | 68 | ## Shiki扩展语法 69 | 70 | 详见: https://shiki.style/packages/transformers (可切换至中文) 71 | 72 | 这是个简单的语法总结: 73 | 74 | - notaion 注释型标注 75 | - diff: `// [!code ++]` `// [!code --]` 差异化 76 | - highlight: `// [!code hl]` `// [!code highlight]` 高亮 77 | - word highlight: `// [!code word::]` `// [!code word:Hello:1]` 单词高亮 78 | - focus: `// [!code focus]` 聚焦 79 | - error level: `// [!code error]` `// [!code warning]` 警告/错误 80 | - (mul line): `// [!code highlight:3]` (多行) 81 | - meta 元数据型标注 82 | - highlight: `{1,3-4}` 83 | - word highlight `//` `/Hello/` 84 | 85 | 示例: see [../README.md](../README.md) or [Shiki document](https://shiki.style/packages/transformers) 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shiki-highlighter", 3 | "version": "0.5.0", 4 | "description": "Highlight code blocks with Shiki.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "bun run automation/build/esbuild.dev.config.ts", 8 | "build": "bun run tsc && bun run automation/build/esbuild.config.ts", 9 | "build-min": "bun run tsc && bun run automation/build/esbuild.config.min.ts", 10 | "tsc": "tsc -noEmit -skipLibCheck", 11 | "test": "bun test", 12 | "test:log": "LOG_TESTS=true bun test", 13 | "format": "prettier --write .", 14 | "format:check": "prettier --check .", 15 | "lint": "eslint --max-warnings=0 src/**", 16 | "lint:fix": "eslint --max-warnings=0 --fix src/**", 17 | "check": "bun run format:check && bun run tsc && bun run lint && bun run test", 18 | "check:fix": "bun run format && bun run tsc && bun run lint:fix && bun run test", 19 | "release": "bun run automation/release.ts", 20 | "stats": "bun run automation/stats.ts" 21 | }, 22 | "keywords": [], 23 | "author": "Moritz Jung", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@codemirror/basic-setup": "^0.20.0", 27 | "@codemirror/lang-markdown": "^6.3.2", 28 | "@codemirror/language": "^6.11.0", 29 | "@codemirror/state": "^6.5.2", 30 | "@codemirror/view": "^6.36.8", 31 | "@eslint/js": "^9.27.0", 32 | "@expressive-code/core": "^0.41.2", 33 | "@expressive-code/plugin-collapsible-sections": "^0.41.2", 34 | "@expressive-code/plugin-frames": "^0.41.2", 35 | "@expressive-code/plugin-line-numbers": "^0.41.2", 36 | "@expressive-code/plugin-shiki": "^0.41.2", 37 | "@expressive-code/plugin-text-markers": "^0.41.2", 38 | "@happy-dom/global-registrator": "^17.4.7", 39 | "@lemons_dev/parsinom": "^0.0.12", 40 | "@lezer/common": "^1.2.3", 41 | "@shikijs/transformers": "^3.4.2", 42 | "@tsconfig/svelte": "^5.0.4", 43 | "@types/bun": "^1.2.13", 44 | "@types/prismjs": "^1.26.5", 45 | "builtin-modules": "^5.0.0", 46 | "esbuild": "^0.25.4", 47 | "esbuild-plugin-copy-watch": "^2.3.1", 48 | "esbuild-plugins-node-modules-polyfill": "^1.7.0", 49 | "eslint": "^9.27.0", 50 | "eslint-plugin-no-relative-import-paths": "^1.6.1", 51 | "eslint-plugin-only-warn": "^1.1.0", 52 | "itertools-ts": "^2.2.0", 53 | "obsidian": "latest", 54 | "prettier": "^3.5.3", 55 | "shiki": "^3.4.2", 56 | "string-argv": "^0.3.2", 57 | "tslib": "^2.8.1", 58 | "typescript": "^5.8.3", 59 | "typescript-eslint": "^8.32.1" 60 | } 61 | } -------------------------------------------------------------------------------- /src/CodeBlock.ts: -------------------------------------------------------------------------------- 1 | import { type MarkdownPostProcessorContext, MarkdownRenderChild } from 'obsidian'; 2 | import type ShikiPlugin from 'src/main'; 3 | 4 | export class CodeBlock extends MarkdownRenderChild { 5 | plugin: ShikiPlugin; 6 | source: string; 7 | language: string; 8 | ctx: MarkdownPostProcessorContext; 9 | cachedMetaString: string; 10 | 11 | constructor(plugin: ShikiPlugin, containerEl: HTMLElement, source: string, language: string, ctx: MarkdownPostProcessorContext) { 12 | super(containerEl); 13 | 14 | this.plugin = plugin; 15 | this.source = source; 16 | this.language = language; 17 | this.ctx = ctx; 18 | this.cachedMetaString = ''; 19 | } 20 | 21 | private getMetaString(): string { 22 | const sectionInfo = this.ctx.getSectionInfo(this.containerEl); 23 | 24 | if (sectionInfo === null) { 25 | return ''; 26 | } 27 | 28 | const lines = sectionInfo.text.split('\n'); 29 | const startLine = lines[sectionInfo.lineStart]; 30 | 31 | // regexp to match the text after the code block language 32 | const regex = new RegExp('^[^`~]*?\\s*(```+|~~~+)' + this.language + ' (.*)', 'g'); 33 | const match = regex.exec(startLine); 34 | if (match !== null) { 35 | return match[2]; 36 | } else { 37 | return ''; 38 | } 39 | } 40 | 41 | private async render(metaString: string): Promise { 42 | await this.plugin.highlighter.renderWithEc(this.source, this.language, metaString, this.containerEl); 43 | } 44 | 45 | public async rerenderOnNoteChange(): Promise { 46 | // compare the new meta string to the cached one 47 | // only rerender if they are different, to avoid unnecessary work 48 | // since the meta string is likely to be the same most of the time 49 | // and if the code block content changes obsidian will rerender for us 50 | const newMetaString = this.getMetaString(); 51 | if (newMetaString !== this.cachedMetaString) { 52 | this.cachedMetaString = newMetaString; 53 | await this.render(newMetaString); 54 | } 55 | } 56 | 57 | public async forceRerender(): Promise { 58 | await this.render(this.cachedMetaString); 59 | } 60 | 61 | public onload(): void { 62 | super.onload(); 63 | 64 | this.plugin.addActiveCodeBlock(this); 65 | 66 | this.cachedMetaString = this.getMetaString(); 67 | void this.render(this.cachedMetaString); 68 | } 69 | 70 | public onunload(): void { 71 | super.onunload(); 72 | 73 | this.plugin.removeActiveCodeBlock(this); 74 | 75 | this.containerEl.empty(); 76 | this.containerEl.innerText = 'unloaded shiki code block'; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /automation/utils/versionUtils.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '@lemons_dev/parsinom/lib/Parser'; 2 | import { P_UTILS } from '@lemons_dev/parsinom/lib/ParserUtils'; 3 | import { P } from '@lemons_dev/parsinom/lib/ParsiNOM'; 4 | import Moment from 'moment'; 5 | import { UserError } from 'utils/utils'; 6 | 7 | export class Version { 8 | major: number; 9 | minor: number; 10 | patch: number; 11 | 12 | constructor(major: number, minor: number, patch: number) { 13 | this.major = major; 14 | this.minor = minor; 15 | this.patch = patch; 16 | } 17 | 18 | toString(): string { 19 | return `${this.major}.${this.minor}.${this.patch}`; 20 | } 21 | } 22 | 23 | export class CanaryVersion extends Version { 24 | canary: string; 25 | 26 | constructor(major: number, minor: number, patch: number, canary: string) { 27 | super(major, minor, patch); 28 | this.canary = canary; 29 | } 30 | 31 | toString(): string { 32 | return `${super.toString()}-canary.${this.canary}`; 33 | } 34 | } 35 | 36 | const numberParser: Parser = P_UTILS.digits() 37 | .map(x => Number.parseInt(x)) 38 | .chain(x => { 39 | if (Number.isNaN(x)) { 40 | return P.fail('a number'); 41 | } else { 42 | return P.succeed(x); 43 | } 44 | }); 45 | 46 | const canaryParser: Parser = P.sequenceMap( 47 | (_, c1, c2, c3) => { 48 | return c1 + c2 + c3; 49 | }, 50 | P.string('-canary.'), 51 | P_UTILS.digit() 52 | .repeat(8, 8) 53 | .map(x => x.join('')), 54 | P.string('T'), 55 | P_UTILS.digit() 56 | .repeat(6, 6) 57 | .map(x => x.join('')), 58 | ); 59 | 60 | export const versionParser: Parser = P.or( 61 | P.sequenceMap( 62 | (major, _1, minor, _2, patch) => { 63 | return new Version(major, minor, patch); 64 | }, 65 | numberParser, 66 | P.string('.'), 67 | numberParser, 68 | P.string('.'), 69 | numberParser, 70 | P_UTILS.eof(), 71 | ), 72 | P.sequenceMap( 73 | (major, _1, minor, _2, patch, canary) => { 74 | return new CanaryVersion(major, minor, patch, canary); 75 | }, 76 | numberParser, 77 | P.string('.'), 78 | numberParser, 79 | P.string('.'), 80 | numberParser, 81 | canaryParser, 82 | P_UTILS.eof(), 83 | ), 84 | ); 85 | 86 | export function parseVersion(str: string): Version { 87 | const parserRes = versionParser.tryParse(str); 88 | if (parserRes.success) { 89 | return parserRes.value; 90 | } else { 91 | throw new UserError(`failed to parse manifest version "${str}"`); 92 | } 93 | } 94 | 95 | export function stringifyVersion(version: Version): string { 96 | return version.toString(); 97 | } 98 | 99 | export function getIncrementOptions(version: Version): [Version, Version, Version, CanaryVersion] { 100 | const moment = Moment(); 101 | const canary = moment.utcOffset(0).format('YYYYMMDDTHHmmss'); 102 | return [ 103 | new Version(version.major + 1, 0, 0), 104 | new Version(version.major, version.minor + 1, 0), 105 | new Version(version.major, version.minor, version.patch + 1), 106 | new CanaryVersion(version.major, version.minor, version.patch, canary), 107 | ]; 108 | } 109 | -------------------------------------------------------------------------------- /src/themes/ThemeMapper.ts: -------------------------------------------------------------------------------- 1 | import { type BundledTheme, bundledThemes, type ThemeRegistration } from 'shiki'; 2 | import type * as hast_types from 'hast'; 3 | import { OBSIDIAN_THEME } from 'src/themes/ObsidianTheme'; 4 | import type ShikiPlugin from 'src/main'; 5 | 6 | export class ThemeMapper { 7 | plugin: ShikiPlugin; 8 | mapCounter: number; 9 | mapping: Map; 10 | 11 | constructor(plugin: ShikiPlugin) { 12 | this.plugin = plugin; 13 | 14 | this.mapCounter = 0; 15 | this.mapping = new Map(); 16 | } 17 | 18 | async getThemeForEC(): Promise { 19 | if (this.plugin.loadedSettings.theme.endsWith('.json')) { 20 | return this.plugin.highlighter.customThemes.find(theme => theme.name === this.plugin.loadedSettings.theme) as ThemeRegistration; 21 | } else if (this.plugin.loadedSettings.theme !== 'obsidian-theme') { 22 | return (await bundledThemes[this.plugin.loadedSettings.theme as BundledTheme]()).default; 23 | } 24 | 25 | return { 26 | displayName: OBSIDIAN_THEME.displayName, 27 | name: OBSIDIAN_THEME.name, 28 | semanticHighlighting: OBSIDIAN_THEME.semanticHighlighting, 29 | colors: Object.fromEntries(Object.entries(OBSIDIAN_THEME.colors).map(([key, value]) => [key, this.mapColor(value)])), 30 | tokenColors: OBSIDIAN_THEME.tokenColors.map(token => { 31 | const newToken = { ...token }; 32 | 33 | if (newToken.settings) { 34 | newToken.settings = { ...newToken.settings }; 35 | } 36 | 37 | if (newToken.settings.foreground) { 38 | newToken.settings.foreground = this.mapColor(newToken.settings.foreground); 39 | } 40 | 41 | return newToken; 42 | }), 43 | }; 44 | } 45 | 46 | async getTheme(): Promise { 47 | if (this.plugin.loadedSettings.theme.endsWith('.json')) { 48 | return this.plugin.highlighter.customThemes.find(theme => theme.name === this.plugin.loadedSettings.theme) as ThemeRegistration; 49 | } else if (this.plugin.loadedSettings.theme !== 'obsidian-theme') { 50 | return (await bundledThemes[this.plugin.loadedSettings.theme as BundledTheme]()).default; 51 | } 52 | 53 | return OBSIDIAN_THEME; 54 | } 55 | 56 | /** 57 | * Maps a color or CSS variable to a hex color. 58 | */ 59 | mapColor(color: string): string { 60 | if (this.mapping.has(color)) { 61 | return this.mapping.get(color)!; 62 | } else { 63 | const newColor = `#${this.mapCounter.toString(16).padStart(6, '0').toUpperCase()}`; 64 | this.mapCounter += 1; 65 | this.mapping.set(color, newColor); 66 | return newColor; 67 | } 68 | } 69 | 70 | /** 71 | * Maps the placeholder colors in the AST to CSS variables. 72 | */ 73 | fixAST(ast: hast_types.Parents): hast_types.Parents { 74 | if (this.plugin.loadedSettings.theme !== 'obsidian-theme') { 75 | return ast; 76 | } 77 | 78 | ast.children = ast.children.map(child => { 79 | if (child.type === 'element') { 80 | return this.fixNode(child); 81 | } else { 82 | return child; 83 | } 84 | }); 85 | 86 | return ast; 87 | } 88 | 89 | private fixNode(node: hast_types.Element): hast_types.Element { 90 | if (node.properties?.style) { 91 | let style = node.properties.style as string; 92 | for (const [key, value] of this.mapping) { 93 | style = style.replaceAll(value, key); 94 | } 95 | node.properties.style = style; 96 | } 97 | 98 | for (const child of node.children) { 99 | if (child.type === 'element') { 100 | this.fixNode(child); 101 | } 102 | } 103 | 104 | return node; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /exampleVault/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | test: hello 3 | --- 4 | 5 | TypeScript codearsiNOM 6 | 7 | ```ts title="A part of ParsiNOM" {13-15, 22-29} showLineNumbers 8 | export class Parser { 9 | public p: ParseFunction; 10 | 11 | constructor(p: ParseFunction) { 12 | this.p = p; 13 | } 14 | 15 | /** 16 | * Parses a string, returning a result object. 17 | * 18 | * @param str 19 | */ 20 | tryParse(str: string): ParseResult { 21 | return this.p(new ParserContext(str, { index: 0, line: 1, column: 1 })); 22 | } 23 | 24 | /** 25 | * Parses a string, throwing a {@link ParsingError} on failure. 26 | * 27 | * @param str 28 | */ 29 | parse(str: string): SType { 30 | const result: ParseResult = this.tryParse(str); 31 | if (result.success) { 32 | return result.value; 33 | } else { 34 | throw new ParsingError(str, result); 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | CSS code 41 | 42 | ```css title="Some CSS by sailKite" showLineNumbers {2} ins={6-8, 15-17} del={15-17} 43 | input:is([data-task="式"], [data-task="式"] > *):checked::after { 44 | content: "式"; 45 | color: transparent; 46 | font-weight: 600; 47 | text-align: center; 48 | -webkit-mask-image: linear-gradient(black, white); 49 | -webkit-mask-size: 100%; 50 | -webkit-mask-clip: text; 51 | } 52 | input:is([data-task="字"], [data-task="字"] > *):checked::after { 53 | content: "字"; 54 | color: transparent; 55 | font-weight: 600; 56 | text-align: center; 57 | -webkit-mask-image: linear-gradient(black, white); 58 | -webkit-mask-size: 100%; 59 | -webkit-mask-clip: text; 60 | } 61 | ``` 62 | 63 | ```css 64 | input:is([data-task="式"], [data-task="式"] > *):checked::after { 65 | content: "式"; 66 | color: transparent; 67 | font-weight: 600; 68 | text-align: center; 69 | -webkit-mask-image: linear-gradient(black, white); 70 | -webkit-mask-size: 100%; 71 | -webkit-mask-clip: text; 72 | } 73 | input:is([data-task="字"], [data-task="字"] > *):checked::after { 74 | content: "字"; 75 | color: transparent; 76 | font-weight: 600; 77 | text-align: center; 78 | -webkit-mask-image: linear-gradient(black, white); 79 | -webkit-mask-size: 100%; 80 | -webkit-mask-clip: text; 81 | } 82 | ``` 83 | 84 | Bash 85 | 86 | ```bash title="Other Title" 87 | echo "Hello" 88 | ``` 89 | 90 | ```diff 91 | + this line will be marked as inserted 92 | - this line will be marked as deleted 93 | this is a regular line 94 | ``` 95 | 96 | > [!NOTE] 97 | > ```diff showLineNumbers 98 | > + this line will be marked as inserted 99 | > - this line will be marked as deleted 100 | > this is a regular line 101 | > ``` 102 | 103 | Inline code 104 | 105 | `{jsx}