├── styles.css ├── .npmrc ├── .eslintignore ├── versions.json ├── .editorconfig ├── manifest.json ├── .gitignore ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── package.json ├── esbuild.config.mjs ├── settings.ts ├── summarytags.ts ├── README.md └── main.ts /styles.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.9.7", 3 | "1.0.1": "0.12.0", 4 | "1.1.0": "0.12.0", 5 | "1.2.0": "0.12.0", 6 | "2.0.0": "0.12.0", 7 | "2.1.0": "0.12.0", 8 | "2.1.5": "0.12.0" 9 | } 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "tag-summary-plugin", 3 | "name": "Tag Summary", 4 | "version": "2.1.5", 5 | "minAppVersion": "0.12.0", 6 | "description": "This plugin creates summaries with paragraphs or blocks of text that share the same tag(s).", 7 | "author": "J.D Gauchat", 8 | "authorUrl": "https://www.jdgauchat.com", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | "lib": [ 14 | "DOM", 15 | "ES5", 16 | "ES6", 17 | "ES7" 18 | ] 19 | }, 20 | "include": [ 21 | "**/*.ts" 22 | ] 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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tag-summary-plugin", 3 | "version": "2.1.5", 4 | "description": "This plugin creates summaries with paragraphs or blocks of text that share the same tag(s).", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "^5.2.0", 17 | "@typescript-eslint/parser": "^5.2.0", 18 | "builtin-modules": "^3.2.0", 19 | "esbuild": "0.13.12", 20 | "obsidian": "latest", 21 | "tslib": "2.3.1", 22 | "typescript": "4.4.4" 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules' 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === 'production'); 13 | 14 | esbuild.build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ['main.ts'], 19 | bundle: true, 20 | external: [ 21 | 'obsidian', 22 | 'electron', 23 | '@codemirror/autocomplete', 24 | '@codemirror/closebrackets', 25 | '@codemirror/collab', 26 | '@codemirror/commands', 27 | '@codemirror/comment', 28 | '@codemirror/fold', 29 | '@codemirror/gutter', 30 | '@codemirror/highlight', 31 | '@codemirror/history', 32 | '@codemirror/language', 33 | '@codemirror/lint', 34 | '@codemirror/matchbrackets', 35 | '@codemirror/panel', 36 | '@codemirror/rangeset', 37 | '@codemirror/rectangular-selection', 38 | '@codemirror/search', 39 | '@codemirror/state', 40 | '@codemirror/stream-parser', 41 | '@codemirror/text', 42 | '@codemirror/tooltip', 43 | '@codemirror/view', 44 | ...builtins], 45 | format: 'cjs', 46 | watch: !prod, 47 | target: 'es2016', 48 | logLevel: "info", 49 | sourcemap: prod ? false : 'inline', 50 | treeShaking: true, 51 | outfile: 'main.js', 52 | }).catch(() => process.exit(1)); 53 | -------------------------------------------------------------------------------- /settings.ts: -------------------------------------------------------------------------------- 1 | 2 | import SummaryPlugin from "main"; 3 | import { App, PluginSettingTab, Setting } from "obsidian"; 4 | 5 | export class SummarySettingTab extends PluginSettingTab { 6 | plugin: SummaryPlugin; 7 | 8 | constructor(app: App, plugin: SummaryPlugin) { 9 | super(app, plugin); 10 | this.plugin = plugin; 11 | } 12 | display(): void { 13 | let { containerEl } = this; 14 | 15 | containerEl.empty(); 16 | 17 | new Setting(containerEl) 18 | .setName("Show Callouts") 19 | .setDesc("Show the text inside callout blocks") 20 | .addToggle((toggle) => 21 | toggle 22 | .setValue(this.plugin.settings.includecallout) 23 | .onChange(async (value) => { 24 | this.plugin.settings.includecallout = value; 25 | await this.plugin.saveSettings(); 26 | }) 27 | ); 28 | new Setting(containerEl) 29 | .setName("Show Link") 30 | .setDesc("Show link to original note") 31 | .addToggle((toggle) => 32 | toggle 33 | .setValue(this.plugin.settings.includelink) 34 | .onChange(async (value) => { 35 | this.plugin.settings.includelink = value; 36 | await this.plugin.saveSettings(); 37 | }) 38 | ); 39 | new Setting(containerEl) 40 | .setName("Remove Tags") 41 | .setDesc("Remove tags from text") 42 | .addToggle((toggle) => 43 | toggle 44 | .setValue(this.plugin.settings.removetags) 45 | .onChange(async (value) => { 46 | this.plugin.settings.removetags = value; 47 | await this.plugin.saveSettings(); 48 | }) 49 | ); 50 | 51 | new Setting(containerEl) 52 | .setName("List Items") 53 | .setDesc("Include only the items of a list that contain the tag, not the entire list.") 54 | .addToggle((toggle) => 55 | toggle 56 | .setValue(this.plugin.settings.listparagraph) 57 | .onChange(async (value) => { 58 | this.plugin.settings.listparagraph = value; 59 | await this.plugin.saveSettings(); 60 | }) 61 | ); 62 | new Setting(containerEl) 63 | .setName("Include Child Items") 64 | .setDesc("Include the child items of a list.") 65 | .addToggle((toggle) => 66 | toggle 67 | .setValue(this.plugin.settings.includechildren) 68 | .onChange(async (value) => { 69 | this.plugin.settings.includechildren = value; 70 | await this.plugin.saveSettings(); 71 | }) 72 | ); 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /summarytags.ts: -------------------------------------------------------------------------------- 1 | 2 | import { App, Modal, Setting, getAllTags, TFile } from "obsidian"; 3 | 4 | export class SummaryModal extends Modal { 5 | include: string; 6 | exclude: string; 7 | 8 | onSubmit: (include: string, exclude: string) => void; 9 | 10 | constructor(app: App, onSubmit: (include: string, exclude: string) => void) { 11 | super(app); 12 | this.onSubmit = onSubmit; 13 | } 14 | 15 | onOpen() { 16 | // Load tags 17 | let listTags: string[] = []; 18 | let listFiles = this.app.vault.getMarkdownFiles(); 19 | 20 | listFiles.forEach((file) => { 21 | const cache = app.metadataCache.getFileCache(file); 22 | listTags = listTags.concat(getAllTags(cache)); 23 | }); 24 | 25 | // Remove duplicates and sort tags 26 | const tagsSet = new Set(listTags); 27 | listTags = Array.from(tagsSet).sort(); 28 | 29 | // Get list of tags to exclude (Including the None option) 30 | const tagsToExclude = ["None"].concat(listTags); 31 | 32 | let { contentEl } = this; 33 | contentEl.createEl("h1", { text: "Add Summary" }); 34 | 35 | if (listTags.length <= 0) { 36 | contentEl.setText("There are no tags in your notes"); 37 | } else { 38 | this.include = listTags[0]; 39 | new Setting(contentEl) 40 | .setName("Select tag to include in the summary") 41 | .addDropdown((menu) => { 42 | for (let i=0; i { 47 | this.include = value; 48 | }); 49 | }); 50 | 51 | this.exclude = tagsToExclude[0]; 52 | new Setting(contentEl) 53 | .setName("Select tag to exclude from the summary") 54 | .addDropdown((menu) => { 55 | for (let i=0; i { 60 | this.exclude = value; 61 | }); 62 | }); 63 | new Setting(contentEl) 64 | .addButton((button) => { 65 | button 66 | .setButtonText("Add Summary") 67 | .setCta() 68 | .onClick(() => { 69 | this.close(); 70 | this.onSubmit(this.include, this.exclude); 71 | }); 72 | }) 73 | } 74 | } 75 | onClose() { 76 | let { contentEl } = this; 77 | contentEl.empty(); 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tag Summary 2 | 3 | This is a plugin for [Obsidian](https://obsidian.md). 4 | 5 | Tag Summary creates summaries with paragraphs or blocks of text that share the same tag(s). This plugin scans your files looking for blocks of text (text separated by empty lines) and creates a summary with all the blocks that contain the specified tag(s). For example, if you have the following paragraphs in your notes: 6 | 7 | ``` 8 | Monsters are real, and ghosts are real too. They live inside us, and sometimes, they win. 9 | #chapter1 10 | ``` 11 | 12 | ``` 13 | People think that I must be a very strange person. This is not correct. I have the heart of a small boy. It is in a glass jar on my desk. 14 | #crazy 15 | ``` 16 | 17 | ``` 18 | Great minds discuss ideas; average minds discuss events; small minds discuss people. 19 | #chapter1 #crazy 20 | ``` 21 | 22 | You can create summaries with the paragraphs that include the #chapter1 and #crazy tags. For instance, if you create a summary with the #chapter1 tag, the first and third paragraphs will be included, but if you create a summary with the #crazy tag, the second and third paragraphs will be included instead. 23 | 24 | ## Add a Summary to a Note 25 | 26 | Summaries are added to a note with a code block and the **add-summary** identifier. The tags are specified inside the code block with the **tags:** label, as shown next. 27 | 28 | ````markdown 29 | ```add-summary 30 | tags: #chapter1 31 | ``` 32 | ```` 33 | 34 | If you need to include blocks of texts with different tags, add the tags separated by a space, as shown next. 35 | 36 | ````markdown 37 | ```add-summary 38 | tags: #chapter1 #crazy 39 | ``` 40 | ```` 41 | 42 | A summary includes every block of text that contains any of the tags specified by the **tags:** label. In the last example, the summary will include the three paragraphs listed above, because each paragrah contains at least one of the tags specified by the label (#chapter1 OR #crazy). If you want to include only the blocks of text that contain all the tags on the list, you can declare the **include:** label instead, as in the following example. 43 | 44 | ````markdown 45 | ```add-summary 46 | include: #chapter1 #crazy 47 | ``` 48 | ```` 49 | 50 | In this case, only the blocks of text that include both tags will be added to the summary. If you process the paragraphs listed above, only the third paragraph will be added to the summary because it is the only one that includes the #chapter1 AND #crazy tags. If what you want instead is to exclude the blocks of text that contain a specific tag(s), you can add the **exclude:** label, as shown next. 51 | 52 | ````markdown 53 | ```add-summary 54 | tags: #chapter1 55 | exclude: #crazy 56 | ``` 57 | ```` 58 | 59 | This will add to the summary every block of text that includes the #chapter1 tag but does NOT include the #crazy tag. If you process the paragraphs listed above, only the first paragraph will be added to the summary. The **exclude:** label can be combined with the **tags:** and **include:** labels to specify complex conditions. The following example creates a summary with the blocks of text that include the #chapter1 OR #chapter2 tags and also contain both the #chapter3 AND #chapter4 tags, but do NOT contain the #crazy tag. 60 | 61 | ````markdown 62 | ```add-summary 63 | tags: #chapter1 #chapter2 64 | include: #chapter3 #chapter4 65 | exclude: #crazy 66 | ``` 67 | ```` 68 | 69 | ## Command to Add a Summary 70 | 71 | The plugin includes the Add Summary command to add a summary to a note. 72 | 73 | - Open the note where you want to include the summary. 74 | - Move the cursor to the position where you want to add the summary. 75 | - Press Command+P (Mac) or Control+P (Windows) to open the Command palette. (or drag down the note on a mobile device) 76 | - Search for the **Tag Summary: Add Summary** command and click on it. 77 | - On the popup window, select the tags you want to include and exclude to create the summary and press the Add Summary button. 78 | 79 | After this, your note should include a code block like the examples above. The Add Summary command allows you to select only one tag to include and another to exclude, but you can manually add all the tags you want separated by a space, as in **tags: #chapter1 #chapter2**. 80 | 81 | ## Summary Configuration 82 | 83 | The plugin includes the following three options to configure the style of the summary. 84 | 85 | - Show Callouts: Shows each block of text inside a callout box (default). If disabled, the blocks are displayed as plain text. 86 | - Show Link: Includes a link at the top to open the note where the paragraph was taken from. 87 | - Remove Tags: Removes the original tags from the text. 88 | 89 | There are also two more options to determine how the plugin will process lists. If the options are enabled, the plugin will process each item of a list independently and include in the summary only the items that match the tags. If you want the plugin to process the entire list as a block of text, you can disable this options. 90 | 91 | - List Items: Include only the items of a list that contain the tag(s), not the entire list. 92 | - Include Child Items: Include the child items of a list. 93 | 94 | ## Usage 95 | 96 | This plugin does not affect the way Obsidian, links, and tags work. You can still organize your notes the way you always do, but now you can assign tags to paragraphs, blocks of text, or items on a list, and then create summaries with those that include a specific list of tags. 97 | 98 | When structuring your notes, please consider the following: 99 | 100 | - The plugin considers a block of text to be all the text between empty lines. If you add an empty line in the middle of a paragraph or a block of text, the plugin will consider that as two different blocks. 101 | - Tags can be specified in any position of the paragraph or block of text, even in a new line at the end, as long as there are no empty lines in between. 102 | 103 | ## Safety 104 | 105 | This plugin does not modify the original notes, it doesn't download or upload any information to the web, and doesn't store or retrieve any private information. 106 | 107 | ## Disclaimer 108 | 109 | This plugin comes with no guarantee of any kind, and neither the author nor Obsidian are responsible for any loss of data or inconvenience. 110 | Use this plugin at your own risk. 111 | 112 | ## From the Author 113 | 114 | I created this plugin for personal use. I will try to post updates from time to time. If you find a bug, you can contact me through my website: 115 | [www.jdgauchat.com](https://www.jdgauchat.com/) 116 | 117 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Console } from 'console'; 3 | import { Editor, Plugin, MarkdownRenderer, getAllTags, TFile } from 'obsidian'; 4 | import { SummarySettingTab } from "./settings"; 5 | import { SummaryModal } from "./summarytags"; 6 | 7 | interface SummarySettings { 8 | includecallout: boolean; 9 | includelink: boolean; 10 | removetags: boolean; 11 | listparagraph: boolean; 12 | includechildren: boolean; 13 | } 14 | const DEFAULT_SETTINGS: Partial = { 15 | includecallout: true, 16 | includelink: true, 17 | removetags: false, 18 | listparagraph: true, 19 | includechildren: true, 20 | }; 21 | export default class SummaryPlugin extends Plugin { 22 | settings: SummarySettings; 23 | 24 | async onload() { 25 | // Prepare Settings 26 | await this.loadSettings(); 27 | this.addSettingTab(new SummarySettingTab(this.app, this)); 28 | 29 | // Create command to create a summary 30 | this.addCommand({ 31 | id: "summary-modal", 32 | name: "Add Summary", 33 | editorCallback: (editor: Editor) => { 34 | new SummaryModal(this.app, (include, exclude) => { 35 | // Format code block to add summary 36 | let summary = "```add-summary\n"; 37 | 38 | // Add the tags label with the tag selected by the user 39 | summary += "tags: " + include + "\n"; 40 | 41 | // Add the exclude label with the tags to exclude 42 | if (exclude != "None") { 43 | summary += "exclude: " + exclude + "\n"; 44 | } 45 | summary += "```\n"; 46 | editor.replaceRange(summary, editor.getCursor()); 47 | }).open(); 48 | }, 49 | }); 50 | 51 | // Post processor 52 | this.registerMarkdownCodeBlockProcessor("add-summary", async (source, el, ctx) => { 53 | // Initialize tag list 54 | let tags: string[] = Array(); 55 | let include: string[] = Array(); 56 | let exclude: string[] = Array(); 57 | 58 | // Process rows inside codeblock 59 | const rows = source.split("\n").filter((row) => row.length > 0); 60 | rows.forEach((line) => { 61 | // Check if the line specifies the tags (OR) 62 | if (line.match(/^\s*tags:[\p{L}0-9_\-/# ]+$/gu)) { 63 | const content = line.replace(/^\s*tags:/, "").trim(); 64 | 65 | // Get the list of valid tags and assign them to the tags variable 66 | let list = content.split(/\s+/).map((tag) => tag.trim()); 67 | list = list.filter((tag) => { 68 | if (tag.match(/^#[\p{L}]+[^#]*$/u)) { 69 | return true; 70 | } else { 71 | return false; 72 | } 73 | }); 74 | tags = list; 75 | } 76 | // Check if the line specifies the tags to include (AND) 77 | if (line.match(/^\s*include:[\p{L}0-9_\-/# ]+$/gu)) { 78 | const content = line.replace(/^\s*include:/, "").trim(); 79 | 80 | // Get the list of valid tags and assign them to the include variable 81 | let list = content.split(/\s+/).map((tag) => tag.trim()); 82 | list = list.filter((tag) => { 83 | if (tag.match(/^#[\p{L}]+[^#]*$/u)) { 84 | return true; 85 | } else { 86 | return false; 87 | } 88 | }); 89 | include = list; 90 | } 91 | // Check if the line specifies the tags to exclude (NOT) 92 | if (line.match(/^\s*exclude:[\p{L}0-9_\-/# ]+$/gu)) { 93 | const content = line.replace(/^\s*exclude:/, "").trim(); 94 | 95 | // Get the list of valid tags and assign them to the exclude variable 96 | let list = content.split(/\s+/).map((tag) => tag.trim()); 97 | list = list.filter((tag) => { 98 | if (tag.match(/^#[\p{L}]+[^#]*$/u)) { 99 | return true; 100 | } else { 101 | return false; 102 | } 103 | }); 104 | exclude = list; 105 | } 106 | }); 107 | 108 | // Create summary only if the user specified some tags 109 | if (tags.length > 0 || include.length > 0) { 110 | await this.createSummary(el, tags, include, exclude, ctx.sourcePath); 111 | } else { 112 | this.createEmptySummary(el); 113 | } 114 | }); 115 | } 116 | 117 | // Show empty summary when the tags are not found 118 | createEmptySummary(element: HTMLElement) { 119 | const container = createEl("div"); 120 | container.createEl("span", { 121 | attr: { style: 'color: var(--text-error) !important;' }, 122 | text: "There are no blocks that match the specified tags." 123 | }); 124 | element.replaceWith(container); 125 | } 126 | 127 | // Load the blocks and create the summary 128 | async createSummary(element: HTMLElement, tags: string[], include: string[], exclude: string[], filePath: string) { 129 | const validTags = tags.concat(include); // All the tags selected by the user 130 | 131 | // Get files 132 | let listFiles = this.app.vault.getMarkdownFiles(); 133 | 134 | // Filter files 135 | listFiles = listFiles.filter((file) => { 136 | // Remove files that do not contain the tags selected by the user 137 | const cache = app.metadataCache.getFileCache(file); 138 | const tagsInFile = getAllTags(cache); 139 | 140 | if (validTags.some((value) => tagsInFile.includes(value))) { 141 | return true; 142 | } 143 | return false; 144 | }); 145 | 146 | // Sort files alphabetically 147 | listFiles = listFiles.sort((file1, file2) => { 148 | if (file1.path < file2.path) { 149 | return -1; 150 | } else if (file1.path > file2.path) { 151 | return 1; 152 | } else { 153 | return 0; 154 | } 155 | }); 156 | 157 | // Get files content 158 | let listContents: [TFile, string][] = await this.readFiles(listFiles); 159 | 160 | // Create summary ttt 161 | let summary: string = ""; 162 | listContents.forEach((item) => { 163 | // Get files name 164 | const fileName = item[0].name.replace(/.md$/g, ""); 165 | const filePath = item[0].path; 166 | 167 | // Get paragraphs 168 | let listParagraphs: string[] = Array(); 169 | const blocks = item[1].split(/\n\s*\n/).filter((row) => row.trim().length > 0); 170 | 171 | // Get list items 172 | blocks.forEach((paragraph) => { 173 | // Check if the paragraph is another plugin 174 | let valid = false; 175 | let listTags = paragraph.match(/#[\p{L}0-9_\-/#]+/gu); 176 | 177 | if (listTags != null && listTags.length > 0) { 178 | if (!paragraph.contains("```")) { 179 | valid = this.isValidText(listTags, tags, include, exclude); 180 | } 181 | } 182 | if (valid) { 183 | if (!this.settings.listparagraph) { 184 | // Add all paragraphs 185 | listParagraphs.push(paragraph); 186 | } else { 187 | // Add paragraphs and the items of a list 188 | let listItems: string[] = Array(); 189 | let itemText = ""; 190 | 191 | paragraph.split('\n\s*\n').forEach((line) => { 192 | let isList = false; 193 | isList = line.search(/(\s*[\-\+\*]){1}|([0-9]\.){1}\s+/) != -1 194 | 195 | if (!isList) { 196 | // Add normal paragraphs 197 | listParagraphs.push(line); 198 | itemText = ""; 199 | } else { 200 | line.split('\n').forEach((itemLine) => { 201 | // Get the item's level 202 | let level = 0; 203 | const endIndex = itemLine.search(/[\-\+\*]{1}|([0-9]\.){1}\s+/); 204 | const tabText = itemLine.slice(0, endIndex); 205 | const tabs = tabText.match(/\t/g); 206 | if (tabs) { 207 | level = tabs.length; 208 | } 209 | // Get items tree 210 | if (level == 0) { 211 | if (itemText != "") { 212 | listItems.push(itemText); 213 | itemText = ""; 214 | } 215 | itemText = itemText.concat(itemLine + "\n"); 216 | } else if (this.settings.includechildren && level > 0 && itemText != "") { 217 | itemText = itemText.concat(itemLine + "\n"); 218 | } 219 | }); 220 | } 221 | }); 222 | if (itemText != "") { 223 | listItems.push(itemText); 224 | itemText = ""; 225 | } 226 | 227 | // Check tags on the items 228 | listItems.forEach((line) => { 229 | listTags = line.match(/#[\p{L}0-9_\-/#]+/gu); 230 | if (listTags != null && listTags.length > 0) { 231 | if (this.isValidText(listTags, tags, include, exclude)) { 232 | listParagraphs.push(line); 233 | } 234 | } 235 | }); 236 | } 237 | } 238 | }) 239 | 240 | // Process each block of text 241 | listParagraphs.forEach((paragraph) => { 242 | // Restore newline at the end 243 | paragraph += "\n"; 244 | 245 | // Remove tags from blocks 246 | if (this.settings.removetags) { 247 | paragraph = paragraph.replace(/#[\p{L}0-9_\-/#]+/gu, ""); 248 | } 249 | 250 | // Add link to original note 251 | if (this.settings.includelink) { 252 | paragraph = "**Source:** [[" + filePath + "|" + fileName + "]]\n" + paragraph; 253 | } 254 | 255 | // Insert the text in a callout 256 | if (this.settings.includecallout) { 257 | // Insert the text in a callout box 258 | let callout = "> [!" + fileName + "]\n"; 259 | const rows = paragraph.split("\n"); 260 | rows.forEach((row) => { 261 | callout += "> " + row + "\n"; 262 | }); 263 | paragraph = callout + "\n\n"; 264 | } else { 265 | // No Callout 266 | paragraph += "\n\n"; 267 | } 268 | 269 | // Add to Summary 270 | summary += paragraph; 271 | }); 272 | }); 273 | 274 | // Add Summary 275 | if (summary != "") { 276 | let summaryContainer = createEl("div"); 277 | await MarkdownRenderer.renderMarkdown(summary, summaryContainer, this.app.workspace.getActiveFile()?.path, null); 278 | element.replaceWith(summaryContainer); 279 | } else { 280 | this.createEmptySummary(element); 281 | } 282 | } 283 | 284 | // Read Files 285 | async readFiles(listFiles: TFile[]): Promise<[TFile, string][]> { 286 | let list: [TFile, string][] = []; 287 | for (let t = 0; t < listFiles.length; t += 1) { 288 | const file = listFiles[t]; 289 | let content = await this.app.vault.cachedRead(file); 290 | list.push([file, content]); 291 | } 292 | return list; 293 | } 294 | 295 | // Check if tags are valid 296 | isValidText(listTags: string[], tags: string[], include: string[], exclude: string[]): boolean { 297 | let valid = true; 298 | 299 | // Check OR (tags) 300 | if (tags.length > 0) { 301 | valid = valid && tags.some((value) => listTags.includes(value)); 302 | } 303 | // Check AND (include) 304 | if (include.length > 0) { 305 | valid = valid && include.every((value) => listTags.includes(value)); 306 | } 307 | // Check NOT (exclude) 308 | if (valid && exclude.length > 0) { 309 | valid = !exclude.some((value) => listTags.includes(value)); 310 | } 311 | return valid; 312 | } 313 | 314 | // Settings 315 | async loadSettings() { 316 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 317 | } 318 | async saveSettings() { 319 | await this.saveData(this.settings); 320 | } 321 | } 322 | 323 | --------------------------------------------------------------------------------