mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 11:16:05 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into ai-llm-integration
This commit is contained in:
		
						commit
						0133e83d23
					
				
					 24 changed files with 264 additions and 52 deletions
				
			
		
							
								
								
									
										3
									
								
								.vscode/extensions.json
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/extensions.json
									
										
									
									
										vendored
									
									
								
							|  | @ -5,6 +5,7 @@ | |||
|     "vitest.explorer", | ||||
|     "ms-playwright.playwright", | ||||
|     "tobermory.es6-string-html", | ||||
|     "dbaeumer.vscode-eslint" | ||||
|     "dbaeumer.vscode-eslint", | ||||
|     "yzhang.markdown-all-in-one" | ||||
|   ] | ||||
| } | ||||
|  |  | |||
|  | @ -25,6 +25,11 @@ keyPath= | |||
| # expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) | ||||
| trustedReverseProxy=false | ||||
| 
 | ||||
| # setting the CORS headers for cross-origin requests | ||||
| # corsAllowOrigin='*' | ||||
| # corsAllowMethods='GET,POST,PUT,DELETE,PATCH' | ||||
| # corsAllowHeaders='Content-Type,Authorization' | ||||
| 
 | ||||
| 
 | ||||
| [Session] | ||||
| # Use this setting to set a custom value for the "Max-Age" Attribute of the session cookie. | ||||
|  |  | |||
|  | @ -1,7 +1,4 @@ | |||
| # v0.93.0 | ||||
| ## 💡 Key highlights | ||||
| 
 | ||||
| *   … | ||||
| 
 | ||||
| ## 🐞 Bugfixes | ||||
| 
 | ||||
