├── dev-vault ├── .obsidian │ ├── hotkeys.json │ ├── community-plugins.json │ ├── file-recovery.json │ ├── snippets │ │ ├── tests.css │ │ └── lila-frontmatter.css │ ├── app.json │ ├── appearance.json │ ├── themes │ │ └── Minimal │ │ │ └── manifest.json │ ├── core-plugins.json │ ├── plugins │ │ └── dataview │ │ │ ├── manifest.json │ │ │ └── styles.css │ ├── graph.json │ └── core-plugins-migration.json └── tests.md ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── semantic-release.yml ├── .editorconfig ├── manifest.json ├── tsconfig.json ├── src ├── styles.css ├── utils.ts ├── main.ts ├── md-line.ts ├── settings.ts ├── live-preview-mode.ts └── read-mode.ts ├── .gitignore ├── versions.json ├── version-bump.mjs ├── package.json ├── LICENSE ├── esbuild.config.mjs ├── .releaserc.json ├── CONTRIBUTING.md ├── CHANGELOG.md ├── CHANGELOG-until-v2.0.0.md └── README.md /dev-vault/.obsidian/hotkeys.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://lila.rest/donations'] 2 | -------------------------------------------------------------------------------- /dev-vault/.obsidian/community-plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | "dataview", 3 | "custom-classes" 4 | ] -------------------------------------------------------------------------------- /dev-vault/.obsidian/file-recovery.json: -------------------------------------------------------------------------------- 1 | { 2 | "intervalMinutes": 0, 3 | "keepDays": 7 4 | } -------------------------------------------------------------------------------- /dev-vault/.obsidian/snippets/tests.css: -------------------------------------------------------------------------------- 1 | .green-title { 2 | background-color: lightgreen; 3 | } 4 | 5 | .fancy-text:hover { 6 | font-weight: bold; 7 | font-size: 30px; 8 | transition-duration: 500ms; 9 | } -------------------------------------------------------------------------------- /dev-vault/.obsidian/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "showInlineTitle": false, 3 | "livePreview": true, 4 | "strictLineBreaks": false, 5 | "legacyEditor": false, 6 | "promptDelete": false, 7 | "showFrontmatter": false 8 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /dev-vault/.obsidian/appearance.json: -------------------------------------------------------------------------------- 1 | { 2 | "accentColor": "", 3 | "enabledCssSnippets": [ 4 | "frontmatter-tests", 5 | "lila-frontmatter", 6 | "tests" 7 | ], 8 | "theme": "system", 9 | "cssTheme": "Minimal" 10 | } -------------------------------------------------------------------------------- /dev-vault/.obsidian/themes/Minimal/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Minimal", 3 | "version": "6.2.1", 4 | "minAppVersion": "1.1.0", 5 | "author": "@kepano", 6 | "authorUrl": "https://twitter.com/kepano", 7 | "fundingUrl": "https://www.buymeacoffee.com/kepano" 8 | } 9 | -------------------------------------------------------------------------------- /dev-vault/.obsidian/core-plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | "file-explorer", 3 | "global-search", 4 | "switcher", 5 | "graph", 6 | "backlink", 7 | "canvas", 8 | "outgoing-link", 9 | "tag-pane", 10 | "page-preview", 11 | "templates", 12 | "command-palette", 13 | "editor-status", 14 | "outline", 15 | "word-count" 16 | ] -------------------------------------------------------------------------------- /dev-vault/.obsidian/plugins/dataview/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dataview", 3 | "name": "Dataview", 4 | "version": "0.5.55", 5 | "minAppVersion": "0.13.11", 6 | "description": "Complex data views for the data-obsessed.", 7 | "author": "Michael Brenan ", 8 | "authorUrl": "https://github.com/blacksmithgu", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "custom-classes", 3 | "name": "Custom Classes", 4 | "version": "2.6.1", 5 | "minAppVersion": "0.15.0", 6 | "description": "Custom Classes is a minimalist plugin that allows you to add custom HTML classes to markdown blocks", 7 | "author": "Lila Rest ", 8 | "authorUrl": "https://lila.rest", 9 | "fundingUrl": "https://lila.rest/donations", 10 | "isDesktopOnly": false 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "dom", 16 | "dom.iterable", 17 | "es5", 18 | "es6", 19 | "es7", 20 | "ES2021" 21 | ] 22 | }, 23 | "include": [ 24 | "**/*.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | h2.settings-header { 2 | font-size: 18px; 3 | margin-left: -10px; 4 | font-weight: 900; 5 | margin-top: 60px; 6 | } 7 | 8 | h2.settings-header:nth-of-type(2) { 9 | margin-top: 71.25px; 10 | } 11 | 12 | .cc-container { 13 | white-space: nowrap; 14 | display: inline-block; 15 | } 16 | 17 | .cc-container > div.cc-renderer > * { 18 | padding-top: 0; 19 | padding-bottom: 0; 20 | margin-top: 0; 21 | margin-bottom: 0; 22 | white-space: normal; 23 | } 24 | 25 | .cc-container > div.cc-renderer ul, 26 | .cc-container > div.cc-renderer ol { 27 | padding-inline-start: 24px; 28 | } -------------------------------------------------------------------------------- /dev-vault/.obsidian/graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "collapse-filter": true, 3 | "search": "", 4 | "showTags": false, 5 | "showAttachments": false, 6 | "hideUnresolved": false, 7 | "showOrphans": true, 8 | "collapse-color-groups": true, 9 | "colorGroups": [], 10 | "collapse-display": true, 11 | "showArrow": false, 12 | "textFadeMultiplier": 0, 13 | "nodeSizeMultiplier": 1, 14 | "lineSizeMultiplier": 1, 15 | "collapse-forces": true, 16 | "centerStrength": 0.518713248970312, 17 | "repelStrength": 10, 18 | "linkStrength": 1, 19 | "linkDistance": 250, 20 | "scale": 1, 21 | "close": false 22 | } -------------------------------------------------------------------------------- /.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 | # Test Obsidian vault 25 | dev-vault/.obsidian/workspace.json 26 | 27 | # Exclude the dist/ folder created while building distributions files 28 | dist/ 29 | 30 | # Exclude the dev builds 31 | dev-vault/.obsidian/plugins/custom-classes -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const ccBlockRegex = /` *((class|cls): *|\.)([a-zA-Z\-_]+[a-zA-Z\-_0-9]*( *\, *)?)+ *`/; 2 | 3 | export const ccBlockRegexGlob = new RegExp(ccBlockRegex.source, "g"); 4 | 5 | export function isCustomClassBlock (codeEl: HTMLElement): boolean { 6 | if (!codeEl) return false; 7 | const mdCodeBlock = "`" + codeEl.innerText + "`"; 8 | return ccBlockRegex.test(mdCodeBlock); 9 | } 10 | 11 | export function retrieveCustomClasses (ccBlockText: string): Array { 12 | return ccBlockText 13 | .replaceAll("`", "") 14 | .replaceAll(" ", "") 15 | .replaceAll("\n", "") 16 | .replace("class:", "") 17 | .replace("cls:", "") 18 | .replace(".", "") 19 | .split(","); 20 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this plugin 4 | title: '' 5 | labels: enhancement 6 | assignees: LilaRest 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.3.0": "0.15.0", 4 | "1.3.1": "0.15.0", 5 | "1.3.5": "0.15.0", 6 | "1.3.6": "0.15.0", 7 | "1.4.0": "0.15.0", 8 | "1.4.1": "0.15.0", 9 | "1.5.0": "0.15.0", 10 | "1.6.0": "0.15.0", 11 | "1.7.0": "0.15.0", 12 | "1.8.0": "0.15.0", 13 | "1.9.0": "0.15.0", 14 | "1.10.0": "0.15.0", 15 | "1.11.0": "0.15.0", 16 | "1.12.0": "0.15.0", 17 | "1.13.0": "0.15.0", 18 | "1.14.0": "0.15.0", 19 | "1.15.0": "0.15.0", 20 | "1.15.1": "0.15.0", 21 | "2.0.0": "0.15.0", 22 | "2.1.0": "0.15.0", 23 | "2.2.0": "0.15.0", 24 | "2.3.0": "0.15.0", 25 | "2.4.0": "0.15.0", 26 | "2.4.2": "0.15.0", 27 | "2.4.3": "0.15.0", 28 | "2.5.0": "0.15.0", 29 | "2.6.0": "0.15.0", 30 | "2.6.1": "0.15.0" 31 | } -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | let packageJSON = JSON.parse(readFileSync("package.json", "utf8")); 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 = packageJSON.version; 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[packageJSON.version] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); -------------------------------------------------------------------------------- /dev-vault/.obsidian/core-plugins-migration.json: -------------------------------------------------------------------------------- 1 | { 2 | "file-explorer": true, 3 | "global-search": true, 4 | "switcher": true, 5 | "graph": true, 6 | "backlink": true, 7 | "canvas": true, 8 | "outgoing-link": true, 9 | "tag-pane": true, 10 | "page-preview": true, 11 | "daily-notes": false, 12 | "templates": true, 13 | "note-composer": false, 14 | "command-palette": true, 15 | "slash-command": false, 16 | "editor-status": true, 17 | "starred": false, 18 | "markdown-importer": false, 19 | "zk-prefixer": false, 20 | "random-note": false, 21 | "outline": true, 22 | "word-count": true, 23 | "slides": false, 24 | "audio-recorder": false, 25 | "workspaces": false, 26 | "file-recovery": false, 27 | "publish": false, 28 | "sync": false 29 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an unwanted / unexpected behavior 4 | title: '' 5 | labels: bug 6 | assignees: LilaRest 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Device (please complete the following information):** 27 | - Operating System: [e.g. Linux | Android | MacOS | Windows | IOS] 28 | - Obsidian version: 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/workflows/semantic-release.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | env: 8 | PLUGIN_NAME: obsidian-custom-classes 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: "lts/*" 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Build 29 | id: build 30 | run: | 31 | npm install 32 | npm run build 33 | 34 | - name: Create ZIP file 35 | id: zip 36 | run: | 37 | zip -r ${{ env.PLUGIN_NAME }}.zip dist 38 | 39 | - name: Release 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | run: npx semantic-release -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-custom-classes", 3 | "version": "2.6.1", 4 | "description": "Custom Classes is a minimalist Obsidian plugin that allows you to add custom HTML classes to markdown blocks", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production" 9 | }, 10 | "keywords": [ 11 | "css", 12 | "html", 13 | "obsidian", 14 | "style" 15 | ], 16 | "author": "Lila Rest ", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@codemirror/language": "^6.4.0", 20 | "@semantic-release/changelog": "^6.0.2", 21 | "@semantic-release/exec": "^6.0.3", 22 | "@semantic-release/git": "^10.0.1", 23 | "@types/node": "^16.11.6", 24 | "builtin-modules": "3.3.0", 25 | "cz-conventional-changelog": "^3.3.0", 26 | "esbuild": "0.14.47", 27 | "obsidian": "latest", 28 | "semantic-release": "^20.1.0", 29 | "tslib": "2.4.0", 30 | "typescript": "4.7.4" 31 | }, 32 | "config": { 33 | "commitizen": { 34 | "path": "./node_modules/cz-conventional-changelog" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mikhail Menshikov 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 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules'; 4 | 5 | const banner = 6 | `/* 7 | THIS FILE IS AUTOMATICALLY GENERATED BY ESBUILD 8 | To browse sources, please visit the repository of this plugin : https://github.com/LilaRest/obsidian-custom-classes 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === 'production'); 13 | 14 | esbuild.build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: { 19 | main: 'src/main.ts', 20 | styles: 'src/styles.css', 21 | manifest: "./manifest.json" 22 | }, 23 | bundle: true, 24 | external: [ 25 | 'obsidian', 26 | 'electron', 27 | '@codemirror/autocomplete', 28 | '@codemirror/collab', 29 | '@codemirror/commands', 30 | '@codemirror/language', 31 | '@codemirror/lint', 32 | '@codemirror/search', 33 | '@codemirror/state', 34 | '@codemirror/view', 35 | '@lezer/common', 36 | '@lezer/highlight', 37 | '@lezer/lr', 38 | ...builtins], 39 | format: 'cjs', 40 | watch: !prod, 41 | target: 'es2018', 42 | logLevel: "info", 43 | sourcemap: prod ? false : 'inline', 44 | treeShaking: true, 45 | outdir: prod ? 'dist' : 'dev-vault/.obsidian/plugins/custom-classes', 46 | loader: { ".json": "copy" }, 47 | }).catch(() => process.exit(1)); 48 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'obsidian'; 2 | import { 3 | CustomClassesSettings, 4 | CustomClassesSettingsTab 5 | } from './settings'; 6 | import { customClassLivePreviewMode } from './live-preview-mode'; 7 | import { customClassReadMode } from './read-mode'; 8 | 9 | export let plugin: CustomClasses | null = null; 10 | 11 | export default class CustomClasses extends Plugin { 12 | settings: CustomClassesSettings; 13 | 14 | async onload () { 15 | // Expose the plugin globally. 16 | plugin = this; 17 | 18 | // Print console message 19 | console.log(`Loading "Custom Classes" plugin...`); 20 | 21 | // Initialize the settings instance 22 | this.settings = new CustomClassesSettings(this); 23 | await this.settings.init(); 24 | 25 | // Initialize the settings' tab 26 | this.addSettingTab(new CustomClassesSettingsTab(this.app, this)); 27 | 28 | // Start the Live Preview mode renderer 29 | this.registerEditorExtension([customClassLivePreviewMode]); 30 | 31 | // Start the Reading mode renderer 32 | this.registerMarkdownPostProcessor(customClassReadMode); 33 | 34 | // Print console message 35 | console.log(`"Custom Classes" plugin successfully loaded.`); 36 | } 37 | } -------------------------------------------------------------------------------- /src/md-line.ts: -------------------------------------------------------------------------------- 1 | import { ccBlockRegexGlob } from "./utils"; 2 | 3 | /** 4 | * This class contains some utils to deal with Markdown raw text lines 5 | */ 6 | export class MDLine { 7 | 8 | static isEmpty (lineText: string): boolean { 9 | return lineText.trim() === ""; 10 | } 11 | 12 | static isListItem (lineText: string): Array { 13 | let listType = null; 14 | if (/^( *)(\-)( +)(.*)/.test(lineText)) listType = "ul"; 15 | else if (/^( *)(\d+[\.\)])( +)(.*)/.test(lineText)) listType = "ol"; 16 | const isList = listType ? true : false; 17 | return [isList, listType]; 18 | } 19 | 20 | static isCodeBlockBound (lineText: string): boolean { 21 | return lineText.trim().startsWith("```"); 22 | } 23 | 24 | static isTableRow (lineText: string): boolean { 25 | return lineText.trim().startsWith("|") && lineText.trim().endsWith("|"); 26 | } 27 | 28 | static isBlockquote (lineText: string): boolean { 29 | return lineText.trim().startsWith(">"); 30 | } 31 | 32 | static findCustomClassesBlocks (lineText: string): Array { 33 | const customClassesBlocks: Array = []; 34 | [...lineText.matchAll(ccBlockRegexGlob)] 35 | .forEach(m => customClassesBlocks.push(m[0])); 36 | return customClassesBlocks; 37 | } 38 | } -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "+([0-9])?(.{+([0-9]),x}).x", 4 | "main", 5 | "next", 6 | "next-major", 7 | {"name": "beta", "prerelease": true}, 8 | {"name": "alpha", "prerelease": true} 9 | ], 10 | "tagFormat": "${version}", 11 | "plugins": [ 12 | [ 13 | "@semantic-release/commit-analyzer", 14 | { 15 | "releaseRules": [ 16 | { "type": "perf", "release": "patch" } 17 | ] 18 | } 19 | ], 20 | "@semantic-release/release-notes-generator", 21 | "@semantic-release/changelog", 22 | [ 23 | "@semantic-release/npm", 24 | { 25 | "npmPublish": false 26 | } 27 | ], 28 | [ 29 | "@semantic-release/exec", 30 | { 31 | "prepareCmd": "node version-bump.mjs && git add manifest.json versions.json" 32 | } 33 | ], 34 | [ 35 | "@semantic-release/git", 36 | { 37 | "assets": ["package.json", "package-lock.json", "CHANGELOG.md", "manifest.json", "versions.json"], 38 | "message": "chore(release): Custom Classes ${nextRelease.version} [skip ci]" 39 | } 40 | ], 41 | [ 42 | "@semantic-release/github", 43 | { 44 | "assets": [ 45 | "dist/*", 46 | { "path": "obsidian-custom-classes.zip", "name": "obsidian-custom-classes-${nextRelease.version}.zip" } 47 | ] 48 | } 49 | ] 50 | ], 51 | "preset": "angular" 52 | } -------------------------------------------------------------------------------- /dev-vault/.obsidian/snippets/lila-frontmatter.css: -------------------------------------------------------------------------------- 1 | div.meta { 2 | display: inline-block; 3 | font-family: monospace; 4 | background-color: var(--background-secondary); 5 | box-shadow: -1px -1px 8px var(--color-base-30); 6 | border-radius: 15px; 7 | padding: 15px 18px 15px 15px; 8 | transition: opacity 300ms linear, max-height 300ms linear, max-width 300ms linear; 9 | opacity: 0.6; 10 | margin-bottom: 15px; 11 | overflow: hidden; 12 | min-height: 52px; 13 | max-height: 52px; 14 | max-width: 120px; 15 | } 16 | 17 | div.meta:hover { 18 | opacity: 1; 19 | max-height: 800px; 20 | max-width: 800px; 21 | } 22 | 23 | div.meta::before { 24 | content: "Metadata"; 25 | display: inline-block; 26 | margin-left: 5px; 27 | font-weight: 900; 28 | letter-spacing: 0.8px; 29 | margin-bottom: 4px; 30 | font-size: 16px; 31 | 32 | /* Add an opaque background */ 33 | position: relative; 34 | background-color: var(--background-secondary); 35 | z-index: 10; 36 | border-radius: 10px; 37 | 38 | } 39 | 40 | div.meta ul { 41 | margin: 0; 42 | padding-left: 28px; 43 | margin-top: -10em; 44 | opacity: 0; 45 | transition: margin-top 300ms linear, opacity 300ms linear; 46 | white-space: nowrap; 47 | max-height: 700px; 48 | 49 | /* Display a clean scrollbar if the height exceeds 700px */ 50 | overflow-y: scroll; 51 | margin-right: -23px; 52 | padding-right: 8px; 53 | } 54 | 55 | div.meta:hover ul { 56 | margin-top: 0; 57 | opacity: 1; 58 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute ? 2 | 3 | ## Contribute by _**testing**_ Custom Classes 4 | 1) Test the plugin's features 5 | 2) Observe a bug or an improvement that could be made 6 | 3) Report it by [creating an issue](https://github.com/LilaRest/obsidian-custom-classes/issues/new) 7 | 8 | ## Contribute by _**coding**_ Custom Classes 9 | - If you have found a bug or a potential improvement for the plugin and want to code it, I would be happy to accept your PRs! 10 | **Important :** It'd be good to talk about it beforehand to make sure that no one else is working on it. You can [open an issue](https://github.com/LilaRest/obsidian-custom-classes/issues/new) for this. 11 | - If you want to code but don't know where to start : 12 | 1) Check out the issues labelled "[help wanted](https://github.com/LilaRest/obsidian-custom-classes/labels/help%20wanted)". 13 | 2) Check out ~~our roadmap~~ (not available yet) and choose an unassigned task 14 | 15 | **Here are the steps to contribute to the Custom Classes's code :** 16 | 1) Fork this repository 17 | 2) Clone your fork on your computer using `git clone https://github.com//obsidian-custom-classes.git` 18 | 3) In local, navigate into the cloned folder called `obsidian-custom-classes/` 19 | 4) Add the _Custom Classes_ project repository as the "upstream" remote using `git remote add upstream https://github.com/LilaRest/obsidian-custom-classes.git` 20 | 5) Now you can easily pull the new updates on the _Custom Classes_ repository using `git pull upstream main` 21 | 6) Perform changes to your local repository 22 | 7) Commit your changes using `git add -A` + `git commit -m ""` 23 | 8) Push your changes to your fork repository using `git push origin main` 24 | 9) Return to your fork page on Github, refresh the page and you should see an highlighted area that invites you to initiate a Pull Request. (alternatively you can click on the "New pull request" button) 25 | -------------------------------------------------------------------------------- /dev-vault/tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | aliases: "Blabla" 3 | --- 4 | 5 | **Test #1** : [Lila's frontmatter](https://forum.obsidian.md/t/a-frontmatter-that-finally-supports-links-lilas-frontmatter/53087/) should be displayed below 6 | 7 | `class: meta` 8 | - creation:: 2023-01-21T18:55:12 9 | - author:: [[John Doe]] 10 | - parents:: [[Note]], [[Another note]] 11 | - status:: #MayBePartial 12 | 13 | **Test #2** : Should not have any custom class 14 | # Lorem Ipsum 15 | 16 | **Test #3** (heading) : Should have a green background 17 | 18 | `class: green-title` 19 | ## Dolor sit 20 | 21 | **Test #4** (paragraph) : Should became bold and big on hover 22 | 23 | 24 | `class: fancy-text` 25 | I'm the paragraph and you ? 26 | 27 | **Test #5** (Dataview query) : The query result should display well and the `dv-custom` class should be applied to the query block 28 | `class: dv-custom` 29 | ```dataview 30 | LIST 31 | WHERE creation 32 | ``` 33 | 34 | **Test #6** (blockquote) : The `quote` class should be applied to the whole blockquote block and the `inline-quote` class should be applied to the `

` element 35 | `class: quote` 36 | > My blockquote 37 | `class: inline-quote` 38 | 39 | **Test #7** (callouts) : The `out-title` class should be applied to the callout title's `div` and the `out-content` class should be applied to the callout content's div (DOESN'T WORK) 40 | 41 | > [!INFO] Consectetur `class: out-title` 42 | > Lorem ipsum dolor site amet 43 | > `class: out-content` 44 | 45 | **Test #8** (consecutive standalone CC blocks) : `am-i-visible` and `and-me` should be visible and `dv-list` should be hidden and applied to the Dataview block 46 | 47 | `class: am-i-visible` 48 | `class: and-me` 49 | `class:dv-list` 50 | ```dataview 51 | LIST 52 | WHERE type 53 | ``` 54 | 55 | **Test #9** (rendered Markdown) : The inline code-block in the below paragraph should look like this one : `normal code block` 56 | `class: paraa` 57 | I'm the last element `normal code block` of a paragraph 58 | 59 | **Test #10** (table render) : `should-be-visible` must be visible and the `table` class should be applied to the table block and the `cell` class should be applied only to the `azaz` cell 60 | 61 | `class: should-be-visible` 62 | 63 | `class: table` 64 | | aaa | bbbbb | ccc | 65 | | -- | -- | -- | 66 | | az | er | azaz `class: cell` | 67 | | aa | aaa | yoo | 68 | 69 | **Test #11** (whole list) : The `whole-list` class should be applied to the whole below list block and the second `

  • ` element should have `yep`, `yop` and `yup` classes 70 | `class: whole-list` 71 | - first element 72 | - second one `class: yep, yop, yup` which have classes 73 | - third 74 | - last 75 | 76 | **Test #12** (paragraph inline class) : The below paragraph should have the `boo` class applied to its `

    ` element 77 | Discrete like `class: boo` a ghost 78 | 79 | **Test #13** (latest class) : A cc block at the end of the document should remain visible because not applied to any block or element 80 | `class: visible-latest` -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting } from "obsidian"; 2 | import CustomClasses from "./main"; 3 | 4 | export interface SettingsData { 5 | [index: string]: any; 6 | } 7 | 8 | const DEFAULT_SETTINGS: SettingsData = { 9 | }; 10 | 11 | export class CustomClassesSettings { 12 | plugin: CustomClasses; 13 | _data: SettingsData; 14 | data: SettingsData; 15 | 16 | constructor (plugin: CustomClasses) { 17 | this.plugin = plugin; 18 | this._data = DEFAULT_SETTINGS; 19 | } 20 | 21 | async init () { 22 | // Load data from the archive file 23 | await this.load(); 24 | 25 | // Set up auto-save for settings 26 | const dataProxy = { 27 | get: function (target: any, key: string): any { 28 | if (typeof target[key] === "object" && target[key] !== null) { 29 | if (key === "_target") return target; 30 | return new Proxy(target[key], dataProxy); 31 | } 32 | return target[key]; 33 | }.bind(this), 34 | set: async function (target: any, prop: string, value: any) { 35 | target[prop] = value; 36 | await this.store(); 37 | return true; 38 | }.bind(this) 39 | }; 40 | this.data = new Proxy(this._data, dataProxy); 41 | } 42 | 43 | async load () { 44 | let dataJSON = await this.plugin.loadData(); 45 | dataJSON = dataJSON ? dataJSON : { settings: DEFAULT_SETTINGS }; 46 | this._data = Object.assign({}, DEFAULT_SETTINGS, dataJSON.settings); 47 | } 48 | 49 | async store () { 50 | await this.plugin.saveData({ 51 | settings: this._data 52 | }); 53 | } 54 | 55 | get (key: string): any { 56 | return this.data[key]; 57 | } 58 | 59 | set (key: string, value: any) { 60 | this.data[key] = value; 61 | } 62 | } 63 | 64 | export class CustomClassesSettingsTab extends PluginSettingTab { 65 | plugin: CustomClasses; 66 | 67 | constructor (app: App, plugin: CustomClasses) { 68 | super(app, plugin); 69 | this.plugin = plugin; 70 | } 71 | 72 | async display (): Promise { 73 | let { containerEl } = this; 74 | 75 | containerEl.empty(); 76 | 77 | // Configuration section 78 | containerEl.createEl("h2", { text: "Documentation", cls: "settings-header" }); 79 | 80 | containerEl.createEl("p", { text: "You can learn more about the usage of the plugin by reading its " }).createEl("a", { href: "https://github.com/LilaRest/obsidian-custom-classes", text: "documentation page." }); 81 | 82 | 83 | // Configuration section 84 | // containerEl.createEl("h2", { text: "Configurations", cls: "settings-header" }); 85 | 86 | // Support section's title 87 | containerEl.createEl("h2", { text: "Support my work", cls: "settings-header" }); 88 | 89 | // Support message 90 | containerEl.createEl("p", { text: "That plugin is provided for free to everyone under the MIT license. If it has been helpful to you, you can thank me for free by :" }); 91 | const supportMethods = containerEl.createEl("ul"); 92 | supportMethods.createEl("li", { text: "Following me on Twitter " }).createEl("a", { href: "https://twitter.com/LilaRest", text: "twitter.com/LilaRest" }); 93 | supportMethods.createEl("li", { text: "Following me on Github " }).createEl("a", { href: "https://github.com/LilaRest", text: "github.com/LilaRest" }); 94 | supportMethods.createEl("li", { text: "Starring that plugin " }).createEl("a", { href: "https://github.com/LilaRest/obsidian-custom-classes", text: "LilaRest/obsidian-custom-classes" }); 95 | containerEl.createEl("p", { text: "Also, I accept donations on my personal website : " }).createEl("a", { href: "https://lila.rest/donations", text: "https://lila.rest/donations" }); 96 | } 97 | } -------------------------------------------------------------------------------- /dev-vault/.obsidian/plugins/dataview/styles.css: -------------------------------------------------------------------------------- 1 | /** Live Preview padding fixes, specifically for DataviewJS custom HTML elements. */ 2 | .is-live-preview .block-language-dataviewjs > p, .is-live-preview .block-language-dataviewjs > span { 3 | line-height: 1.0; 4 | } 5 | 6 | .block-language-dataview { 7 | overflow-y: auto; 8 | } 9 | 10 | /*****************/ 11 | /** Table Views **/ 12 | /*****************/ 13 | 14 | /* List View Default Styling; rendered internally as a table. */ 15 | .table-view-table { 16 | width: 100%; 17 | } 18 | 19 | .table-view-table > thead > tr, .table-view-table > tbody > tr { 20 | margin-top: 1em; 21 | margin-bottom: 1em; 22 | text-align: left; 23 | } 24 | 25 | .table-view-table > tbody > tr:hover { 26 | background-color: var(--text-selection) !important; 27 | } 28 | 29 | .table-view-table > thead > tr > th { 30 | font-weight: 700; 31 | font-size: larger; 32 | border-top: none; 33 | border-left: none; 34 | border-right: none; 35 | border-bottom: solid; 36 | 37 | max-width: 100%; 38 | } 39 | 40 | .table-view-table > tbody > tr > td { 41 | text-align: left; 42 | border: none; 43 | font-weight: 400; 44 | max-width: 100%; 45 | } 46 | 47 | .table-view-table ul, .table-view-table ol { 48 | margin-block-start: 0.2em !important; 49 | margin-block-end: 0.2em !important; 50 | } 51 | 52 | /** Rendered value styling for any view. */ 53 | .dataview-result-list-root-ul { 54 | padding: 0em !important; 55 | margin: 0em !important; 56 | } 57 | 58 | .dataview-result-list-ul { 59 | margin-block-start: 0.2em !important; 60 | margin-block-end: 0.2em !important; 61 | } 62 | 63 | /** Generic grouping styling. */ 64 | .dataview.result-group { 65 | padding-left: 8px; 66 | } 67 | 68 | /*******************/ 69 | /** Inline Fields **/ 70 | /*******************/ 71 | 72 | .dataview.inline-field-key { 73 | padding-left: 8px; 74 | padding-right: 8px; 75 | font-family: var(--font-monospace); 76 | background-color: var(--background-primary-alt); 77 | color: var(--text-nav-selected); 78 | } 79 | 80 | .dataview.inline-field-value { 81 | padding-left: 8px; 82 | padding-right: 8px; 83 | font-family: var(--font-monospace); 84 | background-color: var(--background-secondary-alt); 85 | color: var(--text-nav-selected); 86 | } 87 | 88 | .dataview.inline-field-standalone-value { 89 | padding-left: 8px; 90 | padding-right: 8px; 91 | font-family: var(--font-monospace); 92 | background-color: var(--background-secondary-alt); 93 | color: var(--text-nav-selected); 94 | } 95 | 96 | /***************/ 97 | /** Task View **/ 98 | /***************/ 99 | 100 | .dataview.task-list-item, .dataview.task-list-basic-item { 101 | margin-top: 3px; 102 | margin-bottom: 3px; 103 | transition: 0.4s; 104 | } 105 | 106 | .dataview.task-list-item:hover, .dataview.task-list-basic-item:hover { 107 | background-color: var(--text-selection); 108 | box-shadow: -40px 0 0 var(--text-selection); 109 | cursor: pointer; 110 | } 111 | 112 | /*****************/ 113 | /** Error Views **/ 114 | /*****************/ 115 | 116 | div.dataview-error-box { 117 | width: 100%; 118 | min-height: 150px; 119 | display: flex; 120 | align-items: center; 121 | justify-content: center; 122 | border: 4px dashed var(--background-secondary); 123 | } 124 | 125 | .dataview-error-message { 126 | color: var(--text-muted); 127 | text-align: center; 128 | } 129 | 130 | /*************************/ 131 | /** Additional Metadata **/ 132 | /*************************/ 133 | 134 | .dataview.small-text { 135 | font-size: smaller; 136 | color: var(--text-muted); 137 | margin-left: 3px; 138 | } 139 | 140 | .dataview.small-text::before { 141 | content: "("; 142 | } 143 | 144 | .dataview.small-text::after { 145 | content: ")"; 146 | } 147 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.6.1](https://github.com/LilaRest/obsidian-custom-classes/compare/2.6.0...2.6.1) (2023-02-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * dot format was not rendered in Live Preview mode ([cf6d00b](https://github.com/LilaRest/obsidian-custom-classes/commit/cf6d00bc04b720b998fa96bd48a0fe8d6a2aead8)) 7 | * repair regex matching wrong patterns of CC blocks + merge those ones to prevent code repetition ([9761c90](https://github.com/LilaRest/obsidian-custom-classes/commit/9761c90c8b560e9f52424079d4f6badd7eccf49d)), closes [#5](https://github.com/LilaRest/obsidian-custom-classes/issues/5) [#7](https://github.com/LilaRest/obsidian-custom-classes/issues/7) 8 | 9 | # [2.6.0](https://github.com/LilaRest/obsidian-custom-classes/compare/2.5.0...2.6.0) (2023-02-23) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * partially rewrite the read mode renderer to fix: [#2](https://github.com/LilaRest/obsidian-custom-classes/issues/2) and makes code cleaner ([3a910d9](https://github.com/LilaRest/obsidian-custom-classes/commit/3a910d9315399bc1427bc4f9e089d12f1aad139d)) 15 | 16 | 17 | ### Features 18 | 19 | * add an event shorter way to add custom classes by simply starting a code block with a dot ([3ff6bea](https://github.com/LilaRest/obsidian-custom-classes/commit/3ff6beaa83fe55393ee774b0d97ba0fb0a3dc5b5)) 20 | 21 | # [2.5.0](https://github.com/LilaRest/obsidian-custom-classes/compare/2.4.3...2.5.0) (2023-02-06) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * remove remaining console logs ([3bb386f](https://github.com/LilaRest/obsidian-custom-classes/commit/3bb386f4dff4f36a2111637fe5d1bc66ac2091a6)) 27 | 28 | 29 | ### Features 30 | 31 | * support short-hand "cls:" prefix in Read mode ([ac4fa09](https://github.com/LilaRest/obsidian-custom-classes/commit/ac4fa09a1325307fc015fd352a52eb7cecb080b3)) 32 | 33 | ## [2.4.3](https://github.com/LilaRest/obsidian-custom-classes/compare/2.4.2...2.4.3) (2023-02-06) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * wrong versions ([32fb8ae](https://github.com/LilaRest/obsidian-custom-classes/commit/32fb8aea3cbae1e97b10fc4eabddcd6fd70ae459)) 39 | 40 | # [2.4.0](https://github.com/LilaRest/obsidian-custom-classes/compare/2.3.0...2.4.0) (2023-02-05) 41 | 42 | 43 | ### Features 44 | 45 | * improve detection custom classes block context in Read mode ([4f2bafe](https://github.com/LilaRest/obsidian-custom-classes/commit/4f2bafe451d6dbab5737c5b37a3ed6dee079d987)) 46 | * partially rewrite LP support to use the Read mode renderer to render nested classes blocks ([39ef717](https://github.com/LilaRest/obsidian-custom-classes/commit/39ef717926194c650cea9e5e5b4915ad0659e752)) 47 | * rewrite post processor to support inline classes & non-rendered elements more efficiently ([d819707](https://github.com/LilaRest/obsidian-custom-classes/commit/d819707c7d78c5b657b9eda303135256ca8d5aba)) 48 | 49 | # [2.3.0](https://github.com/LilaRest/obsidian-custom-classes/compare/2.2.0...2.3.0) (2023-02-02) 50 | 51 | 52 | ### Features 53 | 54 | * add support for multiple classes and inline custom classes in Live Preview mode ([f9361c7](https://github.com/LilaRest/obsidian-custom-classes/commit/f9361c7fa9048736fa5508ad09061c5e1138b09f)) 55 | * start rewriting live preview support to support multiple classes and inline custom classes ([c6a4463](https://github.com/LilaRest/obsidian-custom-classes/commit/c6a4463d0ea0528de240624f403b36470b0b1d1c)) 56 | 57 | 58 | ### Reverts 59 | 60 | * remove the dynamic anchor configuration to keep things simple and improve code performances ([ab6e4e2](https://github.com/LilaRest/obsidian-custom-classes/commit/ab6e4e2868456e7e56f7a87cb394b2635367fa66)) 61 | 62 | # [2.2.0](https://github.com/LilaRest/obsidian-custom-classes/compare/2.1.0...2.2.0) (2023-02-02) 63 | 64 | 65 | ### Features 66 | 67 | * **read mode:** support multiple classes, inline custom classes, and improve general behavior ([e84e6b8](https://github.com/LilaRest/obsidian-custom-classes/commit/e84e6b8a9188cadb257d61e7c23456e6d28d682a)) 68 | 69 | # [2.1.0](https://github.com/LilaRest/obsidian-custom-classes/compare/2.0.0...2.1.0) (2023-02-01) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * version of package.json ([85ab8e1](https://github.com/LilaRest/obsidian-custom-classes/commit/85ab8e1b15457856f3b6f414873c5137405c3732)) 75 | 76 | 77 | ### Features 78 | 79 | * add support for custom class on tables in Read mode ([56758f5](https://github.com/LilaRest/obsidian-custom-classes/commit/56758f577cffae5d3c1e7192c3f05b7a8978890a)) 80 | * allow table / paragraph render in Read mode + replace standard-version by semantic-version ([dca7742](https://github.com/LilaRest/obsidian-custom-classes/commit/dca7742337f1025416b818a0a23aaa1c7d60240f)) 81 | * make the table support generic to also paragraphs or any other element not properly rendered ([2484135](https://github.com/LilaRest/obsidian-custom-classes/commit/24841355679d93484653b97df2a5f5fd8102580c)) 82 | 83 | # **2.0.0 and before** 84 | Until its `2.0.0` release the _Custom Classes_ plugin was using [standard-version](https://github.com/conventional-changelog/standard-version) + [conventional-changelog-reader-action](https://github.com/artlaman/conventional-changelog-reader-action) to manage releases and CHANGELOG generation. 85 | 86 | From its `2.0.1` release the plugin is using [semantic-release] to do so, which generate logs using a different format. 87 | 88 | Logs have so been split in two files, and the CHANGELOG until the `2.0.0` release can be found here : [CHANGELOG-until-v2.0.0.md](https://github.com/LilaRest/obsidian-custom-classes/blob/main/CHANGELOG-until-v2.0.0.md) 89 | -------------------------------------------------------------------------------- /CHANGELOG-until-v2.0.0.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.0.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.15.1...2.0.0) (2023-01-30) 6 | 7 | 8 | ### ⚠ BREAKING CHANGES 9 | 10 | * No more compatibility mode 11 | 12 | ### Features 13 | 14 | * fake feature to test GH workflow ([20be7e7](https://github.com/LilaRest/obsidian-custom-classes/commit/20be7e73a5becec07b1ab16db603abb5bb3148fb)) 15 | * totally remove compatibility mode and set it as default behavior to make things clearer ([a67299d](https://github.com/LilaRest/obsidian-custom-classes/commit/a67299d4bc9c9255f0bbeb17b9018b97ee92d0be)) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * fake fix to test CI ([ac00273](https://github.com/LilaRest/obsidian-custom-classes/commit/ac002734f58b8713d7f29b503954f9ee2b95db83)) 21 | * fake fix to test CI ([c726363](https://github.com/LilaRest/obsidian-custom-classes/commit/c726363b393e5d89ccff066c9d67c1c9cf64d540)) 22 | * fake fix to test GH workflow on patch versions ([07d0abc](https://github.com/LilaRest/obsidian-custom-classes/commit/07d0abcea8f5f9d1b3af530b169d1144ecca9d0a)) 23 | 24 | ### [1.16.3](https://github.com/LilaRest/obsidian-custom-classes/compare/1.16.2...1.16.3) (2023-01-30) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * fake fix to test CI ([ac00273](https://github.com/LilaRest/obsidian-custom-classes/commit/ac002734f58b8713d7f29b503954f9ee2b95db83)) 30 | 31 | ### [1.16.2](https://github.com/LilaRest/obsidian-custom-classes/compare/1.16.1...1.16.2) (2023-01-30) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * fake fix to test CI ([c726363](https://github.com/LilaRest/obsidian-custom-classes/commit/c726363b393e5d89ccff066c9d67c1c9cf64d540)) 37 | 38 | ### [1.16.1](https://github.com/LilaRest/obsidian-custom-classes/compare/1.16.0...1.16.1) (2023-01-30) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * fake fix to test GH workflow on patch versions ([07d0abc](https://github.com/LilaRest/obsidian-custom-classes/commit/07d0abcea8f5f9d1b3af530b169d1144ecca9d0a)) 44 | 45 | ## [1.16.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.15.1...1.16.0) (2023-01-30) 46 | 47 | 48 | ### Features 49 | 50 | * fake feature to test GH workflow ([20be7e7](https://github.com/LilaRest/obsidian-custom-classes/commit/20be7e73a5becec07b1ab16db603abb5bb3148fb)) 51 | 52 | ### [1.15.1](https://github.com/LilaRest/obsidian-custom-classes/compare/1.15.0...1.15.1) (2023-01-29) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * limit the scope of the post processor to prevent broking plugins that use renderMarkdown() ([62276dd](https://github.com/LilaRest/obsidian-custom-classes/commit/62276ddc3acaf720326244b3126e4d12dd0b5ab3)), closes [#1](https://github.com/LilaRest/obsidian-custom-classes/issues/1) 58 | 59 | ## [1.15.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.14.0...1.15.0) (2023-01-29) 60 | 61 | 62 | ### Features 63 | 64 | * fake commit to test release ([0042d95](https://github.com/LilaRest/obsidian-custom-classes/commit/0042d95028b0ffdf506110f30a01b55fed904a78)) 65 | 66 | ## [1.13.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.13.0) (2023-01-29) 67 | 68 | 69 | ### Features 70 | 71 | * also consider a the lines range as active if some changes of the transaction are touching it ([8fab0c2](https://github.com/LilaRest/obsidian-custom-classes/commit/8fab0c2fa7c6be777f2b91fcd84a1610877c20ca)) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * remove of live preview mode code file ([d31125d](https://github.com/LilaRest/obsidian-custom-classes/commit/d31125d20f0efb8f2dc69f80d8fad2b0b8f41fd1)) 77 | 78 | ## [1.12.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.12.0) (2023-01-29) 79 | 80 | 81 | ### Features 82 | 83 | * allow to add classes to multiline code blocks and tables in Live Prev. with compatibility mode ([901cb2e](https://github.com/LilaRest/obsidian-custom-classes/commit/901cb2e863214b84de2ca6c7ca89628b50e2267d)) 84 | 85 | ## [1.11.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.11.0) (2023-01-29) 86 | 87 | 88 | ### Features 89 | 90 | * entire rewrite of the Live Preview support ([0c5f5c8](https://github.com/LilaRest/obsidian-custom-classes/commit/0c5f5c877e308033622f82137e1ff366e95f9873)) 91 | * in Live Preview, ignore custom class block that doesn't target any non-empty element ([a39f132](https://github.com/LilaRest/obsidian-custom-classes/commit/a39f1329cff79d5b2f3c3a614068add233a58b19)) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * empty line / line break detection was also detecting paragraphs ([60095b1](https://github.com/LilaRest/obsidian-custom-classes/commit/60095b1d9a06898a744c012116240406b5401be9)) 97 | 98 | ## [1.10.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.10.0) (2023-01-28) 99 | 100 | 101 | ### Features 102 | 103 | * finish the compatibility mode ([25ef2f8](https://github.com/LilaRest/obsidian-custom-classes/commit/25ef2f87a0c29186fc600b2ad1f08de0913fa733)) 104 | * live Preview support : Unhide blocks if user has switched from Live Preview to Source mode ([9def645](https://github.com/LilaRest/obsidian-custom-classes/commit/9def6455ab9d71a4de80cdb61de287775ea790ab)) 105 | 106 | ## [1.9.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.9.0) (2023-01-27) 107 | 108 | 109 | ### Features 110 | 111 | * finish the compatibility mode ([25ef2f8](https://github.com/LilaRest/obsidian-custom-classes/commit/25ef2f87a0c29186fc600b2ad1f08de0913fa733)) 112 | 113 | ## [1.8.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.8.0) (2023-01-26) 114 | 115 | ## [1.7.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.7.0) (2023-01-26) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * patch Typescript errors ([7f1d73c](https://github.com/LilaRest/obsidian-custom-classes/commit/7f1d73c8f10ebf1c805c63995e231f8e012c4b8f)) 121 | 122 | ## [1.6.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.6.0) (2023-01-26) 123 | 124 | 125 | ### Features 126 | 127 | * add Live Preview support (beta) ([fe22a90](https://github.com/LilaRest/obsidian-custom-classes/commit/fe22a907ff73c81aa307752fe1624da5ada1a0a9)) 128 | * expose the plugin globally + replace hardcoded anchor by the one defined in settings ([da1041d](https://github.com/LilaRest/obsidian-custom-classes/commit/da1041dc185928e7fe983246feef93a017a7c536)) 129 | * implement settings + Allow users to customize the anchor / prefix string from settings ([a4f24b7](https://github.com/LilaRest/obsidian-custom-classes/commit/a4f24b788a580540bc5037df4bc5f36001cb43de)) 130 | * improve the Reading mode rendered code (increased performances) ([524b98b](https://github.com/LilaRest/obsidian-custom-classes/commit/524b98bb701f216a070ecbdce0d11769f89a0855)) 131 | 132 | 133 | ### Bug Fixes 134 | 135 | * wrong static anchor key + unuseful code to test code blocks ([657d1a8](https://github.com/LilaRest/obsidian-custom-classes/commit/657d1a818a05faf427f99aebc3b06eb652910dcb)) 136 | 137 | ## [1.5.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.5.0) (2023-01-24) 138 | 139 | 140 | ### Features 141 | 142 | * allow editing custom classes without reloading Obsidian by re-rendering every custom class ([3768b74](https://github.com/LilaRest/obsidian-custom-classes/commit/3768b7422a69746f0da4071c92957527c2c5418a)) 143 | 144 | ### [1.4.1](https://github.com/LilaRest/obsidian-custom-classes/compare/1.4.0...1.4.1) (2023-01-24) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * empty css file ([b796493](https://github.com/LilaRest/obsidian-custom-classes/commit/b796493610e9a47bb2df7deafe93f736af31b3ad)) 150 | 151 | ## [1.4.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.3.6...1.4.0) (2023-01-24) 152 | 153 | 154 | ### Features 155 | 156 | * fake update to test CI/CD ([a939e20](https://github.com/LilaRest/obsidian-custom-classes/commit/a939e20d3bcd7660b2a04589d11bc920ef4285c3)) 157 | 158 | ### [1.3.6](https://github.com/LilaRest/obsidian-custom-classes/compare/1.3.5...1.3.6) (2023-01-23) 159 | 160 | ### [1.3.5](https://github.com/LilaRest/obsidian-custom-classes/compare/1.3.4...1.3.5) (2023-01-23) 161 | 162 | ### [1.3.4](https://github.com/LilaRest/obsidian-custom-classes/compare/1.3.3...1.3.4) (2023-01-23) 163 | 164 | ### [1.3.3](https://github.com/LilaRest/obsidian-custom-classes/compare/1.3.2...1.3.3) (2023-01-23) 165 | 166 | ### [1.3.2](https://github.com/LilaRest/obsidian-custom-classes/compare/1.3.1...1.3.2) (2023-01-23) 167 | 168 | ### [1.3.1](https://github.com/LilaRest/obsidian-custom-classes/compare/1.3.0...1.3.1) (2023-01-23) 169 | 170 | 171 | ### Bug Fixes 172 | 173 | * fix css file wrong name ([d6788d0](https://github.com/LilaRest/obsidian-custom-classes/commit/d6788d0d6d9cc1ed7132775b73aad1954da699e7)) 174 | 175 | ## [1.3.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.2.0...1.3.0) (2023-01-23) 176 | 177 | 178 | ### Features 179 | 180 | * tiny update to test CI/CD ([75570a1](https://github.com/LilaRest/obsidian-custom-classes/commit/75570a16be87722d723ee5f7938bc9d28ed4416e)) 181 | 182 | ## [1.2.0](https://github.com/LilaRest/obsidian-custom-classes/compare/1.1.0...1.2.0) (2023-01-23) 183 | 184 | 185 | ### Features 186 | 187 | * rename 'classname' anchor by 'class' as seen in the docs ([d519058](https://github.com/LilaRest/obsidian-custom-classes/commit/d519058a63c6df4146d9b99c4a97562bf047d05e)) 188 | 189 | ## 1.1.0 (2023-01-23) 190 | 191 | 192 | ### Features 193 | 194 | * first working version ([11332f6](https://github.com/LilaRest/obsidian-custom-classes/commit/11332f699883825b1fab6d90259479421e8b4f05)) 195 | -------------------------------------------------------------------------------- /src/live-preview-mode.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownRenderer } from "obsidian"; 2 | import { 3 | Extension, 4 | RangeSetBuilder, 5 | StateField, 6 | Transaction, 7 | } from "@codemirror/state"; 8 | import { 9 | Decoration, 10 | DecorationSet, 11 | EditorView, 12 | WidgetType, 13 | } from "@codemirror/view"; 14 | import { MDLine } from "./md-line"; 15 | import { retrieveCustomClasses } from "./utils"; 16 | 17 | /** 18 | * This widget create a Markdown render in Read mode format and properly apply the given classes. 19 | */ 20 | class RendererWidget extends WidgetType { 21 | customClasses: Array; 22 | linesText: Array; 23 | 24 | constructor (customClasses: Array, linesText: Array) { 25 | super(); 26 | this.customClasses = customClasses; 27 | this.linesText = linesText; 28 | } 29 | 30 | eq (widget: RendererWidget): boolean { 31 | if (widget.customClasses.every((v, i) => v === this.customClasses[i])) { 32 | if (widget.linesText.every((v, i) => v === this.linesText[i])) { 33 | return true; 34 | } 35 | } 36 | return false; 37 | } 38 | 39 | toDOM (view: EditorView): HTMLElement { 40 | 41 | // Create the Read mode render element 42 | const ccRenderer = document.createElement("div"); 43 | ccRenderer.classList.add("cc-container"); 44 | 45 | // 46 | const renderedMarkdown = document.createElement("div"); 47 | renderedMarkdown.classList.add( 48 | "markdown-rendered", 49 | "cc-renderer", 50 | ...this.customClasses 51 | ); 52 | ccRenderer.appendChild(renderedMarkdown); 53 | 54 | // Render markdown into the custom class block 55 | MarkdownRenderer.renderMarkdown( 56 | this.linesText.join("\n"), 57 | renderedMarkdown, 58 | "", 59 | //@ts-ignore 60 | null); 61 | 62 | return ccRenderer; 63 | } 64 | 65 | ignoreEvent (e: Event | MouseEvent) { 66 | 67 | // Support clicks on links 68 | if (e.type === "mousedown") { 69 | e = e as MouseEvent; 70 | //@ts-ignore 71 | if (e.target?.nodeName === "A") { 72 | e.preventDefault(); 73 | return true; 74 | } 75 | } 76 | return false; 77 | } 78 | } 79 | 80 | function getTargettedLinesNumber (doc: any, lineNumber: number): number { 81 | let numberOfLines = 0; 82 | 83 | // Retrieve first line 84 | if (doc.lines >= lineNumber + 1) { 85 | 86 | const firstLine = doc.line(lineNumber + 1); 87 | 88 | // Return numberOfLine if the firstLine is a line break or empty line 89 | if (MDLine.isEmpty(firstLine.text)) return numberOfLines; 90 | 91 | // Return numberOfLine if the firstLine is also a standalone custom classes block 92 | const firstLineCCBlocks = MDLine.findCustomClassesBlocks(firstLine.text); 93 | if (firstLineCCBlocks.length === 1 && firstLine.text.trim().replace(firstLineCCBlocks[0], "") === "") return numberOfLines; 94 | 95 | // Else increment the number of targetted lines 96 | numberOfLines++; 97 | 98 | // If first line is a list item 99 | const [firstLineIsList, firstListListType] = MDLine.isListItem(firstLine.text); 100 | if (firstLineIsList) { 101 | 102 | // Iterate over next lines 103 | let lastListType = firstListListType; 104 | for (let offset = 1; lineNumber + offset <= doc.lines; offset++) { 105 | 106 | // Retrieve next line 107 | const nextLine = doc.line(firstLine.number + offset); 108 | 109 | // Return numberOfLines if the nextLine is a line break or empty line 110 | if (MDLine.isEmpty(nextLine.text)) return numberOfLines; 111 | 112 | // Return numberOfLine if the nextLine is also a standalone custom classes block 113 | const nextLineCCBlocks = MDLine.findCustomClassesBlocks(nextLine.text); 114 | if (nextLineCCBlocks.length === 1 && nextLine.text.trim().replace(nextLineCCBlocks[0], "") === "") return numberOfLines; 115 | 116 | // If nextLine is a list item 117 | const [nextLineIsList, nextListListType] = MDLine.isListItem(nextLine.text); 118 | if (nextLineIsList) { 119 | 120 | // Return numberOfLines if the listType has changed 121 | if (lastListType !== nextListListType) return numberOfLines; 122 | 123 | // Else simply increment the numberOfLines 124 | numberOfLines++; 125 | 126 | // And update the last list item type 127 | lastListType = nextListListType; 128 | } 129 | 130 | // Else return the numberOfLines 131 | else return numberOfLines; 132 | } 133 | } 134 | 135 | // Else if first line is a multiline code block bounds 136 | else if (MDLine.isCodeBlockBound(firstLine.text)) { 137 | 138 | // Iterate over next lines 139 | for (let offset = 1; lineNumber + offset <= doc.lines; offset++) { 140 | 141 | // Retrieve next line 142 | const nextLine = doc.line(firstLine.number + offset); 143 | 144 | // Increment the number of Lines 145 | numberOfLines++; 146 | 147 | // Return numberOfLines if the other bound is encoutered 148 | if (MDLine.isCodeBlockBound(nextLine.text)) return numberOfLines; 149 | } 150 | } 151 | 152 | // Else if first line is a table row 153 | else if (MDLine.isTableRow(firstLine.text)) { 154 | 155 | // Iterate over next lines 156 | for (let offset = 1; lineNumber + offset <= doc.lines; offset++) { 157 | 158 | // Retrieve next line 159 | const nextLine = doc.line(firstLine.number + offset); 160 | 161 | // Return if the nextLine is not anymore a table line 162 | if (!MDLine.isTableRow(nextLine.text)) return numberOfLines; 163 | 164 | // Else increment the numberOfLines 165 | numberOfLines++; 166 | } 167 | } 168 | 169 | else if (MDLine.isBlockquote(firstLine.text)) { 170 | // Iterate over next lines 171 | for (let offset = 1; lineNumber + offset <= doc.lines; offset++) { 172 | 173 | // Retrieve next line 174 | const nextLine = doc.line(firstLine.number + offset); 175 | 176 | // Return numberOfLines if the nextLine is a line break or empty line 177 | if (MDLine.isEmpty(nextLine.text)) return numberOfLines; 178 | 179 | // Else increment the numberOfLines 180 | numberOfLines++; 181 | } 182 | } 183 | } 184 | 185 | // Else return the number of lines 186 | return numberOfLines; 187 | } 188 | 189 | 190 | 191 | function isRangeActive (tx: Transaction, from: number, to: number): boolean { 192 | let isActive = false; 193 | 194 | // Detect if selection is in the concerned range 195 | if (tx.selection) { 196 | for (const range of tx.selection?.ranges) { 197 | if (range.from >= from && range.to <= to) { 198 | isActive = true; 199 | break; 200 | } 201 | } 202 | } 203 | 204 | // Detect if changes are touching the concerned range 205 | if (tx.changes.touchesRange(from, to)) { 206 | isActive = true; 207 | } 208 | return isActive; 209 | } 210 | 211 | 212 | export const customClassLivePreviewMode = StateField.define({ 213 | 214 | create (state): DecorationSet { 215 | return Decoration.none; 216 | }, 217 | 218 | update (oldState: DecorationSet, tx: Transaction): DecorationSet { 219 | const builder = new RangeSetBuilder(); 220 | 221 | // If Live Preview mode is used 222 | const sourceViewEl = document.querySelector("div.markdown-source-view"); 223 | if (sourceViewEl && sourceViewEl.classList.contains("is-live-preview")) { 224 | 225 | // Loop over each line of the Markdown note 226 | let nextJump = 1; 227 | for (let i = 1; i <= tx.state.doc.lines; i += nextJump) { 228 | nextJump = 1; 229 | 230 | // Retrieve the line object 231 | const line = tx.state.doc.line(i); 232 | 233 | // Set up many vars that will be filled and then used to call the renderer widget 234 | let replacedRangeFrom = line.from; 235 | let replacedRangeTo = line.to; 236 | let renderedRangeFrom = line.from; 237 | let renderedRangeTo = line.to; 238 | let customClasses: Array = []; 239 | let doNotRender = false; 240 | 241 | // Retrieve the Custom Classes blocks contained in that line 242 | const ccBlocks = MDLine.findCustomClassesBlocks(line.text); 243 | 244 | if (ccBlocks.length > 0) { 245 | 246 | // If the line is a standalone ccBlock 247 | if (ccBlocks.length === 1 && line.text.trim().replace(ccBlocks[0], "") === "") { 248 | 249 | // Retrieve the number of lines targetted by the standalone ccBlock 250 | const targettedLinesNumber = getTargettedLinesNumber(tx.state.doc, line.number); 251 | 252 | // If the custom class block targets some lines 253 | if (targettedLinesNumber > 0) { 254 | 255 | // Retrieve the custom classes 256 | customClasses = retrieveCustomClasses(ccBlocks[0]); 257 | 258 | // Update replaced range to 259 | replacedRangeTo = tx.state.doc.line(line.number + targettedLinesNumber).to; 260 | 261 | // Update the renderer range to and from 262 | renderedRangeTo = replacedRangeTo; 263 | renderedRangeFrom = tx.state.doc.line(line.number + 1).from; 264 | 265 | // Jump already processed lines 266 | nextJump = targettedLinesNumber; 267 | } 268 | 269 | else doNotRender = true; 270 | } 271 | 272 | // If the content of the replaced range is not active 273 | if (!isRangeActive(tx, replacedRangeFrom, replacedRangeTo)) { 274 | 275 | // Initiate the render 276 | if (!doNotRender) { 277 | builder.add( 278 | replacedRangeFrom, 279 | replacedRangeTo, 280 | Decoration.replace({ 281 | widget: new RendererWidget( 282 | customClasses, 283 | tx.state.doc.slice( 284 | renderedRangeFrom, 285 | renderedRangeTo 286 | //@ts-ignore 287 | ).text 288 | ) 289 | }) 290 | ); 291 | } 292 | } 293 | } 294 | 295 | } 296 | } 297 | return builder.finish(); 298 | }, 299 | 300 | provide (field: StateField): Extension { 301 | return EditorView.decorations.from(field); 302 | } 303 | }); -------------------------------------------------------------------------------- /src/read-mode.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownRenderer } from "obsidian"; 2 | import { isCustomClassBlock, retrieveCustomClasses } from "./utils"; 3 | 4 | /** 5 | * This function retrieves and returns all the CC blocks from a given element 6 | * @param block 7 | * @returns ccBlocks 8 | */ 9 | function getAllCCBlocks (block: HTMLElement): Array { 10 | const ccBlocks: Array = []; 11 | block.querySelectorAll('code').forEach(e => { 12 | if (isCustomClassBlock(e)) ccBlocks.push(e); 13 | }); 14 | return ccBlocks; 15 | } 16 | 17 | /** 18 | * This function applies the custom classes contained in a given ccBlock to a given targetBlock 19 | * @param targetBlock 20 | * @param ccBlock 21 | */ 22 | function applyLastCC (targetBlock: HTMLElement, ccBlock: HTMLElement) { 23 | 24 | // Ignore standalone classes and footer of the note 25 | if (!targetBlock.getAttribute("cc-standalone") && !targetBlock.classList.contains("mod-footer")) { 26 | 27 | // Add the custom classes 28 | const classes = retrieveCustomClasses(ccBlock.innerText); 29 | targetBlock.classList.add(...classes); 30 | targetBlock.setAttribute("cc-classes", classes.join(",")); 31 | 32 | // Remove the classBlock element from the render; 33 | if (ccBlock.parentElement) { 34 | 35 | const htmlWithoutCCBlock = ccBlock.parentElement.innerHTML.replace(ccBlock.outerHTML, "").trim(); 36 | 37 | // If the CC block was the only element of the parent, hide the whole parent 38 | if (htmlWithoutCCBlock === "") ccBlock.parentElement.style.display = "none"; 39 | 40 | // Else only hide the CC block 41 | else ccBlock.style.display = "none"; 42 | } 43 | } 44 | } 45 | 46 | function isLineBreak (element: ChildNode | HTMLElement | null): boolean { 47 | return Boolean(element && (element.nodeValue === "\n" || element.nodeName === "BR")); 48 | } 49 | 50 | 51 | function process (blocksContainer: HTMLElement, element: HTMLElement, callFromLivePreview: boolean) { 52 | 53 | // If the element has been inserted 54 | if (blocksContainer.contains(element)) { 55 | 56 | // This variable will hold the latest standalone CC block 57 | let lastCCBlock: null | HTMLElement = null; 58 | 59 | // Reset applied custom classes 60 | for (const block of blocksContainer.querySelectorAll("[cc-classes]")) { 61 | const classes = block.getAttribute("cc-classes"); 62 | if (classes?.trim()) block.classList.remove(...classes.split(",")); 63 | } 64 | 65 | // Loop over every inserted block 66 | for (const block of [...blocksContainer.children] as Array) { 67 | // Unhide and every custom classes block as unprocessed 68 | for (const ccBlock of getAllCCBlocks(block)) { 69 | ccBlock.removeAttribute("cc-processed"); 70 | ccBlock.parentElement?.style.removeProperty("display"); 71 | } 72 | 73 | // This variable holds the new last cc blocks that will be applied after the while loop 74 | let newLastCCBlock = null; 75 | 76 | // Loop over the CC blocks until all of them have been rendered (changesOccured === false) 77 | let haveChangesOccured = true; 78 | while (haveChangesOccured) { 79 | haveChangesOccured = false; 80 | 81 | let nextIsNonNestedFirstBlock = false; 82 | for (const ccBlock of getAllCCBlocks(block)) { 83 | 84 | // If the custom classes block has been inserted but not already processed 85 | // --> Ignore insertion check if called from Live Preview 86 | if ((ccBlock.isConnected || callFromLivePreview) && !ccBlock.getAttribute("cc-processed")) { 87 | 88 | let isStandalone = false; 89 | let isNonNestedLast = false; 90 | let isNonNestedFirst = false; 91 | let isNested = false; 92 | const parent = ccBlock.parentElement as HTMLElement; 93 | const parentParent = ccBlock.parentElement?.parentElement as HTMLElement; 94 | 95 | // If the parent's parent is a direct child of the blocksContainer, else consider the ccBlock as nested (e.g. in a blockquote) 96 | if ((!callFromLivePreview && [...blocksContainer.children].contains(parentParent)) || (callFromLivePreview && [...blocksContainer.children].contains(parent))) { 97 | 98 | // Trim leading and trailing space from the parent 99 | const clonedParent = parent.cloneNode(true) as HTMLElement; 100 | clonedParent.innerHTML = clonedParent.innerHTML.trim(); 101 | 102 | // Figure the position of the ccBlock in its parent 103 | const isFirst = clonedParent.innerHTML.startsWith(ccBlock.outerHTML); 104 | const isLast = clonedParent.innerHTML.endsWith(ccBlock.outerHTML); 105 | const prevIsLineBreak = Boolean(ccBlock.previousSibling) && isLineBreak(ccBlock.previousSibling); 106 | const nextIsLineBreak = Boolean(ccBlock.nextElementSibling) && isLineBreak(ccBlock.nextSibling); 107 | 108 | // Figure if this is a standalone ccBlock (alone on its line / not nested in other contents) 109 | isStandalone = isFirst && isLast; 110 | isNonNestedLast = isLast && !isFirst && prevIsLineBreak; 111 | if (!isNonNestedLast) { 112 | isNonNestedFirst = nextIsNonNestedFirstBlock || (isFirst && !isLast && nextIsLineBreak); 113 | } 114 | isNested = !(isStandalone && isNonNestedFirst && isNonNestedLast); 115 | } 116 | 117 | // If ccBlock is a sort standalone custom classes block (not a nested one) 118 | if (isStandalone || isNonNestedFirst || isNonNestedLast) { 119 | newLastCCBlock = ccBlock; 120 | 121 | // Start by a standalone custom classes block 122 | if (!isNonNestedLast) { 123 | block.setAttribute("cc-standalone", "true"); 124 | } 125 | } 126 | 127 | // If it is a non-nested first block 128 | if (isNonNestedFirst) { 129 | 130 | // Note that we don't have to test if ccBlock.nextElementSibling is a
    element because this is already done while figuring isNonNestedFirstBlock 131 | if (!isCustomClassBlock(ccBlock.nextElementSibling?.nextElementSibling as HTMLElement)) { 132 | 133 | // Build the remaining HTML after the current ccBlock has been removed 134 | let remainingHTML = block.firstChild?.nodeName == "P" ? block.firstElementChild?.innerHTML : block.innerHTML; 135 | 136 | if (remainingHTML) { 137 | remainingHTML = remainingHTML 138 | .replace(ccBlock.outerHTML, "") 139 | .replaceAll("
    ", "") 140 | .trim(); 141 | 142 | /* If some contents remains, render it as Markdown in case it as not been properly rendered the first time (e.g. tables require a blank line above them in Obsidian's Read mode, see this bug report : https://forum.obsidian.md/t/table-renders-in-editing-mode-live-preview-but-not-reading-mode/38667) 143 | */ 144 | if (remainingHTML?.replaceAll("\n", "") !== "") { 145 | block.innerHTML = ""; 146 | MarkdownRenderer.renderMarkdown( 147 | remainingHTML, 148 | block, 149 | "", 150 | //@ts-ignore 151 | null); 152 | if (isNonNestedFirst) block.classList.add(...retrieveCustomClasses(ccBlock.innerText)); 153 | } 154 | } 155 | } 156 | 157 | // If the next element sibling is a CC block, don't render that one and consider the next one as non-nested first block 158 | else { 159 | newLastCCBlock = null; 160 | nextIsNonNestedFirstBlock = true; 161 | } 162 | } 163 | 164 | // Or if it is nested in the middle of a bigger block (a.k.a inline custom classes block) 165 | if (!isStandalone && !isNonNestedFirst && !isNonNestedLast) { 166 | if (ccBlock.parentElement) { 167 | 168 | // Retrive the targetted parent element 169 | let parentElement: HTMLElement = ccBlock.parentElement; 170 | // Support list items 171 | if (ccBlock.parentElement.parentElement) { 172 | if (ccBlock.parentElement.parentElement.nodeName === "LI") { 173 | parentElement = ccBlock.parentElement.parentElement; 174 | } 175 | } 176 | // Note: support of table cells is implicit since the `td` element is the first parent 177 | 178 | // Append custom classes to the parent element 179 | parentElement.classList.add(...retrieveCustomClasses(ccBlock.innerText)); 180 | 181 | // Hide the inline custom classes block 182 | ccBlock.style.display = "none"; 183 | } 184 | } 185 | 186 | // Mark the CC block as processed and set haveChangesOccured 187 | ccBlock.setAttribute("cc-processed", "true"); 188 | haveChangesOccured = true; 189 | } 190 | } 191 | } 192 | 193 | // If the block is not a standalone custom classes block, apply last custom classes to it 194 | if (lastCCBlock) applyLastCC(block, lastCCBlock); 195 | lastCCBlock = newLastCCBlock; 196 | } 197 | } 198 | } 199 | 200 | 201 | export function customClassReadMode (element: HTMLElement, context: any) { 202 | 203 | // Retrieve the blocks' container element 204 | const blocksContainer = context.containerEl; 205 | 206 | // If element has been inserted in Read mode's preview section 207 | if (blocksContainer.classList.contains("markdown-preview-section")) { 208 | 209 | // Listen for the element insertion into the blocks' container 210 | const observer = new MutationObserver(() => { 211 | 212 | // Render the custom classes of the element 213 | process(blocksContainer, element, false); 214 | 215 | // Finally, stop listening for element insertion 216 | observer.disconnect(); 217 | }); 218 | observer.observe(blocksContainer, { attributes: false, childList: true, characterData: false, subtree: true }); 219 | } 220 | 221 | // Or if has been inserted in a Live Preview mode Custom Classes renderer 222 | else if (blocksContainer.classList.contains("cc-renderer")) { 223 | 224 | // Render the custom classes of the element 225 | process(blocksContainer, element, true); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    Obsidian Custom Classes

    2 | 3 |
    4 | GitHub Workflow Status 5 | GitHub Downloads 6 | GitHub License 7 | Semantic-release: angular 8 |
    9 | 10 |
    11 | 12 |

    A minimal Obsidian plugin that allows you to add your own HTML
    classes to chosen Markdown blocks directly from your notes.

    13 | 14 |
    15 |
    16 | 17 | ## Usage 18 | You can add custom classes to : 19 |
      20 |
    • entire blocks (e.g. a whole list) → By inserting `class: <customClass>` on the line right before it

      21 | 22 | 23 | 24 | 25 | 26 | 27 | 36 | 48 | 49 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      28 | 29 | ```markdown 30 | `class: fancy-list` 31 | - Lorem ipsum 32 | - Dolor 33 | - Amet consectetur 34 | ``` 35 |

      37 | 38 | ```html 39 |

      40 |
        41 |
      • Lorem ipsum
      • 42 |
      • Dolor sit
      • 43 |
      • Amet consectetur
      • 44 |
      45 |
      46 | ``` 47 |

      50 |
    • 51 |
      52 |
    • specific elements (e.g. a list item) → By inserting `class: <customClass>` inside of it

      53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 68 | 80 | 81 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      61 | 62 | ```markdown 63 | - Lorem ipsum 64 | - Dolor sit `class: fancy-item` 65 | - Amet consectetur 66 | ``` 67 |

      69 | 70 | ```html 71 |

      72 |
        73 |
      • Lorem ipsum
      • 74 |
      • Dolor sit
      • 75 |
      • Amet consectetur
      • 76 |
      77 |
      78 | ``` 79 |

      82 |
    • 83 |
      84 | 85 |
    • or even both : 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 102 | 114 | 115 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      94 | 95 | ```markdown 96 | `class: fancy-list` 97 | - Lorem ipsum 98 | - Dolor `class: fancy-item` sit 99 | - Amet consectetur 100 | ``` 101 |

      103 | 104 | ```html 105 |

      106 |
        107 |
      • Lorem ipsum
      • 108 |
      • Dolor sit
      • 109 |
      • Amet consectetur
      • 110 |
      111 |
      112 | ``` 113 |

      116 |

    • 117 |
    118 | 119 |
    120 | 121 | > #### ℹ️   For advanced usages and/or informations see the [FAQ section](#-FAQ). 122 | 123 |
    124 |
    125 |
    126 | 127 | ## Demonstrations 128 | Here are some ways to use this plugin that may inspire you for your workflows. 129 | 130 | Add a class to : 131 | 132 |
      133 |
    1. 134 |
      135 | A whole table 136 |
      137 | 138 | 139 | 140 | 141 | 142 | 143 | 152 | 175 | 176 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      144 | 145 | ```markdown 146 | `class: mytable` 147 | | AAA | BBB | CCC | 148 | | --- | --- | --- | 149 | | 111 | 222 | 333 | 150 | ``` 151 |

      153 | 154 | ```html 155 |

      156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 |
      AAABBBCCC
      111222333
      172 |
      173 | ``` 174 |

      177 |
      178 |
      179 |
    2. 180 | 181 |
    3. 182 |
      183 | A table cell 184 |
      185 | 186 | 187 | 188 | 189 | 190 | 191 | 199 | 222 | 223 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      192 | 193 | ```markdown 194 | | AAA | BBB | CCC | 195 | | --- | -------------------- | --- | 196 | | 111 | 222 `class: my-cell` | 333 | 197 | ``` 198 |

      200 | 201 | ```html 202 |

      203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
      AAABBBCCC
      111222333
      219 |
      220 | ``` 221 |

      224 |
      225 |
      226 |
    4. 227 |
    5. 228 |
      229 | A Dataview query 230 |
      231 | 232 | 233 | 234 | 235 | 236 | 237 | 247 | 261 | 262 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      238 | 239 | ````markdown 240 | `class: my-dv-list` 241 | ```dataview 242 | LIST 243 | WHERE creation 244 | ``` 245 | ```` 246 |

      248 | 249 | ```html 250 |

      251 |
      252 |
        253 | // The results of your query 254 | //
      • ...
      • 255 | // ... 256 |
      257 |
      258 |
      259 | ``` 260 |

      263 |
      264 |
      265 |
    6. 266 | 267 |
    7. 268 |
      269 | A heading 270 |
      271 | 272 | 273 | 274 | 275 | 276 | 277 | 284 | 292 | 293 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      278 | 279 | ```markdown 280 | `class: important-title` 281 | ### My super heading 282 | ``` 283 |

      285 | 286 | ```html 287 |

      288 |

      My super heading

      289 |
      290 | ``` 291 |

      294 |
      295 |
      296 |
    8. 297 | 298 |
    9. 299 |
      300 | A blockquote 301 |
      302 | 303 | 304 | 305 | 306 | 307 | 308 | 315 | 325 | 326 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      309 | 310 | ```markdown 311 | `class: interesting-quote` 312 | > Lorem ipsum dolor sit amet 313 | ``` 314 |

      316 | 317 | ```html 318 |

      319 |
      320 |

      Lorem ipsum dolor sit amet

      321 |
      322 |
      323 | ``` 324 |

      327 |
      328 |
      329 |
    10. 330 | 331 |
    11. 332 |
      333 | An inline formatting 334 |
      335 | 336 | 337 | 338 | 339 | 340 | 341 | 347 | 353 | 354 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      342 | 343 | ```markdown 344 | I'm a **bold text `class: big`** and _`.small` me an italic one_ 345 | ``` 346 |

      348 | 349 | ```html 350 |

      I'm a bold text and me an italic one

      351 | ``` 352 |

      355 |
      356 |
      357 |
    12. 358 |
    359 | 360 |
    361 |
    362 |
    363 | 364 | ## Showcase / Integrations 365 | That section displays some example of how people have integrated the _Custom Classes_ plugin in their workflows. 366 | Feel free to share yours by [opening an issue](https://github.com/LilaRest/obsidian-custom-classes/issues/new). 367 | 368 |
      369 |
    1. The Lila's frontmatter :cherry_blossom:

      370 | 371 | Here the _Custom Classes_ plugin is used to render a Markdown unordered list (`ul`) as a clean frontmatter block. 372 | 373 | → Source: https://forum.obsidian.md/t/a-frontmatter-that-finally-supports-links-lilas-frontmatter/53087 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 391 | 399 | 400 |
      This markdown
      (Edit mode)
      Will be rendered
      (Live Preview / Read mode)

      382 | 383 | ```markdown 384 | `class:meta` 385 | - creation:: 2023-01-21T18:55:12 386 | - author:: [[John Doe]] 387 | - parents:: [[Note]], [[Another note]] 388 | - status:: #MayBePartial 389 | ``` 390 |

      392 | 393 | | Theme | | 394 | | -- | -- | 395 | | Dark | ![](https://forum.obsidian.md/uploads/default/original/3X/1/4/1418a3659b033fcf8d925105d6a3da3c6b9984fc.gif) | 396 | | Light | ![](https://forum.obsidian.md/uploads/default/original/3X/3/5/35b209dfa79a2b3df13166e9ddd6d1b208480fca.gif) | 397 | 398 |

      401 |
    2. 402 |
    403 | 404 |
    405 |
    406 |
    407 | 408 | ## ❔ FAQ 409 |
    410 | Why not to use <div class="my-custom-class"> instead ? 411 |
    412 |
    413 | 414 | In Obsidian, wrapping a Markdown element in a `div` will break its render in Live Preview and Read modes, and prevent links from being clicked in Edit mode. Also, writing HTML into your notes makes them less readable. 415 | 416 | **Thanks to the _Custom Classes_ plugin you're able to add a custom classes to Markdown elements without breaking anything and using plain-markdown format !** :tada: 417 |
    418 |
    419 |
    420 | 421 |
    422 | Will it works in other Markdown editors ? 423 |
    424 |
    425 | 426 | Since this plugin is exclusive to Obsidian, the custom classes will not be applied in other editors. 427 | 428 | However since the custom classes blocks (``` `class: ...` ```) are simple Markdown inline code-blocks, they will properly render as code blocks in other Markdown editors. 429 |
    430 |
    431 |
    432 | 433 |
    434 | Is it possible to add multiple classes at once ? 435 |
    436 |
    437 | 438 | Yes, just separate each class by a comma : 439 | 440 | 441 | 442 | 443 | 444 | 445 | 452 | 460 | 461 |
    This markdown
    (Edit mode)
    Will be rendered
    (Live Preview / Read mode)

    446 | 447 | ```markdown 448 | `class: first, second, third-one` 449 | I'm the paragraph and you ? 450 | ``` 451 |

    453 | 454 | ```html 455 |

    456 |

    I'm the paragraph and you ?

    457 |
    458 | ``` 459 |

    462 |
    463 |
    464 |
    465 | 466 |
    467 | Does it works in Live Preview mode ? 468 |
    469 |
    470 | 471 | Yes the Live Preview mode is fully supported by this plugin. 472 | 473 | By the way, elements targetted by a _Custom Classes_ block are rendered in the exact same way in both Read and LP modes, allowing you to write CSS that will work everywhere. 474 |
    475 |
    476 |
    477 | 478 |
    479 | The class: prefix is too long, is there any shorthand version ? 480 |
    481 |
    482 | 483 | Yes the _Custom Classes_ plugin will also consider as custom classes block every inline code-block that starts with `cls:`or with `.` 484 | 485 | So ``` `cls: wow` ``` and ``` `.wow` ``` are equivalent to ``` `class: wow` ```. 486 |
    487 |
    488 |
    489 | 490 |
    491 |
    492 |
    493 | 494 | ## Installation 495 | 1) Go to **Community Plugins** section of your Obsidian's settings 496 | 2) Click on **Browse** and search for "Custom classes" 497 | 3) Select the _Custom Classes_ plugin and click on **Install** 498 | 4) Once installed, click on **Enable** 499 | 5) Enjoy ! 500 | 501 | 502 |
    503 |
    504 |
    505 | 506 | ## Inspiration 507 | This plugin is originally inspired by the [Obsidian Stylist](https://github.com/ixth/obsidian-stylist) plugin but has been entirely rewritten to : 508 | - focus exclusively on adding custom HTML classes, 509 | - support the Live Preview mode, 510 | - fix some majors bugs (e.g. classes were not properly appended if the targetted block was modified and then re-rendered). 511 | 512 |
    513 |
    514 |
    515 | 516 | ## Contributing 517 | See [CONTRIBUTING.md](https://github.com/LilaRest/obsidian-custom-classes/blob/main/CONTRIBUTING.md). 518 | --------------------------------------------------------------------------------