mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into feature/trilium_next_theme
This commit is contained in:
		| @@ -4,6 +4,15 @@ | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| .page-break { | ||||
|     page-break-after: always; | ||||
| } | ||||
|  | ||||
| .printed-content .page-break:after, | ||||
| .printed-content .page-break > * { | ||||
|     display: none !important; | ||||
| } | ||||
|  | ||||
| .ck-content li p { | ||||
|     margin: 0 !important; | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -48,9 +48,9 @@ | ||||
|         "html2plaintext": "2.1.4", | ||||
|         "http-proxy-agent": "7.0.2", | ||||
|         "https-proxy-agent": "7.0.5", | ||||
|         "i18next": "23.16.4", | ||||
|         "i18next-fs-backend": "2.3.2", | ||||
|         "i18next-http-backend": "2.6.2", | ||||
|         "i18next": "23.16.8", | ||||
|         "i18next-fs-backend": "2.6.0", | ||||
|         "i18next-http-backend": "2.7.1", | ||||
|         "image-type": "4.1.0", | ||||
|         "ini": "5.0.0", | ||||
|         "is-animated": "2.0.2", | ||||
| @@ -10042,9 +10042,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/i18next": { | ||||
|       "version": "23.16.4", | ||||
|       "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.4.tgz", | ||||
|       "integrity": "sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==", | ||||
|       "version": "23.16.8", | ||||
|       "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", | ||||
|       "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "individual", | ||||
| @@ -10059,19 +10059,22 @@ | ||||
|           "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" | ||||
|         } | ||||
|       ], | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": "^7.23.2" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/i18next-fs-backend": { | ||||
|       "version": "2.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.3.2.tgz", | ||||
|       "integrity": "sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q==" | ||||
|       "version": "2.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.0.tgz", | ||||
|       "integrity": "sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/i18next-http-backend": { | ||||
|       "version": "2.6.2", | ||||
|       "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.6.2.tgz", | ||||
|       "integrity": "sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==", | ||||
|       "version": "2.7.1", | ||||
|       "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.7.1.tgz", | ||||
|       "integrity": "sha512-vPksHIckysGgykCD8JwCr2YsJEml9Cyw+Yu2wtb4fQ7xIn9RH/hkUDh5UkwnIzb0kSL4SJ30Ab/sCInhQxbCgg==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "cross-fetch": "4.0.0" | ||||
|       } | ||||
|   | ||||
| @@ -89,9 +89,9 @@ | ||||
|     "html2plaintext": "2.1.4", | ||||
|     "http-proxy-agent": "7.0.2", | ||||
|     "https-proxy-agent": "7.0.5", | ||||
|     "i18next": "23.16.4", | ||||
|     "i18next-fs-backend": "2.3.2", | ||||
|     "i18next-http-backend": "2.6.2", | ||||
|     "i18next": "23.16.8", | ||||
|     "i18next-fs-backend": "2.6.0", | ||||
|     "i18next-http-backend": "2.7.1", | ||||
|     "image-type": "4.1.0", | ||||
|     "ini": "5.0.0", | ||||
|     "is-animated": "2.0.2", | ||||
|   | ||||
							
								
								
									
										3
									
								
								src/public/app/doc_notes/cn/hidden.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/public/app/doc_notes/cn/hidden.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| <p>隐藏树用于记录各种应用层数据,这些数据大部分时间可能对用户不可见。</p> | ||||
|  | ||||
| <p>确保你知道自己在做什么。对这个子树的错误更改可能会导致应用程序崩溃。</p> | ||||
| @@ -0,0 +1 @@ | ||||
| <p>此启动器操作的键盘快捷键可以在“选项”->“快捷键”中进行配置。</p> | ||||
| @@ -0,0 +1,3 @@ | ||||
| <p>“后退”和“前进”按钮允许您在导航历史中移动。</p> | ||||
|  | ||||
| <p>这些启动器仅在桌面版本中有效,在服务器版本中将被忽略,您可以使用浏览器的原生导航按钮代替。</p> | ||||
							
								
								
									
										11
									
								
								src/public/app/doc_notes/cn/launchbar_intro.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/public/app/doc_notes/cn/launchbar_intro.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <p>欢迎来到启动栏配置界面。</p> | ||||
|  | ||||
| <p>您可以在此处执行以下操作:</p> | ||||
|  | ||||
| <ul> | ||||
|     <li>通过拖动将可用的启动器移动到可见列表中(从而将它们放入启动栏)</li> | ||||
|     <li>通过拖动将可见的启动器移动到可用列表中(从而将它们从启动栏中隐藏)</li> | ||||
|     <li>您可以通过拖动重新排列列表中的项目</li> | ||||
|     <li>通过右键点击“可见启动器”文件夹来创建新的启动器</li> | ||||
|     <li>如果您想恢复默认设置,可以在右键菜单中找到“重置”选项。</li> | ||||
| </ul> | ||||
							
								
								
									
										9
									
								
								src/public/app/doc_notes/cn/launchbar_note_launcher.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/public/app/doc_notes/cn/launchbar_note_launcher.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| <p>您可以定义以下属性:</p> | ||||
|  | ||||
| <ol> | ||||
|     <li><code>target</code> - 激活启动器时应打开的笔记</li> | ||||
|     <li><code>hoistedNote</code> - 可选,在打开目标笔记之前将更改提升的笔记</li> | ||||
|     <li><code>keyboardShortcut</code> - 可选,按下键盘快捷键将打开该笔记</li> | ||||
| </ol> | ||||
|  | ||||
| <p>启动栏显示来自启动器的标题/图标,这不一定与目标笔记的标题/图标一致。</p> | ||||
							
								
								
									
										12
									
								
								src/public/app/doc_notes/cn/launchbar_script_launcher.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/public/app/doc_notes/cn/launchbar_script_launcher.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| <p>脚本启动器可以执行通过 <code>~script</code> 关系连接的脚本(代码笔记)。</p> | ||||
|  | ||||
| <ol> | ||||
|     <li><code>script</code> - 与应在启动器激活时执行的脚本笔记的关系</li> | ||||
|     <li><code>keyboardShortcut</code> - 可选,按下键盘快捷键将激活启动器</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>示例脚本</h4> | ||||
|  | ||||
| <pre> | ||||
| api.showMessage("当前笔记是 " + api.getActiveContextNote().title); | ||||
| </pre> | ||||
							
								
								
									
										6
									
								
								src/public/app/doc_notes/cn/launchbar_spacer.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/public/app/doc_notes/cn/launchbar_spacer.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| <p>间隔器允许您在视觉上将启动器分组。您可以在提升的属性中进行配置:</p> | ||||
|  | ||||
| <ul> | ||||
|     <li><code>baseSize</code> - 定义以像素为单位的大小(如果有足够的空间)</li> | ||||
|     <li><code>growthFactor</code> - 如果您希望间隔器保持恒定的 <code>baseSize</code>,则设置为 0;如果设置为正值,它将增长。</li> | ||||
| </ul> | ||||
							
								
								
									
										34
									
								
								src/public/app/doc_notes/cn/launchbar_widget_launcher.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/public/app/doc_notes/cn/launchbar_widget_launcher.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <p>请在提升的属性中定义目标小部件笔记。该小部件将用于渲染启动栏图标。</p> | ||||
|  | ||||
| <h4>示例启动栏小部件</h4> | ||||
|  | ||||
| <pre> | ||||
| const TPL = `<div style="height: 53px; width: 53px;"></div>`; | ||||
|  | ||||
| class ExampleLaunchbarWidget extends api.NoteContextAwareWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|     } | ||||
|  | ||||
|     async refreshWithNote(note) { | ||||
|         this.$widget.css("background-color", this.stringToColor(note.title)); | ||||
|     } | ||||
|  | ||||
|     stringToColor(str) { | ||||
|         let hash = 0; | ||||
|         for (let i = 0; i < str.length; i++) { | ||||
|             hash = str.charCodeAt(i) + ((hash << 5) - hash); | ||||
|         } | ||||
|  | ||||
|         let color = '#'; | ||||
|         for (let i = 0; i < 3; i++) { | ||||
|             const value = (hash >> (i * 8)) & 0xFF; | ||||
|             color += ('00' + value.toString(16)).substr(-2); | ||||
|         } | ||||
|  | ||||
|         return color; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = new ExampleLaunchbarWidget(); | ||||
| </pre> | ||||
							
								
								
									
										1
									
								
								src/public/app/doc_notes/cn/share.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/public/app/doc_notes/cn/share.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <p>在这里您可以找到所有分享的笔记。</p> | ||||
							
								
								
									
										1
									
								
								src/public/app/doc_notes/cn/user_hidden.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/public/app/doc_notes/cn/user_hidden.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <p>此笔记作为一个子树,用于存储由用户脚本生成的数据,这些数据本应避免在隐藏子树中随意创建。</p> | ||||
| @@ -98,6 +98,7 @@ export default class DesktopLayout { | ||||
|  | ||||
|         return new RootContainer(launcherPaneIsHorizontal) | ||||
|             .setParent(appContext) | ||||
|             .class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout") | ||||
|             .optChild(launcherPaneIsHorizontal, new FlexContainer('row')                | ||||
|                 .child(new TabRowWidget().class("full-width")) | ||||
|                 .child(new TitleBarButtonsWidget()) | ||||
| @@ -150,7 +151,6 @@ export default class DesktopLayout { | ||||
|                                                 // when visible. When this happens to multiple of them, the first one "wins". | ||||
|                                                 // promoted attributes should always win. | ||||
|                                                 .ribbon(new ClassicEditorToolbar())                                                 | ||||
|                                                 .ribbon(new PromotedAttributesWidget()) | ||||
|                                                 .ribbon(new ScriptExecutorWidget()) | ||||
|                                                 .ribbon(new SearchDefinitionWidget()) | ||||
|                                                 .ribbon(new EditedNotesWidget()) | ||||
| @@ -185,6 +185,7 @@ export default class DesktopLayout { | ||||
|                                         .child( | ||||
|                                             new ScrollingContainer() | ||||
|                                                 .filling() | ||||
|                                                 .child(new PromotedAttributesWidget()) | ||||
|                                                 .child(new SqlTableSchemasWidget()) | ||||
|                                                 .child(new NoteDetailWidget()) | ||||
|                                                 .child(new NoteListWidget()) | ||||
|   | ||||
| @@ -117,6 +117,7 @@ export default class MobileLayout { | ||||
|  | ||||
|         return new RootContainer(launcherPaneIsHorizontal) | ||||
|             .setParent(appContext) | ||||
|             .class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout") | ||||
|             .cssBlock(MOBILE_CSS) | ||||
|             .child(this.#buildLauncherPane(launcherPaneIsHorizontal)) | ||||
|             .child(new FlexContainer("row") | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import server from "./server.js"; | ||||
| import ws from "./ws.js"; | ||||
| import utils from "./utils.js"; | ||||
| import appContext from "../components/app_context.js"; | ||||
| import { t } from "./i18n.js"; | ||||
|  | ||||
| export async function uploadFiles(entityType, parentNoteId, files, options) { | ||||
|     if (!['notes', 'attachments'].includes(entityType)) { | ||||
| @@ -47,7 +48,7 @@ export async function uploadFiles(entityType, parentNoteId, files, options) { | ||||
| function makeToast(id, message) { | ||||
|     return { | ||||
|         id: id, | ||||
|         title: "Import status", | ||||
|         title: t("import.import-status"), | ||||
|         message: message, | ||||
|         icon: "plus" | ||||
|     }; | ||||
| @@ -62,9 +63,9 @@ ws.subscribeToMessages(async message => { | ||||
|         toastService.closePersistent(message.taskId); | ||||
|         toastService.showError(message.message); | ||||
|     } else if (message.type === 'taskProgressCount') { | ||||
|         toastService.showPersistent(makeToast(message.taskId, `Import in progress: ${message.progressCount}`)); | ||||
|         toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount }))); | ||||
|     } else if (message.type === 'taskSucceeded') { | ||||
|         const toast = makeToast(message.taskId, "Import finished successfully."); | ||||
|         const toast = makeToast(message.taskId, t("import.successful")); | ||||
|         toast.closeAfter = 5000; | ||||
|  | ||||
|         toastService.showPersistent(toast); | ||||
| @@ -84,9 +85,9 @@ ws.subscribeToMessages(async message => { | ||||
|         toastService.closePersistent(message.taskId); | ||||
|         toastService.showError(message.message); | ||||
|     } else if (message.type === 'taskProgressCount') { | ||||
|         toastService.showPersistent(makeToast(message.taskId, `Import in progress: ${message.progressCount}`)); | ||||
|         toastService.showPersistent(makeToast(message.taskId, t("import.in-progress", { progress: message.progressCount }))); | ||||
|     } else if (message.type === 'taskSucceeded') { | ||||
|         const toast = makeToast(message.taskId, "Import finished successfully."); | ||||
|         const toast = makeToast(message.taskId, t("import.successful")); | ||||
|         toast.closeAfter = 5000; | ||||
|  | ||||
|         toastService.showPersistent(toast); | ||||
|   | ||||
| @@ -30,10 +30,22 @@ async function autocompleteSourceForCKEditor(queryText) { | ||||
| } | ||||
|  | ||||
| async function autocompleteSource(term, cb, options = {}) { | ||||
|     const fastSearch = options.fastSearch === false ? false : true; | ||||
|     if (fastSearch === false) { | ||||
|         if (term.trim().length === 0){ | ||||
|             return; | ||||
|         } | ||||
|         cb( | ||||
|             [{ | ||||
|                 noteTitle: term, | ||||
|                 highlightedNotePathTitle: `Searching...` | ||||
|             }] | ||||
|         ); | ||||
|     } | ||||
|      | ||||
|     const activeNoteId = appContext.tabManager.getActiveContextNoteId(); | ||||
|  | ||||
|     let results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}`); | ||||
|  | ||||
|     let results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`); | ||||
|     if (term.trim().length >= 1 && options.allowCreatingNotes) { | ||||
|         results = [ | ||||
|             { | ||||
| @@ -45,7 +57,7 @@ async function autocompleteSource(term, cb, options = {}) { | ||||
|         ].concat(results); | ||||
|     } | ||||
|  | ||||
|     if (term.trim().length >= 1 && options.allowSearchNotes) { | ||||
|     if (term.trim().length >= 1 && options.allowJumpToSearchNotes) { | ||||
|         results = results.concat([ | ||||
|             { | ||||
|                 action: 'search-notes', | ||||
| @@ -95,12 +107,22 @@ function showRecentNotes($el) { | ||||
|  | ||||
|     $el.setSelectedNotePath(""); | ||||
|     $el.autocomplete("val", ""); | ||||
|     $el.autocomplete('open'); | ||||
|     $el.trigger('focus'); | ||||
| } | ||||
|  | ||||
|     // simulate pressing down arrow to trigger autocomplete | ||||
|     const e = $.Event('keydown'); | ||||
|     e.which = 40; // arrow down | ||||
|     $el.trigger(e); | ||||
| function fullTextSearch($el, options){ | ||||
|     const searchString = $el.autocomplete('val'); | ||||
|     if (options.fastSearch === false || searchString.trim().length === 0) { | ||||
|         return; | ||||
|     }     | ||||
|     $el.trigger('focus'); | ||||
|     options.fastSearch = false; | ||||
|     $el.autocomplete('val', ''); | ||||
|     $el.setSelectedNotePath(""); | ||||
|     $el.autocomplete('val', searchString); | ||||
|     // Set a delay to avoid resetting to true before full text search (await server.get) is called. | ||||
|     setTimeout(() => { options.fastSearch = true; }, 100); | ||||
| } | ||||
|  | ||||
| function initNoteAutocomplete($el, options) { | ||||
| @@ -123,10 +145,14 @@ function initNoteAutocomplete($el, options) { | ||||
|         .addClass("input-group-text show-recent-notes-button bx bx-time") | ||||
|         .prop("title", "Show recent notes"); | ||||
|  | ||||
|     const $fullTextSearchButton = $("<button>") | ||||
|         .addClass("input-group-text full-text-search-button bx bx-search") | ||||
|         .prop("title", "Full text search (Shift+Enter)");     | ||||
|  | ||||
|     const $goToSelectedNoteButton = $("<button>") | ||||
|         .addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right"); | ||||
|  | ||||
|     $el.after($clearTextButton).after($showRecentNotesButton); | ||||
|     $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton); | ||||
|  | ||||
|     if (!options.hideGoToSelectedNoteButton) { | ||||
|         $el.after($goToSelectedNoteButton); | ||||
| @@ -142,13 +168,18 @@ function initNoteAutocomplete($el, options) { | ||||
|         return false; | ||||
|     }); | ||||
|  | ||||
|     $fullTextSearchButton.on('click', e => { | ||||
|         fullTextSearch($el, options); | ||||
|         return false; | ||||
|     }); | ||||
|  | ||||
|     let autocompleteOptions = {}; | ||||
|     if (options.container) { | ||||
|         autocompleteOptions.dropdownMenuContainer = options.container; | ||||
|         autocompleteOptions.debug = true;   // don't close on blur | ||||
|     } | ||||
|  | ||||
|     if (options.allowSearchNotes) { | ||||
|     if (options.allowJumpToSearchNotes) { | ||||
|         $el.on('keydown', (event) => { | ||||
|             if (event.ctrlKey && event.key === 'Enter') { | ||||
|                 // Prevent Ctrl + Enter from triggering autoComplete. | ||||
| @@ -158,6 +189,14 @@ function initNoteAutocomplete($el, options) { | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     $el.on('keydown', async (event) => { | ||||
|         if (event.shiftKey && event.key === 'Enter') { | ||||
|             // Prevent Enter from triggering autoComplete. | ||||
|             event.stopImmediatePropagation(); | ||||
|             event.preventDefault(); | ||||
|             fullTextSearch($el,options) | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     $el.autocomplete({ | ||||
|         ...autocompleteOptions, | ||||
|   | ||||
| @@ -58,7 +58,7 @@ export default class JumpToNoteDialog extends BasicWidget { | ||||
|         noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, { | ||||
|             allowCreatingNotes: true, | ||||
|             hideGoToSelectedNoteButton: true, | ||||
|             allowSearchNotes: true, | ||||
|             allowJumpToSearchNotes: true, | ||||
|             container: this.$results | ||||
|         }) | ||||
|             // clear any event listener added in previous invocation of this function | ||||
|   | ||||
| @@ -18,32 +18,41 @@ const TPL = ` | ||||
|     } | ||||
|      | ||||
|     .promoted-attributes-container { | ||||
|         margin: auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         flex-shrink: 0; | ||||
|         flex-grow: 0; | ||||
|         justify-content: space-evenly; | ||||
|         margin: 0 1.5em; | ||||
|         overflow: auto; | ||||
|         max-height: 400px; | ||||
|         flex-wrap: wrap; | ||||
|         display: table; | ||||
|     } | ||||
|      | ||||
|     .promoted-attribute-cell { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         margin: 10px; | ||||
|         display: table-row; | ||||
|     } | ||||
|     .promoted-attribute-cell > label { | ||||
|         user-select: none; | ||||
|         font-weight: bold; | ||||
|         vertical-align: middle; | ||||
|     } | ||||
|     .promoted-attribute-cell > * { | ||||
|         display: table-cell; | ||||
|         padding: 1px 0; | ||||
|     } | ||||
|      | ||||
|     .promoted-attribute-cell div.input-group { | ||||
|         margin-left: 10px; | ||||
|         display: flex; | ||||
|         min-height: 40px; | ||||
|     } | ||||
|     .promoted-attribute-cell strong { | ||||
|         word-break:keep-all; | ||||
|         white-space: nowrap; | ||||
|     } | ||||
|     .promoted-attribute-cell input[type="checkbox"] { | ||||
|         height: 1.5em; | ||||
|         width: 22px !important; | ||||
|         flex-grow: 0; | ||||
|         width: unset; | ||||
|     } | ||||
|      | ||||
|     </style> | ||||
| @@ -137,9 +146,11 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { | ||||
|  | ||||
|     async createPromotedAttributeCell(definitionAttr, valueAttr, valueName) { | ||||
|         const definition = definitionAttr.getDefinition(); | ||||
|         const id = `value-${this.noteId}-${definitionAttr.position}`; | ||||
|  | ||||
|         const $input = $("<input>") | ||||
|             .prop("tabindex", 200 + definitionAttr.position) | ||||
|             .prop("id", id) | ||||
|             .attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one | ||||
|             .attr("data-attribute-type", valueAttr.type) | ||||
|             .attr("data-attribute-name", valueAttr.name) | ||||
| @@ -154,7 +165,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { | ||||
|             .attr("nowrap", true); | ||||
|  | ||||
|         const $wrapper = $('<div class="promoted-attribute-cell">') | ||||
|             .append($("<strong>").text(definition.promotedAlias ?? valueName)) | ||||
|             .append($("<label>").prop("for", id).text(definition.promotedAlias ?? valueName)) | ||||
|             .append($("<div>").addClass("input-group").append($input)) | ||||
|             .append($actionCell) | ||||
|             .append($multiplicityCell); | ||||
| @@ -211,9 +222,6 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { | ||||
|             } | ||||
|             else if (definition.labelType === 'boolean') { | ||||
|                 $input.prop("type", "checkbox"); | ||||
|                 // hack, without this the checkbox is invisible | ||||
|                 // we should be using a different bootstrap structure for checkboxes | ||||
|                 $input.css('width', '80px'); | ||||
|  | ||||
|                 if (valueAttr.value === "true") { | ||||
|                     $input.prop("checked", "checked"); | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.js"; | ||||
| import RevisionsSnapshotIntervalOptions from "./options/other/revisions_snapshot_interval.js"; | ||||
| import RevisionSnapshotsLimitOptions from "./options/other/revision_snapshots_limit.js"; | ||||
| import NetworkConnectionsOptions from "./options/other/network_connections.js"; | ||||
| import HtmlImportTagsOptions from "./options/other/html_import_tags.js"; | ||||
| import AdvancedSyncOptions from "./options/advanced/sync.js"; | ||||
| import DatabaseIntegrityCheckOptions from "./options/advanced/database_integrity_check.js"; | ||||
| import ConsistencyChecksOptions from "./options/advanced/consistency_checks.js"; | ||||
| @@ -94,7 +95,8 @@ const CONTENT_WIDGETS = { | ||||
|         AttachmentErasureTimeoutOptions, | ||||
|         RevisionsSnapshotIntervalOptions, | ||||
|         RevisionSnapshotsLimitOptions, | ||||
|         NetworkConnectionsOptions | ||||
|         NetworkConnectionsOptions, | ||||
|         HtmlImportTagsOptions | ||||
|     ], | ||||
|     _optionsAdvanced: [ | ||||
|         DatabaseIntegrityCheckOptions, | ||||
|   | ||||
| @@ -31,7 +31,14 @@ export default class DocTypeWidget extends TypeWidget { | ||||
|         const docName = note.getLabelValue('docName'); | ||||
|  | ||||
|         if (docName) { | ||||
|             this.$content.load(`${window.glob.appPath}/doc_notes/${docName}.html`); | ||||
|             // find doc based on language | ||||
|             const lng = i18next.language; | ||||
|             this.$content.load(`${window.glob.appPath}/doc_notes/${lng}/${docName}.html`, (response, status) => { | ||||
|                 // fallback to english doc if no translation available | ||||
|                 if (status === 'error') { | ||||
|                     this.$content.load(`${window.glob.appPath}/doc_notes/en/${docName}.html`); | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             this.$content.empty(); | ||||
|         } | ||||
|   | ||||
| @@ -176,7 +176,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|         }); | ||||
|  | ||||
|         this.watchdog.setCreator(async (elementOrData, editorConfig) => { | ||||
|             const editor = await editorClass.create(elementOrData, editorConfig); | ||||
|             const editor = await editorClass.create(elementOrData, { | ||||
|                 ...editorConfig, | ||||
|                 htmlSupport: { | ||||
|                     allow: JSON.parse(options.get("allowedHtmlTags")), | ||||
|                     styles: true, | ||||
|                     classes: true, | ||||
|                     attributes: true | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             await initSyntaxHighlighting(editor); | ||||
|  | ||||
|   | ||||
| @@ -70,7 +70,7 @@ export default class EmptyTypeWidget extends TypeWidget { | ||||
|         noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, { | ||||
|             hideGoToSelectedNoteButton: true, | ||||
|             allowCreatingNotes: true, | ||||
|             allowSearchNotes: true, | ||||
|             allowJumpToSearchNotes: true, | ||||
|             container: this.$results | ||||
|         }) | ||||
|             .on('autocomplete:noteselected', function(event, suggestion, dataset) { | ||||
|   | ||||
| @@ -0,0 +1,84 @@ | ||||
| import OptionsWidget from "../options_widget.js"; | ||||
| import { t } from "../../../../services/i18n.js"; | ||||
|  | ||||
| // TODO: Deduplicate with src/services/html_sanitizer once there is a commons project between client and server. | ||||
| export const DEFAULT_ALLOWED_TAGS = [ | ||||
|     'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', | ||||
|     'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div', | ||||
|     'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img', | ||||
|     'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer', | ||||
|     'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time', | ||||
|     'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins', | ||||
|     'en-media', // for ENEX import | ||||
|     // Additional tags (https://github.com/TriliumNext/Notes/issues/567) | ||||
|     'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd', | ||||
|     'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp', | ||||
|     'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt' | ||||
| ]; | ||||
|  | ||||
| const TPL = ` | ||||
| <div class="options-section"> | ||||
|     <h4>${t("import.html_import_tags.title")}</h4> | ||||
|      | ||||
|     <p>${t("import.html_import_tags.description")}</p> | ||||
|      | ||||
|         <textarea class="allowed-html-tags form-control" style="height: 150px; font-family: monospace;"  | ||||
|                   placeholder="${t("import.html_import_tags.placeholder")}"></textarea> | ||||
|          | ||||
|         <div class="form-text"> | ||||
|             ${t("import.html_import_tags.help")} | ||||
|         </div> | ||||
|      | ||||
|     <div> | ||||
|         <button class="btn btn-sm btn-secondary reset-to-default"> | ||||
|             ${t("import.html_import_tags.reset_button")} | ||||
|         </button> | ||||
|     </div> | ||||
| </div>`; | ||||
|  | ||||
| export default class HtmlImportTagsOptions extends OptionsWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.contentSized(); | ||||
|  | ||||
|         this.$allowedTags = this.$widget.find('.allowed-html-tags'); | ||||
|         this.$resetButton = this.$widget.find('.reset-to-default'); | ||||
|          | ||||
|         this.$allowedTags.on('change', () => this.saveTags()); | ||||
|         this.$resetButton.on('click', () => this.resetToDefault()); | ||||
|          | ||||
|         // Load initial tags | ||||
|         this.refresh(); | ||||
|     } | ||||
|  | ||||
|     async optionsLoaded(options) { | ||||
|         try { | ||||
|             if (options.allowedHtmlTags) { | ||||
|                 const tags = JSON.parse(options.allowedHtmlTags); | ||||
|                 this.$allowedTags.val(tags.join(' ')); | ||||
|             } else { | ||||
|                 // If no tags are set, show the defaults | ||||
|                 this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join(' ')); | ||||
|             } | ||||
|         } | ||||
|         catch (e) { | ||||
|             console.error('Could not load HTML tags:', e); | ||||
|             // On error, show the defaults | ||||
|             this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join(' ')); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async saveTags() { | ||||
|         const tagsText = this.$allowedTags.val(); | ||||
|         const tags = tagsText.split(/[\n,\s]+/) // Split on newlines, commas, or spaces | ||||
|             .map(tag => tag.trim()) | ||||
|             .filter(tag => tag.length > 0); | ||||
|              | ||||
|         await this.updateOption('allowedHtmlTags', JSON.stringify(tags)); | ||||
|     } | ||||
|  | ||||
|     async resetToDefault() { | ||||
|         this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join('\n')); // Use actual newline | ||||
|         await this.saveTags(); | ||||
|     } | ||||
| } | ||||
| @@ -448,7 +448,7 @@ pre:not(.CodeMirror-line):not(.hljs) { | ||||
|     padding-top: 8px; | ||||
| } | ||||
|  | ||||
| .show-recent-notes-button { | ||||
| .show-recent-notes-button, .full-text-search-button { | ||||
|     cursor: pointer; | ||||
|     font-size: 1.3em; | ||||
|     padding-left: 5px; | ||||
|   | ||||
| @@ -175,7 +175,17 @@ | ||||
|     "codeImportedAsCode": "Import recognized code files (e.g. <code>.json</code>) as code notes if it's unclear from metadata", | ||||
|     "replaceUnderscoresWithSpaces": "Replace underscores with spaces in imported note names", | ||||
|     "import": "Import", | ||||
|     "failed": "Import failed: {{message}}." | ||||
|     "failed": "Import failed: {{message}}.", | ||||
|     "html_import_tags": { | ||||
|       "title": "HTML Import Tags", | ||||
|       "description": "Configure which HTML tags should be preserved when importing notes. Tags not in this list will be removed during import.", | ||||
|       "placeholder": "Enter HTML tags, one per line", | ||||
|       "help": "Enter HTML tags to preserve during import. Some tags (like 'script') are always removed for security.", | ||||
|       "reset_button": "Reset to Default List" | ||||
|     }, | ||||
|     "import-status": "Import status", | ||||
|     "in-progress": "Import in progress: {{progress}}", | ||||
|     "successful": "Import finished successfully." | ||||
|   }, | ||||
|   "include_note": { | ||||
|     "dialog_title": "Include note", | ||||
|   | ||||
| @@ -175,7 +175,17 @@ | ||||
|     "codeImportedAsCode": "Importar archivos de código reconocidos (por ejemplo, <code>.json</code>) como notas de código si no están claros en los metadatos", | ||||
|     "replaceUnderscoresWithSpaces": "Reemplazar guiones bajos con espacios en nombres de notas importadas", | ||||
|     "import": "Importar", | ||||
|     "failed": "La importación falló: {{message}}." | ||||
|     "failed": "La importación falló: {{message}}.", | ||||
|     "html_import_tags": { | ||||
|       "title": "HTML Importar Etiquetas", | ||||
|       "description": "Configurar que etiquetas HTML deben ser preservadas al importar notas. Las etiquetas que no estén en esta lista serán eliminadas durante la importación.", | ||||
|       "placeholder": "Ingrese las etiquetas HTML, una por línea", | ||||
|       "help": "Ingrese las etiquetas HTML a preservar durante la importación. Algunas etiquetas (como 'script') siempre son eliminadas por seguridad.", | ||||
|       "reset_button": "Restablecer a lista por defectp" | ||||
|     }, | ||||
|     "import-status": "Estado de importación", | ||||
|     "in-progress": "Importación en progreso: {{progress}}", | ||||
|     "successful": "Importación finalizada exitosamente." | ||||
|   }, | ||||
|   "include_note": { | ||||
|     "dialog_title": "Incluir nota", | ||||
| @@ -1547,12 +1557,21 @@ | ||||
|     } | ||||
|   }, | ||||
|   "electron_context_menu": { | ||||
|     "add-term-to-dictionary": "Agregar \"{{term}}\" al diccionario.", | ||||
|     "add-term-to-dictionary": "Agregar \"{{term}}\" al diccionario", | ||||
|     "cut": "Cortar", | ||||
|     "copy": "Copiar", | ||||
|     "copy-link": "Copiar enlace", | ||||
|     "paste": "Pegar", | ||||
|     "paste-as-plain-text": "Pegar como texto plano", | ||||
|     "search_online": "Buscar \"{{term}}\" con {{searchEngine}}" | ||||
|   }, | ||||
|   "image_context_menu": { | ||||
|     "copy_reference_to_clipboard": "Copiar referencia al portapapeles", | ||||
|     "copy_image_to_clipboard": "Copiar imagen al portapapeles" | ||||
|   }, | ||||
|   "link_context_menu": { | ||||
|     "open_note_in_new_tab": "Abrir nota en una pestaña nueva", | ||||
|     "open_note_in_new_split": "Abrir nota en una nueva división", | ||||
|     "open_note_in_new_window": "Abrir nota en una nueva ventana" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -692,7 +692,17 @@ | ||||
|     "shrinkImages": "Micșorare imagini", | ||||
|     "shrinkImagesTooltip": "<p>Dacă bifați această opțiune, Trilium va încerca să micșoreze imaginea importată prin scalarea și importarea ei, aspect ce poate afecta calitatea aparentă a imaginii. Dacă nu este bifat, imaginile vor fi importate fără nicio modificare.</p><p>Acest lucru nu se aplică la importuri de tip <code>.zip</code> cu metainformații deoarece se asumă că aceste fișiere sunt deja optimizate.</p>", | ||||
|     "textImportedAsText": "Importă HTML, Markdown și TXT ca notițe de tip text dacă este neclar din metainformații", | ||||
|     "failed": "Eroare la importare: {{message}}." | ||||
|     "failed": "Eroare la importare: {{message}}.", | ||||
|     "import-status": "Starea importului", | ||||
|     "in-progress": "Import în curs: {{progress}}", | ||||
|     "successful": "Import finalizat cu succes.", | ||||
|     "html_import_tags": { | ||||
|       "description": "Configurați ce etichete HTML să fie păstrate atunci când se importă notițe. Etichetele ce nu se află în această listă vor fi înlăturate la importul de date.", | ||||
|       "help": "Introduceți etichetele HTML pentru a se păstra în timpul importului. Unele etichete (precum „script”) sunt înlăturate indiferent din motive de securitate.", | ||||
|       "placeholder": "Introduceți etichetele HTML, câte unul pe linie", | ||||
|       "reset_button": "Resetează la lista implicită", | ||||
|       "title": "Etichete HTML la importare" | ||||
|     } | ||||
|   }, | ||||
|   "include_archived_notes": { | ||||
|     "include_archived_notes": "Include notițele arhivate" | ||||
| @@ -1188,7 +1198,12 @@ | ||||
|     "light_theme": "Temă luminoasă", | ||||
|     "override_theme_fonts_label": "Suprascrie fonturile temei", | ||||
|     "theme_label": "Temă", | ||||
|     "title": "Tema aplicației" | ||||
|     "title": "Tema aplicației", | ||||
|     "layout": "Aspect", | ||||
|     "layout-horizontal-description": "bara de lansare se află sub bara de taburi, bara de taburi este pe toată lungimea.", | ||||
|     "layout-horizontal-title": "Orizontal", | ||||
|     "layout-vertical-title": "Vertical", | ||||
|     "layout-vertical-description": "bara de lansare se află pe stânga (implicit)" | ||||
|   }, | ||||
|   "toast": { | ||||
|     "critical-error": { | ||||
| @@ -1549,5 +1564,14 @@ | ||||
|     "paste": "Lipește", | ||||
|     "paste-as-plain-text": "Lipește doar textul", | ||||
|     "search_online": "Caută „{{term}}” cu {{searchEngine}}" | ||||
|   }, | ||||
|   "image_context_menu": { | ||||
|     "copy_image_to_clipboard": "Copiază imaginea în clipboard", | ||||
|     "copy_reference_to_clipboard": "Copiază referința în clipboard" | ||||
|   }, | ||||
|   "link_context_menu": { | ||||
|     "open_note_in_new_split": "Deschide notița într-un panou nou", | ||||
|     "open_note_in_new_tab": "Deschide notița într-un tab nou", | ||||
|     "open_note_in_new_window": "Deschide notița într-o fereastră nouă" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -14,6 +14,8 @@ function getAutocomplete(req: Request) { | ||||
|         throw new ValidationError("Invalid query data type."); | ||||
|     } | ||||
|     const query = (req.query.query || "").trim(); | ||||
|     const fastSearch = String(req.query.fastSearch).toLowerCase() === "false" ? false : true; | ||||
|      | ||||
|     const activeNoteId = req.query.activeNoteId || 'none'; | ||||
|  | ||||
|     let results; | ||||
| @@ -24,7 +26,7 @@ function getAutocomplete(req: Request) { | ||||
|         results = getRecentNotes(activeNoteId); | ||||
|     } | ||||
|     else { | ||||
|         results = searchService.searchNotesForAutocomplete(query); | ||||
|         results = searchService.searchNotesForAutocomplete(query, fastSearch); | ||||
|     } | ||||
|  | ||||
|     const msTaken = Date.now() - timestampStarted; | ||||
|   | ||||
| @@ -67,7 +67,8 @@ const ALLOWED_OPTIONS = new Set([ | ||||
|     'locale', | ||||
|     'firstDayOfWeek', | ||||
|     'textNoteEditorType', | ||||
|     'layoutOrientation' | ||||
|     'layoutOrientation', | ||||
|     'allowedHtmlTags' // Allow configuring HTML import tags | ||||
| ]); | ||||
|  | ||||
| function getOptions() { | ||||
|   | ||||
| @@ -210,9 +210,9 @@ const HIDDEN_SUBTREE_DEFINITION: Item = { | ||||
|                     isExpanded: true, | ||||
|                     attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ], | ||||
|                     children: [ | ||||
|                         { id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square', | ||||
|                         { id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-chevron-left', | ||||
|                             attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]}, | ||||
|                         { id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square', | ||||
|                         { id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-chevron-right', | ||||
|                             attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]}, | ||||
|                         { id: '_lbBackendLog', title: 'Backend Log', type: 'launcher', targetNoteId: '_backendLog', icon: 'bx bx-terminal' }, | ||||
|                     ] | ||||
|   | ||||
| @@ -1,5 +1,21 @@ | ||||
| import sanitizeHtml from "sanitize-html"; | ||||
| import sanitizeUrl from "@braintree/sanitize-url"; | ||||
| import optionService from "./options.js"; | ||||
|  | ||||
| // Default list of allowed HTML tags | ||||
| export const DEFAULT_ALLOWED_TAGS = [ | ||||
|     'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', | ||||
|     'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div', | ||||
|     'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img', | ||||
|     'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer', | ||||
|     'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time', | ||||
|     'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins', | ||||
|     'en-media', // for ENEX import | ||||
|     // Additional tags (https://github.com/TriliumNext/Notes/issues/567) | ||||
|     'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd', | ||||
|     'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp', | ||||
|     'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt' | ||||
| ] as const; | ||||
|  | ||||
| // intended mainly as protection against XSS via import | ||||
| // secondarily, it (partly) protects against "CSS takeover" | ||||
| @@ -23,17 +39,18 @@ function sanitize(dirtyHtml: string) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Get allowed tags from options, with fallback to default list if option not yet set | ||||
|     let allowedTags; | ||||
|     try { | ||||
|         allowedTags = JSON.parse(optionService.getOption('allowedHtmlTags')); | ||||
|     } catch (e) { | ||||
|         // Fallback to default list if option doesn't exist or is invalid | ||||
|         allowedTags = DEFAULT_ALLOWED_TAGS; | ||||
|     } | ||||
|  | ||||
|     // to minimize document changes, compress H | ||||
|     return sanitizeHtml(dirtyHtml, { | ||||
|         allowedTags: [ | ||||
|             'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', | ||||
|             'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div', | ||||
|             'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img', | ||||
|             'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer', | ||||
|             'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time', | ||||
|             'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins', | ||||
|             'en-media' // for ENEX import | ||||
|         ], | ||||
|         allowedTags, | ||||
|         allowedAttributes: { | ||||
|             '*': [ 'class', 'style', 'title', 'src', 'href', 'hash', 'disabled', 'align', 'alt', 'center', 'data-*' ] | ||||
|         }, | ||||
| @@ -43,7 +60,10 @@ function sanitize(dirtyHtml: string) { | ||||
|             'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp', | ||||
|             'view-source', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack' | ||||
|         ], | ||||
|         transformTags, | ||||
|         nonTextTags: [ | ||||
|             'head' | ||||
|         ], | ||||
|         transformTags | ||||
|     }); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -149,14 +149,19 @@ function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote) | ||||
| } | ||||
|  | ||||
| function importHtml(taskContext: TaskContext, file: File, parentNote: BNote) { | ||||
|     const title = utils.getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces); | ||||
|     let content = file.buffer.toString("utf-8"); | ||||
|  | ||||
|     // Try to get title from HTML first, fall back to filename | ||||
|     // We do this before sanitization since that turns all <h1>s into <h2> | ||||
|     const htmlTitle = importUtils.extractHtmlTitle(content); | ||||
|     const title = htmlTitle || utils.getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces); | ||||
|  | ||||
|     content = importUtils.handleH1(content, title); | ||||
|  | ||||
|     if (taskContext?.data?.safeImport) { | ||||
|         content = htmlSanitizer.sanitize(content); | ||||
|     }     | ||||
|  | ||||
|     content = importUtils.handleH1(content, title); | ||||
|      | ||||
|     const {note} = noteService.createNewNote({ | ||||
|         parentNoteId: parentNote.noteId, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| function handleH1(content: string, title: string) { | ||||
|     content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => { | ||||
|     content = content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => { | ||||
|         if (title.trim() === text.trim()) { | ||||
|             return ""; // remove whole H1 tag | ||||
|         } else { | ||||
| @@ -11,6 +11,12 @@ function handleH1(content: string, title: string) { | ||||
|     return content; | ||||
| } | ||||
|  | ||||
| function extractHtmlTitle(content: string): string | null { | ||||
|     const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i); | ||||
|     return titleMatch ? titleMatch[1].trim() : null; | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     handleH1 | ||||
|     handleH1, | ||||
|     extractHtmlTitle | ||||
| }; | ||||
|   | ||||
| @@ -135,7 +135,20 @@ const defaultOptions: DefaultOption[] = [ | ||||
|     // Text note configuration | ||||
|     { name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true }, | ||||
|  | ||||
|     { name: "layoutOrientation", value: "vertical", isSynced: false } | ||||
|     // HTML import configuration | ||||
|     { name: "layoutOrientation", value: "vertical", isSynced: false }, | ||||
|     { name: "allowedHtmlTags", value: JSON.stringify([ | ||||
|         'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', | ||||
|         'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div', | ||||
|         'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img', | ||||
|         'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer', | ||||
|         'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time', | ||||
|         'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins', | ||||
|         'en-media', | ||||
|         'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd', | ||||
|         'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp', | ||||
|         'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt' | ||||
|     ]), isSynced: true }, | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -340,9 +340,9 @@ function findFirstNoteWithQuery(query: string, searchContext: SearchContext): BN | ||||
|     return searchResults.length > 0 ? becca.notes[searchResults[0].noteId] : null; | ||||
| } | ||||
|  | ||||
| function searchNotesForAutocomplete(query: string) { | ||||
| function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | ||||
|     const searchContext = new SearchContext({ | ||||
|         fastSearch: true, | ||||
|         fastSearch: fastSearch, | ||||
|         includeArchivedNotes: false, | ||||
|         includeHiddenNotes: true, | ||||
|         fuzzyAttributeSearch: true, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user