|  | @ -15,6 +12,10 @@ | |||
| *   [config.Session.cookieMaxAge is ignored](https://github.com/TriliumNext/Notes/issues/1709) by @pano9000 | ||||
| *   [Return correct HTTP status code on failed login attempts instead of 200](https://github.com/TriliumNext/Notes/issues/1707) by @pano9000 | ||||
| *   [Calendar stops displaying notes after adding a Day Note](https://github.com/TriliumNext/Notes/issues/1705) | ||||
| *   Full anonymization not redacting attachment titles. | ||||
| *   Unable to trigger "Move to" dialog via keyboard shortcut. | ||||
| *   [Note ordering doesn't load correctly, only shows up right after moving a note](https://github.com/TriliumNext/Notes/issues/1727) | ||||
| *   [Note selection dialog shows icon class when selecting result with arrow button (jump to note / create link)](https://github.com/TriliumNext/Notes/issues/1721) | ||||
| 
 | ||||
| ## ✨ Improvements | ||||
| 
 | ||||
|  | @ -23,6 +24,7 @@ | |||
|     *   Reduce extra whitespace between list items. | ||||
|     *   Preserve include note. | ||||
|     *   Handle note titles that contain inline code. | ||||
|     *   Support to-do lists. | ||||
| *   In-app help: | ||||
|     *   Document structure is now precalculated, so start-up time should be slightly better. | ||||
|     *   Optimized the content in order to reduce the size on disk. | ||||
|  | @ -35,10 +37,8 @@ | |||
| *   Basic Touch Bar support for macOS. | ||||
| *   [Support Bearer Token](https://github.com/TriliumNext/Notes/issues/1701) | ||||
| *   The tab bar is now scrollable when there are many tabs by @SiriusXT | ||||
| 
 | ||||
| ## 🌍 Internationalization | ||||
| 
 | ||||
| *   … | ||||
| *   Make each part of the note path clickable by @SiriusXT | ||||
| *   Allow setting CORS headers by @yiranlus | ||||
| 
 | ||||
| ## 📖 Documentation | ||||
| 
 | ||||
|  | @ -47,6 +47,5 @@ | |||
| 
 | ||||
| ## 🛠️ Technical updates | ||||
| 
 | ||||
| *   upgrade to express 5.1.0 by @pano9000 | ||||
| *   update dependency mind-elixir to v4.5.1 | ||||
| *   remove non-working cookiePath option by @pano9000 | ||||
|  | @ -9968,6 +9968,26 @@ | |||
|                                     "format": "markdown", | ||||
|                                     "dataFileName": "Trilium instance.md", | ||||
|                                     "attachments": [] | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     "isClone": false, | ||||
|                                     "noteId": "LWtBjFej3wX3", | ||||
|                                     "notePath": [ | ||||
|                                         "pOsGYCXsbNQG", | ||||
|                                         "tC7s2alapj8V", | ||||
|                                         "Gzjqa934BdH4", | ||||
|                                         "LWtBjFej3wX3" | ||||
|                                     ], | ||||
|                                     "title": "Cross-Origin Resource Sharing (CORS)", | ||||
|                                     "notePosition": 20, | ||||
|                                     "prefix": null, | ||||
|                                     "isExpanded": false, | ||||
|                                     "type": "text", | ||||
|                                     "mime": "text/html", | ||||
|                                     "attributes": [], | ||||
|                                     "format": "markdown", | ||||
|                                     "dataFileName": "Cross-Origin Resource Sharing .md", | ||||
|                                     "attachments": [] | ||||
|                                 } | ||||
|                             ] | ||||
|                         }, | ||||
|  |  | |||
|  | @ -0,0 +1,6 @@ | |||
| # Cross-Origin Resource Sharing (CORS) | ||||
| By default, Trilium cannot be accessed in web browsers by requests coming from other domains/origins than Trilium itself.  | ||||
| 
 | ||||
| However, it is possible to manually configure [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) since Trilium v0.93.0 using environment variables or `config.ini`, as follows: | ||||
| 
 | ||||
| <figure class="table" style="width:100%;"><table class="ck-table-resized"><colgroup><col style="width:26.93%;"><col style="width:32.46%;"><col style="width:40.61%;"></colgroup><thead><tr><th>CORS Header</th><th>Corresponding option in <code>config.ini</code></th><th>Corresponding option in environment variables in the <code>Network</code> section</th></tr></thead><tbody><tr><td><code>Access-Control-Allow-Origin</code></td><td><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code></td><td><code>corsAllowOrigin</code> </td></tr><tr><td><code>Access-Control-Allow-Methods</code></td><td><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code></td><td><code>corsAllowMethods</code> </td></tr><tr><td><code>Access-Control-Allow-Headers</code></td><td><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code></td><td><code>corsAllowHeaders</code></td></tr></tbody></table></figure> | ||||
							
								
								
									
										20
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -1,12 +1,12 @@ | |||
| { | ||||
|   "name": "trilium", | ||||
|   "version": "0.92.7", | ||||
|   "version": "0.93.0", | ||||
|   "lockfileVersion": 3, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "trilium", | ||||
|       "version": "0.92.7", | ||||
|       "version": "0.93.0", | ||||
|       "license": "AGPL-3.0-only", | ||||
|       "dependencies": { | ||||
|         "@anthropic-ai/sdk": "0.39.0", | ||||
|  | @ -50,7 +50,7 @@ | |||
|         "html2plaintext": "2.1.4", | ||||
|         "http-proxy-agent": "7.0.2", | ||||
|         "https-proxy-agent": "7.0.6", | ||||
|         "i18next": "24.2.3", | ||||
|         "i18next": "25.0.0", | ||||
|         "i18next-fs-backend": "2.6.0", | ||||
|         "image-type": "5.2.0", | ||||
|         "ini": "5.0.0", | ||||
|  | @ -73,7 +73,7 @@ | |||
|         "rand-token": "1.0.1", | ||||
|         "safe-compare": "1.1.4", | ||||
|         "sanitize-filename": "1.6.3", | ||||
|         "sanitize-html": "2.15.0", | ||||
|         "sanitize-html": "2.16.0", | ||||
|         "sax": "1.4.1", | ||||
|         "serve-favicon": "2.5.0", | ||||
|         "session-file-store": "1.5.0", | ||||
|  | @ -12742,9 +12742,9 @@ | |||
|       } | ||||
|     }, | ||||
|     "node_modules/i18next": { | ||||
|       "version": "24.2.3", | ||||
|       "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", | ||||
|       "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", | ||||
|       "version": "25.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.0.tgz", | ||||
|       "integrity": "sha512-POPvwjOPR1GQvRnbikTMPEhQD+ekd186MHE6NtVxl3Lby+gPp0iq60eCqGrY6wfRnp1lejjFNu0EKs1afA322w==", | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "individual", | ||||
|  | @ -18324,9 +18324,9 @@ | |||
|       } | ||||
|     }, | ||||
|     "node_modules/sanitize-html": { | ||||
|       "version": "2.15.0", | ||||
|       "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.15.0.tgz", | ||||
|       "integrity": "sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==", | ||||
|       "version": "2.16.0", | ||||
|       "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.16.0.tgz", | ||||
|       "integrity": "sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "deepmerge": "^4.2.2", | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
|   "name": "trilium", | ||||
|   "productName": "TriliumNext Notes", | ||||
|   "description": "Build your personal knowledge base with TriliumNext Notes", | ||||
|   "version": "0.92.7", | ||||
|   "version": "0.93.0", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "./electron-main.js", | ||||
|   "author": { | ||||
|  | @ -110,7 +110,7 @@ | |||
|     "html2plaintext": "2.1.4", | ||||
|     "http-proxy-agent": "7.0.2", | ||||
|     "https-proxy-agent": "7.0.6", | ||||
|     "i18next": "24.2.3", | ||||
|     "i18next": "25.0.0", | ||||
|     "i18next-fs-backend": "2.6.0", | ||||
|     "image-type": "5.2.0", | ||||
|     "ini": "5.0.0", | ||||
|  | @ -133,7 +133,7 @@ | |||
|     "rand-token": "1.0.1", | ||||
|     "safe-compare": "1.1.4", | ||||
|     "sanitize-filename": "1.6.3", | ||||
|     "sanitize-html": "2.15.0", | ||||
|     "sanitize-html": "2.16.0", | ||||
|     "sax": "1.4.1", | ||||
|     "serve-favicon": "2.5.0", | ||||
|     "session-file-store": "1.5.0", | ||||
|  |  | |||
							
								
								
									
										12
									
								
								src/app.ts
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								src/app.ts
									
										
									
									
									
								
							|  | @ -7,6 +7,7 @@ import compression from "compression"; | |||
| import { fileURLToPath } from "url"; | ||||
| import { dirname } from "path"; | ||||
| import sessionParser from "./routes/session_parser.js"; | ||||
| import config from "./services/config.js"; | ||||
| import utils from "./services/utils.js"; | ||||
| import assets from "./routes/assets.js"; | ||||
| import routes from "./routes/routes.js"; | ||||
|  | @ -71,6 +72,17 @@ app.set("views", path.join(scriptDir, "views")); | |||
| app.set("view engine", "ejs"); | ||||
| 
 | ||||
| app.use((req, res, next) => { | ||||
|     // set CORS header
 | ||||
|     if (config["Network"]["corsAllowOrigin"]) { | ||||
|         res.header("Access-Control-Allow-Origin", config["Network"]["corsAllowOrigin"]); | ||||
|     } | ||||
|     if (config["Network"]["corsAllowMethods"]) { | ||||
|         res.header("Access-Control-Allow-Methods", config["Network"]["corsAllowMethods"]); | ||||
|     } | ||||
|     if (config["Network"]["corsAllowHeaders"]) { | ||||
|         res.header("Access-Control-Allow-Headers", config["Network"]["corsAllowHeaders"]); | ||||
|     } | ||||
| 
 | ||||
|     res.locals.t = t; | ||||
|     return next(); | ||||
| }); | ||||
|  |  | |||
|  | @ -53,8 +53,8 @@ export interface ContextMenuCommandData extends CommandData { | |||
|     node: Fancytree.FancytreeNode; | ||||
|     notePath?: string; | ||||
|     noteId?: string; | ||||
|     selectedOrActiveBranchIds?: any; // TODO: Remove any once type is defined
 | ||||
|     selectedOrActiveNoteIds: any; // TODO: Remove  any once type is defined
 | ||||
|     selectedOrActiveBranchIds: string[]; | ||||
|     selectedOrActiveNoteIds?: string[]; | ||||
| } | ||||
| 
 | ||||
| export interface NoteCommandData extends CommandData { | ||||
|  |  | |||
|  | @ -18,10 +18,26 @@ export default class MainTreeExecutors extends Component { | |||
|     } | ||||
| 
 | ||||
|     async cloneNotesToCommand({ selectedOrActiveNoteIds }: EventData<"cloneNotesTo">) { | ||||
|         if (!selectedOrActiveNoteIds && this.tree) { | ||||
|             selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.noteId); | ||||
|         } | ||||
| 
 | ||||
|         if (!selectedOrActiveNoteIds) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.triggerCommand("cloneNoteIdsTo", { noteIds: selectedOrActiveNoteIds }); | ||||
|     } | ||||
| 
 | ||||
|     async moveNotesToCommand({ selectedOrActiveBranchIds }: EventData<"moveNotesTo">) { | ||||
|         if (!selectedOrActiveBranchIds && this.tree) { | ||||
|             selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.branchId); | ||||
|         } | ||||
| 
 | ||||
|         if (!selectedOrActiveBranchIds) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.triggerCommand("moveBranchIdsTo", { branchIds: selectedOrActiveBranchIds }); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -0,0 +1,46 @@ | |||
| <p>By default, Trilium cannot be accessed in web browsers by requests coming | ||||
|   from other domains/origins than Trilium itself. </p> | ||||
| <p>However, it is possible to manually configure <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">Cross-Origin Resource Sharing (CORS)</a> since | ||||
|   Trilium v0.93.0 using environment variables or <code>config.ini</code>, | ||||
|   as follows:</p> | ||||
| <figure class="table" style="width:100%;"> | ||||
|   <table class="ck-table-resized"> | ||||
|     <colgroup> | ||||
|       <col style="width:26.93%;"> | ||||
|         <col style="width:32.46%;"> | ||||
|           <col style="width:40.61%;"> | ||||
|     </colgroup> | ||||
|     <thead> | ||||
|       <tr> | ||||
|         <th>CORS Header</th> | ||||
|         <th>Corresponding option in <code>config.ini</code> | ||||
|         </th> | ||||
|         <th>Corresponding option in environment variables in the <code>Network</code> section</th> | ||||
|       </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       <tr> | ||||
|         <td><code>Access-Control-Allow-Origin</code> | ||||
|         </td> | ||||
|         <td><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> | ||||
|         </td> | ||||
|         <td><code>corsAllowOrigin</code> </td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>Access-Control-Allow-Methods</code> | ||||
|         </td> | ||||
|         <td><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> | ||||
|         </td> | ||||
|         <td><code>corsAllowMethods</code> </td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td><code>Access-Control-Allow-Headers</code> | ||||
|         </td> | ||||
|         <td><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> | ||||
|         </td> | ||||
|         <td><code>corsAllowHeaders</code> | ||||
|         </td> | ||||
|       </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
| </figure> | ||||
|  | @ -152,7 +152,7 @@ class FNote { | |||
| 
 | ||||
|         for (const branchId of Object.values(this.childToBranch)) { | ||||
|             const notePosition = this.froca.getBranch(branchId)?.notePosition; | ||||
|             if (notePosition) { | ||||
|             if (notePosition !== undefined) { | ||||
|                 branchIdPos[branchId] = notePosition; | ||||
|             } | ||||
|         } | ||||
|  |  | |||
|  | @ -278,15 +278,20 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | |||
|     const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); | ||||
| 
 | ||||
|     const ctrlKey = utils.isCtrlKey(evt); | ||||
|     const shiftKey = evt.shiftKey; | ||||
|     const isLeftClick = "which" in evt && evt.which === 1; | ||||
|     const isMiddleClick = "which" in evt && evt.which === 2; | ||||
|     const targetIsBlank = ($link?.attr("target") === "_blank"); | ||||
|     const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank; | ||||
|     const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey); | ||||
|     const openInNewWindow = isLeftClick && evt.shiftKey && !ctrlKey; | ||||
| 
 | ||||
|     if (notePath) { | ||||
|         if (openInNewTab) { | ||||
|         if (openInNewWindow) { | ||||
|             appContext.triggerCommand("openInWindow", { notePath, viewScope }); | ||||
|         } else if (openInNewTab) { | ||||
|             appContext.tabManager.openTabWithNoteWithHoisting(notePath, { | ||||
|                 activate: targetIsBlank, | ||||
|                 activate: activate ? true : targetIsBlank, | ||||
|                 viewScope | ||||
|             }); | ||||
|         } else if (isLeftClick) { | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ function getSearchDelay(notesCount: number): number { | |||
| } | ||||
| let searchDelay = getSearchDelay(notesCount); | ||||
| 
 | ||||
| // TODO: Deduplicate with server.
 | ||||
| export interface Suggestion { | ||||
|     noteTitle?: string; | ||||
|     externalLink?: string; | ||||
|  | @ -29,6 +30,7 @@ export interface Suggestion { | |||
|     highlightedNotePathTitle?: string; | ||||
|     action?: string | "create-note" | "search-notes" | "external-link"; | ||||
|     parentNoteId?: string; | ||||
|     icon?: string; | ||||
| } | ||||
| 
 | ||||
| interface Options { | ||||
|  | @ -262,7 +264,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | |||
|                 }, | ||||
|                 displayKey: "notePathTitle", | ||||
|                 templates: { | ||||
|                     suggestion: (suggestion) => suggestion.highlightedNotePathTitle | ||||
|                     suggestion: (suggestion) => `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}` | ||||
|                 }, | ||||
|                 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added
 | ||||
|                 cache: false | ||||
|  |  | |||
|  | @ -19,15 +19,15 @@ const TPL = /*html*/` | |||
|         margin-top: 10px; | ||||
|     } | ||||
| 
 | ||||
|     .note-path-list .path-current { | ||||
|     .note-path-list .path-current a { | ||||
|         font-weight: bold; | ||||
|     } | ||||
| 
 | ||||
|     .note-path-list .path-archived { | ||||
|     .note-path-list .path-archived a { | ||||
|         color: var(--muted-text-color) !important; | ||||
|     } | ||||
| 
 | ||||
|     .note-path-list .path-search { | ||||
|     .note-path-list .path-search a { | ||||
|         font-style: italic; | ||||
|     } | ||||
|     </style> | ||||
|  | @ -72,7 +72,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||
|         this.$notePathList.empty(); | ||||
| 
 | ||||
|         if (!this.note || this.noteId === "root") { | ||||
|             this.$notePathList.empty().append(await this.getRenderedPath("root")); | ||||
|             this.$notePathList.empty().append(await this.getRenderedPath(["root"])); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|  | @ -88,7 +88,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||
|         const renderedPaths = []; | ||||
| 
 | ||||
|         for (const notePathRecord of sortedNotePaths) { | ||||
|             const notePath = notePathRecord.notePath.join("/"); | ||||
|             const notePath = notePathRecord.notePath; | ||||
| 
 | ||||
|             renderedPaths.push(await this.getRenderedPath(notePath, notePathRecord)); | ||||
|         } | ||||
|  | @ -96,42 +96,54 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||
|         this.$notePathList.empty().append(...renderedPaths); | ||||
|     } | ||||
| 
 | ||||
|     async getRenderedPath(notePath: string, notePathRecord: NotePathRecord | null = null) { | ||||
|         const title = await treeService.getNotePathTitle(notePath); | ||||
|     async getRenderedPath(notePath: string[], notePathRecord: NotePathRecord | null = null) { | ||||
|         const $pathItem = $("<li>"); | ||||
|         const pathSegments: string[] = []; | ||||
|         const lastIndex = notePath.length - 1; | ||||
|          | ||||
|         for (let i = 0; i < notePath.length; i++) { | ||||
|             const noteId = notePath[i]; | ||||
|             pathSegments.push(noteId); | ||||
|             const title = await treeService.getNoteTitle(noteId); | ||||
|             const $noteLink = await linkService.createLink(pathSegments.join("/"), { title }); | ||||
| 
 | ||||
|         const $noteLink = await linkService.createLink(notePath, { title }); | ||||
| 
 | ||||
|         $noteLink.find("a").addClass("no-tooltip-preview tn-link"); | ||||
|             $noteLink.find("a").addClass("no-tooltip-preview tn-link"); | ||||
|             $pathItem.append($noteLink); | ||||
|              | ||||
|             if (i != lastIndex) { | ||||
|                 $pathItem.append(" / "); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const icons = []; | ||||
| 
 | ||||
|         if (this.notePath === notePath) { | ||||
|             $noteLink.addClass("path-current"); | ||||
|         if (this.notePath === notePath.join("/")) { | ||||
|             $pathItem.addClass("path-current"); | ||||
|         } | ||||
| 
 | ||||
|         if (!notePathRecord || notePathRecord.isInHoistedSubTree) { | ||||
|             $noteLink.addClass("path-in-hoisted-subtree"); | ||||
|             $pathItem.addClass("path-in-hoisted-subtree"); | ||||
|         } else { | ||||
|             icons.push(`<span class="bx bx-trending-up" title="${t("note_paths.outside_hoisted")}"></span>`); | ||||
|         } | ||||
| 
 | ||||
|         if (notePathRecord?.isArchived) { | ||||
|             $noteLink.addClass("path-archived"); | ||||
|             $pathItem.addClass("path-archived"); | ||||
| 
 | ||||
|             icons.push(`<span class="bx bx-archive" title="${t("note_paths.archived")}"></span>`); | ||||
|         } | ||||
| 
 | ||||
|         if (notePathRecord?.isSearch) { | ||||
|             $noteLink.addClass("path-search"); | ||||
|             $pathItem.addClass("path-search"); | ||||
| 
 | ||||
|             icons.push(`<span class="bx bx-search" title="${t("note_paths.search")}"></span>`); | ||||
|         } | ||||
| 
 | ||||
|         if (icons.length > 0) { | ||||
|             $noteLink.append(` ${icons.join(" ")}`); | ||||
|             $pathItem.append(` ${icons.join(" ")}`); | ||||
|         } | ||||
| 
 | ||||
|         return $("<li>").append($noteLink); | ||||
|         return $pathItem; | ||||
|     } | ||||
| 
 | ||||
|     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|  |  | |||
|  | @ -75,7 +75,8 @@ function getRecentNotes(activeNoteId: string) { | |||
|             notePath: rn.notePath, | ||||
|             noteTitle: title, | ||||
|             notePathTitle, | ||||
|             highlightedNotePathTitle: `<span class="${icon ?? "bx bx-note"}"></span> ${notePathTitle}` | ||||
|             highlightedNotePathTitle: utils.escapeHtml(notePathTitle), | ||||
|             icon: icon ?? "bx bx-note" | ||||
|         }; | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -13,11 +13,12 @@ function getFullAnonymizationScript() { | |||
|         .map((attr) => `'${attr.name}'`) | ||||
|         .join(", "); | ||||
| 
 | ||||
|     const anonymizeScript = ` | ||||
|     const anonymizeScript = /*sql*/`\ | ||||
| UPDATE etapi_tokens SET tokenHash = 'API token hash value'; | ||||
| UPDATE notes SET title = 'title' WHERE title NOT IN ('root', '_hidden', '_share'); | ||||
| UPDATE blobs SET content = 'text' WHERE content IS NOT NULL; | ||||
| UPDATE revisions SET title = 'title'; | ||||
| UPDATE attachments SET title = 'title'; | ||||
| 
 | ||||
| UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrNames}); | ||||
| UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrNames}); | ||||
|  |  | |||
|  | @ -29,6 +29,9 @@ export interface TriliumConfig { | |||
|         certPath: string; | ||||
|         keyPath: string; | ||||
|         trustedReverseProxy: boolean | string; | ||||
|         corsAllowOrigin: string; | ||||
|         corsAllowMethods: string; | ||||
|         corsAllowHeaders: string; | ||||
|     }; | ||||
|     Session: { | ||||
|         cookieMaxAge: number; | ||||
|  | @ -79,7 +82,16 @@ const config: TriliumConfig = { | |||
|             process.env.TRILIUM_NETWORK_KEYPATH || iniConfig.Network.keyPath || "", | ||||
| 
 | ||||
|         trustedReverseProxy: | ||||
|             process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false | ||||
|             process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false, | ||||
| 
 | ||||
|         corsAllowOrigin: | ||||
|             process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN || iniConfig.Network.corsAllowOrigin || "", | ||||
| 
 | ||||
|         corsAllowMethods: | ||||
|             process.env.TRILIUM_NETWORK_CORS_ALLOW_METHODS || iniConfig.Network.corsAllowMethods || "", | ||||
| 
 | ||||
|         corsAllowHeaders: | ||||
|             process.env.TRILIUM_NETWORK_CORS_ALLOW_HEADERS || iniConfig.Network.corsAllowHeaders || "" | ||||
|     }, | ||||
| 
 | ||||
|     Session: { | ||||
|  |  | |||
|  | @ -321,4 +321,25 @@ describe("Markdown export", () => { | |||
|         expect(markdownExportService.toMarkdown(html)).toBe(expected); | ||||
|     }); | ||||
| 
 | ||||
|     it("exports todo lists properly", () => { | ||||
|         const html = trimIndentation/*html*/`\ | ||||
|             <ul class="todo-list"> | ||||
|                 <li> | ||||
|                     <label class="todo-list__label"> | ||||
|                         <input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">Hello</span> | ||||
|                     </label> | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     <label class="todo-list__label"> | ||||
|                         <input type="checkbox" disabled="disabled"><span class="todo-list__label__description">World</span> | ||||
|                     </label> | ||||
|                 </li> | ||||
|             </ul> | ||||
|         `;
 | ||||
|         const expected = trimIndentation`\ | ||||
|             - [x] Hello | ||||
|             - [ ] World`;
 | ||||
|         expect(markdownExportService.toMarkdown(html)).toBe(expected); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
|  |  | |||
|  | @ -230,7 +230,11 @@ function buildListItemFilter(): Rule { | |||
|                 var start = parent.getAttribute('start') | ||||
|                 var index = Array.prototype.indexOf.call(parent.children, node) | ||||
|                 prefix = (start ? Number(start) + index : index + 1) + '.  ' | ||||
|             } else if (parent.classList.contains("todo-list")) { | ||||
|                 const isChecked = node.querySelector("input[type=checkbox]:checked"); | ||||
|                 prefix = (isChecked ? "- [x] " : "- [ ] "); | ||||
|             } | ||||
| 
 | ||||
|             const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : ''); | ||||
|             return result; | ||||
|         } | ||||
|  |  | |||
|  | @ -233,4 +233,12 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li | |||
|         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||
|     }); | ||||
| 
 | ||||
|     it("imports todo lists properly", () => { | ||||
|          const input = trimIndentation`\ | ||||
|             - [x] Hello | ||||
|             - [ ] World`;
 | ||||
|         const expected = `<ul class="todo-list"><li><label class="todo-list__label"><input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">Hello</span></label></li><li><label class="todo-list__label"><input type="checkbox" disabled="disabled"><span class="todo-list__label__description">World</span></label></li></ul>`; | ||||
|         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
|  |  | |||
|  | @ -48,12 +48,52 @@ class CustomMarkdownRenderer extends Renderer { | |||
|     } | ||||
| 
 | ||||
|     list(token: Tokens.List): string { | ||||
|         return super.list(token) | ||||
|         let result = super.list(token) | ||||
|             .replace("\n", "")  // we replace the first one only.
 | ||||
|             .trimEnd(); | ||||
| 
 | ||||
|         // Handle todo-list in the CKEditor format.
 | ||||
|         if (token.items.some(item => item.task)) { | ||||
|             result = result.replace(/^<ul>/, "<ul class=\"todo-list\">"); | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     checkbox({ checked }: Tokens.Checkbox): string { | ||||
|         return '<input type="checkbox"' | ||||
|             + (checked ? 'checked="checked" ' : '') | ||||
|             + 'disabled="disabled">'; | ||||
|     } | ||||
| 
 | ||||
|     listitem(item: Tokens.ListItem): string { | ||||
|         // Handle todo-list in the CKEditor format.
 | ||||
|         if (item.task) { | ||||
|             let itemBody = ''; | ||||
|             const checkbox = this.checkbox({ checked: !!item.checked }); | ||||
|             if (item.loose) { | ||||
|                 if (item.tokens[0]?.type === 'paragraph') { | ||||
|                     item.tokens[0].text = checkbox + item.tokens[0].text; | ||||
|                     if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { | ||||
|                         item.tokens[0].tokens[0].text = checkbox + escape(item.tokens[0].tokens[0].text); | ||||
|                         item.tokens[0].tokens[0].escaped = true; | ||||
|                     } | ||||
|                 } else { | ||||
|                     item.tokens.unshift({ | ||||
|                         type: 'text', | ||||
|                         raw: checkbox, | ||||
|                         text: checkbox, | ||||
|                         escaped: true, | ||||
|                     }); | ||||
|                 } | ||||
|             } else { | ||||
|                 itemBody += checkbox; | ||||
|             } | ||||
| 
 | ||||
|             itemBody += `<span class="todo-list__label__description">${this.parser.parse(item.tokens, !!item.loose)}</span>`; | ||||
|             return `<li><label class="todo-list__label">${itemBody}</label></li>`; | ||||
|         } | ||||
| 
 | ||||
|         return super.listitem(item).trimEnd(); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -358,8 +358,9 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | |||
|         return { | ||||
|             notePath: result.notePath, | ||||
|             noteTitle: title, | ||||
|             notePathTitle: `${icon} ${result.notePathTitle}`, | ||||
|             highlightedNotePathTitle: `<span class="${icon ?? "bx bx-note"}"></span> ${result.highlightedNotePathTitle}` | ||||
|             notePathTitle: result.notePathTitle, | ||||
|             highlightedNotePathTitle: result.highlightedNotePathTitle, | ||||
|             icon: icon ?? "bx bx-note" | ||||
|         }; | ||||
|     }); | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue