mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Merge branch 'master' into custom-search-dialog
# Conflicts: # docs/frontend_api/FrontendScriptApi.html # package-lock.json # package.json
This commit is contained in:
		
							
								
								
									
										5
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							| @@ -6,9 +6,4 @@ | ||||
|   <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK"> | ||||
|     <output url="file://$PROJECT_DIR$/out" /> | ||||
|   </component> | ||||
|   <component name="SwUserDefinedSpecifications"> | ||||
|     <option name="specTypeByUrl"> | ||||
|       <map /> | ||||
|     </option> | ||||
|   </component> | ||||
| </project> | ||||
| @@ -0,0 +1,2 @@ | ||||
| -- removing potential remnants of recent notes in entity changes, see https://github.com/zadam/trilium/issues/2842 | ||||
| DELETE FROM entity_changes WHERE entityName = 'recent_notes'; | ||||
| @@ -237,7 +237,7 @@ | ||||
|  | ||||
|              | ||||
|  | ||||
|             <td class="description last">text, code, file, image, search, book, relation-map - MANDATORY</td> | ||||
|             <td class="description last">text, code, file, image, search, book, relation-map, canvas - MANDATORY</td> | ||||
|         </tr> | ||||
|  | ||||
|      | ||||
|   | ||||
| @@ -238,7 +238,7 @@ function BackendScriptApi(currentNote, apiParams) { | ||||
|      * @property {string} parentNoteId - MANDATORY | ||||
|      * @property {string} title - MANDATORY | ||||
|      * @property {string|buffer} content - MANDATORY | ||||
|      * @property {string} type - text, code, file, image, search, book, relation-map - MANDATORY | ||||
|      * @property {string} type - text, code, file, image, search, book, relation-map, canvas - MANDATORY | ||||
|      * @property {string} mime - value is derived from default mimes for type | ||||
|      * @property {boolean} isProtected - default is false | ||||
|      * @property {boolean} isExpanded - default is false | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -167,7 +167,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line27">line 27</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line28">line 28</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -267,7 +267,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line36">line 36</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line37">line 37</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -335,7 +335,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line44">line 44</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line45">line 45</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -403,7 +403,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line50">line 50</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line51">line 51</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -471,7 +471,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line61">line 61</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line62">line 62</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -543,7 +543,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line71">line 71</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line72">line 72</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -611,7 +611,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line57">line 57</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line58">line 58</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -679,7 +679,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line42">line 42</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line43">line 43</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -747,7 +747,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line47">line 47</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line48">line 48</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -815,7 +815,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line39">line 39</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line40">line 40</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -883,7 +883,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line59">line 59</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line60">line 60</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -955,7 +955,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line66">line 66</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line67">line 67</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -1103,7 +1103,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line498">line 498</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line499">line 499</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -1303,7 +1303,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line236">line 236</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line237">line 237</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -1481,7 +1481,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line520">line 520</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line521">line 521</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -1589,7 +1589,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line161">line 161</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line162">line 162</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -1693,7 +1693,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line144">line 144</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line145">line 145</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -1795,7 +1795,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line171">line 171</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line172">line 172</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -1897,7 +1897,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line209">line 209</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line210">line 210</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -1999,7 +1999,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line214">line 214</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line215">line 215</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -2150,7 +2150,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line560">line 560</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line561">line 561</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -2317,7 +2317,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line384">line 384</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line385">line 385</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -2472,7 +2472,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line584">line 584</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line585">line 585</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -2582,7 +2582,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line706">line 706</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line707">line 707</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -2756,7 +2756,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line487">line 487</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line488">line 488</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -2956,7 +2956,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line223">line 223</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line224">line 224</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -3134,7 +3134,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line509">line 509</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line510">line 510</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -3289,7 +3289,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line554">line 554</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line555">line 555</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -3456,7 +3456,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line376">line 376</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line377">line 377</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -3611,7 +3611,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line578">line 578</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line579">line 579</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -3766,7 +3766,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line566">line 566</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line567">line 567</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -3933,7 +3933,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line452">line 452</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line453">line 453</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -4088,7 +4088,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line590">line 590</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line591">line 591</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -4194,7 +4194,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line151">line 151</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line152">line 152</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -4296,7 +4296,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line136">line 136</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line137">line 137</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -4398,7 +4398,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line179">line 179</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line180">line 180</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -4500,7 +4500,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line184">line 184</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line185">line 185</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -4651,7 +4651,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line572">line 572</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line573">line 573</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -4818,7 +4818,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line460">line 460</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line461">line 461</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -4973,7 +4973,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line602">line 602</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line603">line 603</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -5143,7 +5143,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line612">line 612</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line613">line 613</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -5294,7 +5294,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line596">line 596</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line597">line 597</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -5400,7 +5400,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line750">line 750</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line751">line 751</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -5513,7 +5513,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line685">line 685</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line686">line 686</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -5619,7 +5619,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line695">line 695</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line696">line 696</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -5721,7 +5721,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line626">line 626</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line627">line 627</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -5895,7 +5895,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line469">line 469</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line470">line 470</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6001,7 +6001,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line166">line 166</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line167">line 167</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6152,7 +6152,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line536">line 536</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line537">line 537</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6330,7 +6330,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line478">line 478</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line479">line 479</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6485,7 +6485,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line530">line 530</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line531">line 531</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6640,7 +6640,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line542">line 542</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line543">line 543</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6795,7 +6795,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line548">line 548</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line549">line 549</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6903,7 +6903,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line678">line 678</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line679">line 679</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6987,7 +6987,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line745">line 745</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line746">line 746</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -7093,7 +7093,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line737">line 737</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line738">line 738</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -7199,7 +7199,7 @@ This note's representation is used in note tree and is kept in Froca.</div> | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line109">line 109</a> | ||||
|         <a href="entities_note_short.js.html">entities/note_short.js</a>, <a href="entities_note_short.js.html#line110">line 110</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,8 @@ const NOTE_TYPE_ICONS = { | ||||
|     "relation-map": "bx bx-map-alt", | ||||
|     "book": "bx bx-book", | ||||
|     "note-map": "bx bx-map-alt", | ||||
|     "mermaid": "bx bx-selection" | ||||
|     "mermaid": "bx bx-selection", | ||||
|     "canvas": "bx bx-pen" | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -327,6 +327,24 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
|      */ | ||||
|     this.showError = toastService.showError; | ||||
|  | ||||
|     /** | ||||
|      * Trigger command. | ||||
|      * | ||||
|      * @method | ||||
|      * @param {string} name | ||||
|      * @param {object} data | ||||
|      */ | ||||
|     this.triggerCommand = (name, data) => appContext.triggerCommand(name, data); | ||||
|  | ||||
|     /** | ||||
|      * Trigger event. | ||||
|      * | ||||
|      * @method | ||||
|      * @param {string} name | ||||
|      * @param {object} data | ||||
|      */ | ||||
|     this.triggerEvent = (name, data) => appContext.triggerEvent(name, data); | ||||
|  | ||||
|     /** | ||||
|      * @method | ||||
|      * @deprecated - this is now no-op since all the changes should be gracefully handled per widget | ||||
|   | ||||
							
								
								
									
										70
									
								
								libraries/lodash.debounce.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								libraries/lodash.debounce.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| /** | ||||
|  * Returns a function, that, as long as it continues to be invoked, will not | ||||
|  * be triggered. The function will be called after it stops being called for | ||||
|  * N milliseconds. If `immediate` is passed, trigger the function on the | ||||
|  * leading edge, instead of the trailing. The function also has a property 'clear' | ||||
|  * that is a function which will clear the timer to prevent previously scheduled executions. | ||||
|  * | ||||
|  * @source underscore.js | ||||
|  * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/ | ||||
|  * @param {Function} function to wrap | ||||
|  * @param {Number} timeout in ms (`100`) | ||||
|  * @param {Boolean} whether to execute at the beginning (`false`) | ||||
|  * @api public | ||||
|  */ | ||||
| function debounce(func, wait_ms, immediate){ | ||||
|   var timeout, args, context, timestamp, result; | ||||
|   if (null == wait_ms) wait_ms = 100; | ||||
|  | ||||
|   function later() { | ||||
|     var last = Date.now() - timestamp; | ||||
|  | ||||
|     if (last < wait_ms && last >= 0) { | ||||
|       timeout = setTimeout(later, wait_ms - last); | ||||
|     } else { | ||||
|       timeout = null; | ||||
|       if (!immediate) { | ||||
|         result = func.apply(context, args); | ||||
|         context = args = null; | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   var debounced = function(){ | ||||
|     context = this; | ||||
|     args = arguments; | ||||
|     timestamp = Date.now(); | ||||
|     var callNow = immediate && !timeout; | ||||
|     if (!timeout) timeout = setTimeout(later, wait_ms); | ||||
|     if (callNow) { | ||||
|       result = func.apply(context, args); | ||||
|       context = args = null; | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
|   }; | ||||
|  | ||||
|   debounced.clear = function() { | ||||
|     if (timeout) { | ||||
|       clearTimeout(timeout); | ||||
|       timeout = null; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   debounced.flush = function() { | ||||
|     if (timeout) { | ||||
|       result = func.apply(context, args); | ||||
|       context = args = null; | ||||
|  | ||||
|       clearTimeout(timeout); | ||||
|       timeout = null; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return debounced; | ||||
| }; | ||||
|  | ||||
| // Adds compatibility for ES modules | ||||
| debounce.debounce = debounce; | ||||
|  | ||||
| export default debounce; | ||||
							
								
								
									
										1274
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1274
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							| @@ -24,6 +24,7 @@ | ||||
|     "test-all": "npm run test && npm run test-es6" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@excalidraw/excalidraw": "0.11.0", | ||||
|     "archiver": "5.3.1", | ||||
|     "async-mutex": "0.3.2", | ||||
|     "axios": "0.27.2", | ||||
| @@ -34,7 +35,7 @@ | ||||
|     "cookie-parser": "1.4.6", | ||||
|     "csurf": "1.11.0", | ||||
|     "dayjs": "1.11.2", | ||||
|     "ejs": "3.1.7", | ||||
|     "ejs": "3.1.8", | ||||
|     "electron-debug": "3.2.0", | ||||
|     "electron-dl": "3.3.1", | ||||
|     "electron-window-state": "5.0.3", | ||||
| @@ -42,7 +43,7 @@ | ||||
|     "express": "4.18.1", | ||||
|     "express-partial-content": "1.0.2", | ||||
|     "express-rate-limit": "6.4.0", | ||||
|     "express-session": "1.17.2", | ||||
|     "express-session": "1.17.3", | ||||
|     "fs-extra": "10.1.0", | ||||
|     "helmet": "5.0.2", | ||||
|     "html": "1.0.0", | ||||
| @@ -63,6 +64,8 @@ | ||||
|     "open": "8.4.0", | ||||
|     "portscanner": "2.2.0", | ||||
|     "rand-token": "1.0.1", | ||||
|     "react": "17.0.2", | ||||
|     "react-dom": "17.0.2", | ||||
|     "request": "2.88.2", | ||||
|     "rimraf": "3.0.2", | ||||
|     "sanitize-filename": "1.6.3", | ||||
| @@ -81,7 +84,7 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "cross-env": "7.0.3", | ||||
|     "electron": "16.2.4", | ||||
|     "electron": "16.2.6", | ||||
|     "electron-builder": "23.0.3", | ||||
|     "electron-packager": "15.5.1", | ||||
|     "electron-rebuild": "3.2.7", | ||||
| @@ -90,7 +93,7 @@ | ||||
|     "jsdoc": "3.6.10", | ||||
|     "lorem-ipsum": "2.0.4", | ||||
|     "rcedit": "3.0.1", | ||||
|     "webpack": "5.72.0", | ||||
|     "webpack": "5.72.1", | ||||
|     "webpack-cli": "4.9.2" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|   | ||||
| @@ -58,11 +58,8 @@ describe("Parser", () => { | ||||
|         expect(subs[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(subs[0].tokens).toEqual(["hello", "hi"]); | ||||
|  | ||||
|         expect(subs[1].constructor.name).toEqual("NoteContentProtectedFulltextExp"); | ||||
|         expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp"); | ||||
|         expect(subs[1].tokens).toEqual(["hello", "hi"]); | ||||
|  | ||||
|         expect(subs[2].constructor.name).toEqual("NoteContentUnprotectedFulltextExp"); | ||||
|         expect(subs[2].tokens).toEqual(["hello", "hi"]); | ||||
|     }); | ||||
|  | ||||
|     it("simple label comparison", () => { | ||||
|   | ||||
| @@ -30,6 +30,11 @@ app.use(express.urlencoded({extended: false})); | ||||
| app.use(cookieParser()); | ||||
| app.use(express.static(path.join(__dirname, 'public'))); | ||||
| app.use('/libraries', express.static(path.join(__dirname, '..', 'libraries'))); | ||||
| // excalidraw-view mode in shared notes | ||||
| app.use('/node_modules/react/umd/react.production.min.js', express.static(path.join(__dirname, '..', 'node_modules/react/umd/react.production.min.js'))); | ||||
| app.use('/node_modules/react-dom/umd/react-dom.production.min.js', express.static(path.join(__dirname, '..', 'node_modules/react-dom/umd/react-dom.production.min.js'))); | ||||
| // expose whole dist folder since complete assets are needed in edit and share | ||||
| app.use('/node_modules/@excalidraw/excalidraw/dist/', express.static(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/'))); | ||||
| app.use('/images', express.static(path.join(__dirname, '..', 'images'))); | ||||
| const sessionParser = session({ | ||||
|     secret: sessionSecret, | ||||
|   | ||||
| @@ -171,6 +171,28 @@ async function setContentPane() { | ||||
|  | ||||
|         $content.html($table); | ||||
|     } | ||||
|     else if (revisionItem.type === 'canvas') { | ||||
|         /** | ||||
|          * FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com | ||||
|          *        REMOVE external dependency!!!! This is defined in the svg in defs.style | ||||
|          */ | ||||
|         const content = fullNoteRevision.content; | ||||
|  | ||||
|         try { | ||||
|             const data = JSON.parse(content) | ||||
|             const svg = data.svg || "no svg present." | ||||
|  | ||||
|             /** | ||||
|              * maxWidth: 100% use full width of container but do not enlarge! | ||||
|              * height:auto to ensure that height scales with width | ||||
|              */ | ||||
|             const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"}); | ||||
|             $content.html($('<div>').append($svgHtml)); | ||||
|         } catch(err) { | ||||
|             console.error("error parsing fullNoteRevision.content as JSON", fullNoteRevision.content, err); | ||||
|             $content.html($("<div>").text("Error parsing content. Please check console.error() for more details.")); | ||||
|         } | ||||
|     } | ||||
|     else { | ||||
|         $content.text("Preview isn't available for this note type."); | ||||
|     } | ||||
|   | ||||
| @@ -16,7 +16,8 @@ const NOTE_TYPE_ICONS = { | ||||
|     "relation-map": "bx bx-map-alt", | ||||
|     "book": "bx bx-book", | ||||
|     "note-map": "bx bx-map-alt", | ||||
|     "mermaid": "bx bx-selection" | ||||
|     "mermaid": "bx bx-selection", | ||||
|     "canvas": "bx bx-pen" | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -299,6 +299,24 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
|      */ | ||||
|     this.showError = toastService.showError; | ||||
|  | ||||
|     /** | ||||
|      * Trigger command. | ||||
|      * | ||||
|      * @method | ||||
|      * @param {string} name | ||||
|      * @param {object} data | ||||
|      */ | ||||
|     this.triggerCommand = (name, data) => appContext.triggerCommand(name, data); | ||||
|  | ||||
|     /** | ||||
|      * Trigger event. | ||||
|      * | ||||
|      * @method | ||||
|      * @param {string} name | ||||
|      * @param {object} data | ||||
|      */ | ||||
|     this.triggerEvent = (name, data) => appContext.triggerEvent(name, data); | ||||
|  | ||||
|     /** | ||||
|      * @method | ||||
|      * @deprecated - this is now no-op since all the changes should be gracefully handled per widget | ||||
|   | ||||
| @@ -56,6 +56,17 @@ const MERMAID = { | ||||
|     js: [ "libraries/mermaid.min.js" ] | ||||
| } | ||||
|  | ||||
| const EXCALIDRAW = { | ||||
|     js: [ | ||||
|         "node_modules/react/umd/react.production.min.js", | ||||
|         "node_modules/react-dom/umd/react-dom.production.min.js", | ||||
|         "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js", | ||||
|     ], | ||||
|     // css: [ | ||||
|     //     "stylesheets/somestyle.css" | ||||
|     // ] | ||||
| }; | ||||
|  | ||||
| async function requireLibrary(library) { | ||||
|     if (library.css) { | ||||
|         library.css.map(cssUrl => requireCss(cssUrl)); | ||||
| @@ -106,5 +117,6 @@ export default { | ||||
|     KATEX, | ||||
|     WHEEL_ZOOM, | ||||
|     FORCE_GRAPH, | ||||
|     MERMAID | ||||
|     MERMAID, | ||||
|     EXCALIDRAW | ||||
| } | ||||
|   | ||||
| @@ -141,6 +141,27 @@ async function getRenderedContent(note, options = {}) { | ||||
|  | ||||
|         $renderedContent.append($content); | ||||
|     } | ||||
|     else if (type === 'canvas') { | ||||
|         // make sure surrounding container has size of what is visible. Then image is shrinked to its boundaries | ||||
|         $renderedContent.css({height: "100%", width:"100%"}); | ||||
|          | ||||
|         const noteComplement = await froca.getNoteComplement(note.noteId); | ||||
|         const content = noteComplement.content || ""; | ||||
|  | ||||
|         try { | ||||
|             const placeHolderSVG = "<svg />"; | ||||
|             const data = JSON.parse(content) | ||||
|             const svg = data.svg || placeHolderSVG; | ||||
|             /** | ||||
|              * maxWidth: size down to 100% (full) width of container but do not enlarge! | ||||
|              * height:auto to ensure that height scales with width | ||||
|              */ | ||||
|             $renderedContent.append($(svg).css({maxWidth: "100%", maxHeight: "100%", height: "auto", width: "auto"})); | ||||
|         } catch(err) { | ||||
|             console.error("error parsing content as JSON", content, err); | ||||
|             $renderedContent.append($("<div>").text("Error parsing content. Please check console.error() for more details.")); | ||||
|         } | ||||
|     } | ||||
|     else if (!options.tooltip && type === 'protected-session') { | ||||
|         const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`) | ||||
|             .on('click', protectedSessionService.enterProtectedSession); | ||||
|   | ||||
| @@ -99,13 +99,15 @@ const TPL = ` | ||||
|         padding: 10px; | ||||
|     } | ||||
|      | ||||
|     .note-book-content.type-image img { | ||||
|     .note-book-content.type-image img, .note-book-content.type-canvas svg { | ||||
|         max-width: 100%; | ||||
|         max-height: 100%; | ||||
|         object-fit: contain; | ||||
|     } | ||||
|      | ||||
|     .note-book-card.type-image .note-book-content img, .note-book-card.type-text .note-book-content img { | ||||
|     .note-book-card.type-image .note-book-content img, | ||||
|     .note-book-card.type-text .note-book-content img, | ||||
|     .note-book-card.type-canvas .note-book-content img { | ||||
|         max-width: 100%; | ||||
|         max-height: 100%; | ||||
|     } | ||||
|   | ||||
| @@ -1,17 +1,5 @@ | ||||
| import options from './options.js'; | ||||
| import server from "./server.js"; | ||||
|  | ||||
| let lastProtectedSessionOperationDate = 0; | ||||
|  | ||||
| setInterval(() => { | ||||
|     const protectedSessionTimeout = options.getInt('protectedSessionTimeout'); | ||||
|     if (lastProtectedSessionOperationDate | ||||
|         && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { | ||||
|  | ||||
|         resetProtectedSession(); | ||||
|     } | ||||
| }, 10000); | ||||
|  | ||||
| function enableProtectedSession() { | ||||
|     glob.isProtectedSessionAvailable = true; | ||||
|  | ||||
| @@ -26,9 +14,9 @@ function isProtectedSessionAvailable() { | ||||
|     return glob.isProtectedSessionAvailable; | ||||
| } | ||||
|  | ||||
| function touchProtectedSession() { | ||||
| async function touchProtectedSession() { | ||||
|     if (isProtectedSessionAvailable()) { | ||||
|         lastProtectedSessionOperationDate = Date.now(); | ||||
|         await server.post("login/protected/touch"); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -33,7 +33,8 @@ class TreeContextMenu { | ||||
|             { title: "Note Map", command: command, type: "note-map", uiIcon: "map-alt" }, | ||||
|             { title: "Render HTML note", command: command, type: "render", uiIcon: "extension" }, | ||||
|             { title: "Book", command: command, type: "book", uiIcon: "book" }, | ||||
|             { title: "Mermaid diagram", command: command, type: "mermaid", uiIcon: "selection" } | ||||
|             { title: "Mermaid diagram", command: command, type: "mermaid", uiIcon: "selection" }, | ||||
|             { title: "Canvas", command: command, type: "canvas", uiIcon: "pen" }, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -359,6 +359,12 @@ function isValidAttributeName(name) { | ||||
|     return ATTR_NAME_MATCHER.test(name); | ||||
| } | ||||
|  | ||||
| function sleep(time_ms) { | ||||
|     return new Promise((resolve) => { | ||||
|         setTimeout(resolve, time_ms); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     reloadFrontendApp, | ||||
|     parseDate, | ||||
| @@ -402,5 +408,6 @@ export default { | ||||
|     initHelpButtons, | ||||
|     openHelp, | ||||
|     filterAttributeName, | ||||
|     isValidAttributeName | ||||
|     isValidAttributeName, | ||||
|     sleep, | ||||
| }; | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import FileTypeWidget from "./type_widgets/file.js"; | ||||
| import ImageTypeWidget from "./type_widgets/image.js"; | ||||
| import RenderTypeWidget from "./type_widgets/render.js"; | ||||
| import RelationMapTypeWidget from "./type_widgets/relation_map.js"; | ||||
| import CanvasTypeWidget from "./type_widgets/canvas.js"; | ||||
| import ProtectedSessionTypeWidget from "./type_widgets/protected_session.js"; | ||||
| import BookTypeWidget from "./type_widgets/book.js"; | ||||
| import appContext from "../services/app_context.js"; | ||||
| @@ -50,6 +51,7 @@ const typeWidgetClasses = { | ||||
|     'search': NoneTypeWidget, | ||||
|     'render': RenderTypeWidget, | ||||
|     'relation-map': RelationMapTypeWidget, | ||||
|     'canvas': CanvasTypeWidget, | ||||
|     'protected-session': ProtectedSessionTypeWidget, | ||||
|     'book': BookTypeWidget, | ||||
|     'note-map': NoteMapTypeWidget | ||||
| @@ -66,7 +68,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
|             const {noteId} = note; | ||||
|  | ||||
|             const dto = note.dto; | ||||
|             dto.content = this.getTypeWidget().getContent(); | ||||
|             dto.content = await this.getTypeWidget().getContent(); | ||||
|  | ||||
|             // for read only notes | ||||
|             if (dto.content === undefined) { | ||||
| @@ -145,11 +147,14 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
|         this.checkFullHeight(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sets full height of container that contains note content for a subset of note-types | ||||
|      */ | ||||
|     checkFullHeight() { | ||||
|         // https://github.com/zadam/trilium/issues/2522 | ||||
|         this.$widget.toggleClass("full-height", | ||||
|             !this.noteContext.hasNoteList() | ||||
|             && ['editable-text', 'editable-code'].includes(this.type) | ||||
|             && ['editable-text', 'editable-code', 'canvas'].includes(this.type) | ||||
|             && this.mime !== 'text/x-sqlite;schema=trilium'); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -11,6 +11,7 @@ const NOTE_TYPES = [ | ||||
|     { type: "text", mime: "text/html", title: "Text", selectable: true }, | ||||
|     { type: "relation-map", mime: "application/json", title: "Relation Map", selectable: true }, | ||||
|     { type: "render", mime: '', title: "Render Note", selectable: true }, | ||||
|     { type: "canvas", mime: 'application/json', title: "Canvas", selectable: true }, | ||||
|     { type: "book", mime: '', title: "Book", selectable: true }, | ||||
|     { type: "mermaid", mime: 'text/mermaid', title: "Mermaid Diagram", selectable: true }, | ||||
|     { type: "code", mime: 'text/plain', title: "Code", selectable: true } | ||||
|   | ||||
| @@ -32,7 +32,7 @@ export default class NoteWrapperWidget extends FlexContainer { | ||||
|  | ||||
|     refresh(noteContext) { | ||||
|         this.$widget.toggleClass("full-content-width", | ||||
|             ['image', 'mermaid', 'book', 'render'].includes(noteContext?.note?.type) | ||||
|             ['image', 'mermaid', 'book', 'render', 'canvas'].includes(noteContext?.note?.type) | ||||
|             || !!noteContext?.note?.hasLabel('fullContentWidth') | ||||
|         ); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										477
									
								
								src/public/app/widgets/type_widgets/canvas.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										477
									
								
								src/public/app/widgets/type_widgets/canvas.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,477 @@ | ||||
| import libraryLoader from "../../services/library_loader.js"; | ||||
| import TypeWidget from "./type_widget.js"; | ||||
| import utils from '../../services/utils.js'; | ||||
| import froca from "../../services/froca.js"; | ||||
| import debounce from "../../../../../libraries/lodash.debounce.js"; | ||||
|  | ||||
| const {sleep} = utils; | ||||
|  | ||||
| const TPL = ` | ||||
|     <div class="canvas-widget note-detail-canvas note-detail-printable note-detail"> | ||||
|         <style type="text/css"> | ||||
|         .excalidraw .App-menu_top .buttonList { | ||||
|             display: flex; | ||||
|         } | ||||
|  | ||||
|         .excalidraw-wrapper { | ||||
|             height: 100%; | ||||
|         } | ||||
|  | ||||
|         :root[dir="ltr"] | ||||
|         .excalidraw | ||||
|         .layer-ui__wrapper | ||||
|         .zen-mode-transition.App-menu_bottom--transition-left { | ||||
|             transform: none; | ||||
|         } | ||||
|  | ||||
|         </style> | ||||
|         <!-- height here necessary. otherwise excalidraw not shown --> | ||||
|         <div class="canvas-render" style="height: 100%"></div> | ||||
|     </div> | ||||
| `; | ||||
|  | ||||
| /** | ||||
|  * # Canvas note with excalidraw | ||||
|  * @author thfrei 2022-05-11 | ||||
|  *  | ||||
|  * Background: | ||||
|  * excalidraw gives great support for hand drawn notes. It also allows to include images and support | ||||
|  * for sketching. Excalidraw has a vibrant and active community. | ||||
|  *  | ||||
|  * Functionality: | ||||
|  * We store the excalidraw assets (elements, appState, files) in the note. In addition to that, we  | ||||
|  * export the SVG from the canvas on every update. The SVG is also saved in the note. It is used | ||||
|  * for displaying any canvas note inside of a text note as an image. | ||||
|  *  | ||||
|  * Paths not taken. | ||||
|  *  - excalidraw-to-svg (node.js) could be used to avoid storing the svg in the backend. | ||||
|  *    We could render the SVG on the fly. However, as of now, it does not render any hand drawn | ||||
|  *    (freedraw) paths. There is an issue with Path2D object not present in node-canvas library  | ||||
|  *    used by jsdom. (See Trilium PR for samples and other issues in respective library. | ||||
|  *    Link will be added later). Related links: | ||||
|  *     - https://github.com/Automattic/node-canvas/pull/2013 | ||||
|  *     - https://github.com/google/canvas-5-polyfill  | ||||
|  *     - https://github.com/Automattic/node-canvas/issues/1116  | ||||
|  *     - https://www.npmjs.com/package/path2d-polyfill  | ||||
|  *  - excalidraw-to-svg (node.js) takes quite some time to load an image (1-2s) | ||||
|  *  - excalidraw-utils (browser) does render freedraw, however NOT freedraw with background. It is not | ||||
|  *    used, since it is a big dependency, and has the same functionality as react + excalidraw. | ||||
|  *  - infinite-drawing-canvas with fabric.js. This library lacked a lot of feature, excalidraw already | ||||
|  *    has. | ||||
|  *  | ||||
|  * Known issues: | ||||
|  *  - v0.11.0 of excalidraw does not render freedraw backgrounds in the svg | ||||
|  *  - the 3 excalidraw fonts should be included in the share and everywhere, so that it is shown | ||||
|  *    when requiring svg. | ||||
|  *  | ||||
|  * Discussion of storing svg in the note: | ||||
|  *  - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there. | ||||
|  *  - Con: The note will get bigger (~40-50%?), we will generate more bandwith. However, using trilium  | ||||
|  *         desktop instance mitigates that issue. | ||||
|  *  | ||||
|  * Roadmap: | ||||
|  *  - Support image-notes as reference in excalidraw | ||||
|  *  - Support canvas note as reference (svg) in other canvas notes. | ||||
|  *  - Make it easy to include a canvas note inside a text note | ||||
|  *  - Support for excalidraw libraries. Maybe special code notes with a tag. | ||||
|  */ | ||||
| export default class ExcalidrawTypeWidget extends TypeWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         // constants | ||||
|         this.SCENE_VERSION_INITIAL = -1; | ||||
|         this.SCENE_VERSION_ERROR = -2; | ||||
|  | ||||
|         // config | ||||
|         this.DEBOUNCE_TIME_ONCHANGEHANDLER = 750; // ms | ||||
|         // ensure that assets are loaded from trilium | ||||
|         window.EXCALIDRAW_ASSET_PATH = `${window.location.origin}/node_modules/@excalidraw/excalidraw/dist/`; | ||||
|  | ||||
|         // temporary vars | ||||
|         this.currentNoteId = ""; | ||||
|         this.currentSceneVersion = this.SCENE_VERSION_INITIAL; | ||||
|  | ||||
|         // will be overwritten | ||||
|         this.excalidrawRef; | ||||
|         this.$render; | ||||
|         this.renderElement; | ||||
|         this.$widget; | ||||
|         this.reactHandlers; // used to control react state | ||||
|          | ||||
|         this.createExcalidrawReactApp = this.createExcalidrawReactApp.bind(this); | ||||
|         this.onChangeHandler = this.onChangeHandler.bind(this); | ||||
|         this.isNewSceneVersion = this.isNewSceneVersion.bind(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * (trilium) | ||||
|      * @returns {string} "canvas" | ||||
|      */ | ||||
|     static getType() { | ||||
|         return "canvas"; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * (trilium) | ||||
|      * renders note | ||||
|      */ | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|  | ||||
|         this.$widget.toggleClass("full-height", true); // only add | ||||
|         this.$render = this.$widget.find('.canvas-render'); | ||||
|         this.renderElement = this.$render.get(0); | ||||
|  | ||||
|         libraryLoader | ||||
|             .requireLibrary(libraryLoader.EXCALIDRAW) | ||||
|             .then(() => { | ||||
|                 const React = window.React; | ||||
|                 const ReactDOM = window.ReactDOM; | ||||
|                  | ||||
|                 ReactDOM.unmountComponentAtNode(this.renderElement); | ||||
|                 ReactDOM.render(React.createElement(this.createExcalidrawReactApp), this.renderElement); | ||||
|             }) | ||||
|  | ||||
|         return this.$widget; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * (trilium) | ||||
|      * called to populate the widget container with the note content | ||||
|      *  | ||||
|      * @param {note} note  | ||||
|      */ | ||||
|     async doRefresh(note) { | ||||
|         // see if note changed, since we do not get a new class for a new note | ||||
|         const noteChanged = this.currentNoteId !== note.noteId; | ||||
|         if (noteChanged) { | ||||
|             // reset scene to omit unnecessary onchange handler | ||||
|             this.currentSceneVersion = this.SCENE_VERSION_INITIAL; | ||||
|         } | ||||
|         this.currentNoteId = note.noteId; | ||||
|          | ||||
|         // get note from backend and put into canvas | ||||
|         const noteComplement = await froca.getNoteComplement(note.noteId); | ||||
|  | ||||
|         // before we load content into excalidraw, make sure excalidraw has loaded | ||||
|         while (!this.excalidrawRef || !this.excalidrawRef.current) { | ||||
|             this.log("excalidrawRef not yet loeaded, sleep 200ms..."); | ||||
|             await sleep(200); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * new and empty note - make sure that canvas is empty. | ||||
|          * If we do not set it manually, we occasionally get some "bleeding" from another | ||||
|          * note into this fresh note. Probably due to that this note-instance does not get | ||||
|          * newly instantiated? | ||||
|          */ | ||||
|         if (this.excalidrawRef.current && noteComplement.content === "") { | ||||
|             const sceneData = { | ||||
|                 elements: [],  | ||||
|                 appState: {}, | ||||
|                 collaborators: [] | ||||
|             }; | ||||
|              | ||||
|             this.excalidrawRef.current.updateScene(sceneData); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * load saved content into excalidraw canvas | ||||
|          */ | ||||
|         else if (this.excalidrawRef.current && noteComplement.content) { | ||||
|             let content ={ | ||||
|                 elements: [], | ||||
|                 appState: [], | ||||
|                 files: [], | ||||
|             }; | ||||
|  | ||||
|             try { | ||||
|                 content = JSON.parse(noteComplement.content || ""); | ||||
|             } catch(err) { | ||||
|                 console.error("Error parsing content. Probably note.type changed", | ||||
|                               "Starting with empty canvas" | ||||
|                               , note, noteComplement, err); | ||||
|             } | ||||
|  | ||||
|             const {elements, appState, files} = content; | ||||
|  | ||||
|             /** | ||||
|              * use widths and offsets of current view, since stored appState has the state from | ||||
|              * previous edit. using the stored state would lead to pointer mismatch. | ||||
|              */ | ||||
|             const boundingClientRect = this.excalidrawWrapperRef.current.getBoundingClientRect(); | ||||
|             appState.width = boundingClientRect.width; | ||||
|             appState.height = boundingClientRect.height; | ||||
|             appState.offsetLeft = boundingClientRect.left; | ||||
|             appState.offsetTop = boundingClientRect.top; | ||||
|  | ||||
|             const sceneData = { | ||||
|                 elements,  | ||||
|                 appState, | ||||
|                 collaborators: [] | ||||
|             }; | ||||
|  | ||||
|             // files are expected in an array when loading. they are stored as an key-index object | ||||
|             // see example for loading here: | ||||
|             // https://github.com/excalidraw/excalidraw/blob/c5a7723185f6ca05e0ceb0b0d45c4e3fbcb81b2a/src/packages/excalidraw/example/App.js#L68 | ||||
|             const fileArray = []; | ||||
|             for (const fileId in files) { | ||||
|                 const file = files[fileId]; | ||||
|                 // TODO: dataURL is replaceable with a trilium image url | ||||
|                 //       maybe we can save normal images (pasted) with base64 data url, and trilium images | ||||
|                 //       with their respective url! nice | ||||
|                 // file.dataURL = "http://localhost:8080/api/images/ltjOiU8nwoZx/start.png"; | ||||
|                 fileArray.push(file); | ||||
|             } | ||||
|  | ||||
|             this.sceneVersion = window.Excalidraw.getSceneVersion(elements); | ||||
|  | ||||
|             this.excalidrawRef.current.updateScene(sceneData); | ||||
|             this.excalidrawRef.current.addFiles(fileArray); | ||||
|         } | ||||
|          | ||||
|         // set initial scene version | ||||
|         if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) { | ||||
|             this.currentSceneVersion = this.getSceneVersion(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * (trilium) | ||||
|      * gets data from widget container that will be sent via spacedUpdate.scheduleUpdate(); | ||||
|      * this is automatically called after this.saveData(); | ||||
|      */ | ||||
|     async getContent() { | ||||
|         const elements = this.excalidrawRef.current.getSceneElements(); | ||||
|         const appState = this.excalidrawRef.current.getAppState(); | ||||
|          | ||||
|         /** | ||||
|          * A file is not deleted, even though removed from canvas. therefore we only keep | ||||
|          * files that are referenced by an element. Maybe this will change with new excalidraw version? | ||||
|          */ | ||||
|         const files = this.excalidrawRef.current.getFiles(); | ||||
|          | ||||
|         /** | ||||
|          * parallel svg export to combat bitrot and enable rendering image for note inclusion, | ||||
|          * preview and share. | ||||
|          */ | ||||
|         const svg = await window.Excalidraw.exportToSvg({ | ||||
|             elements, | ||||
|             appState, | ||||
|             exportPadding: 5, // 5 px padding | ||||
|             metadata: 'trilium-export', | ||||
|             files | ||||
|         }); | ||||
|         const svgString = svg.outerHTML; | ||||
|  | ||||
|         /** | ||||
|          * workaround until https://github.com/excalidraw/excalidraw/pull/5065 is merged and published | ||||
|          */ | ||||
|         const svgSafeString = this.replaceExternalAssets(svgString); | ||||
|  | ||||
|         const activeFiles = {}; | ||||
|         elements.forEach((element) => { | ||||
|             if (element.fileId) { | ||||
|                 activeFiles[element.fileId] = files[element.fileId]; | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         const content = { | ||||
|             _meta: "This note has type `canvas`. It uses excalidraw and stores an exported svg alongside.", | ||||
|             elements, // excalidraw | ||||
|             appState, // excalidraw | ||||
|             files: activeFiles, // excalidraw | ||||
|             svg: svgSafeString, // not needed for excalidraw, used for note_short, content, and image api | ||||
|         }; | ||||
|  | ||||
|         const contentString = JSON.stringify(content); | ||||
|  | ||||
|         return contentString; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * (trilium) | ||||
|      * save content to backend | ||||
|      * spacedUpdate is kind of a debouncer. | ||||
|      */ | ||||
|     saveData() { | ||||
|         this.spacedUpdate.scheduleUpdate(); | ||||
|     } | ||||
|  | ||||
|     onChangeHandler() { | ||||
|         const appState = this.excalidrawRef.current.getAppState() || {}; | ||||
|  | ||||
|         // changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc. | ||||
|         // make sure only when a new element is added, we actually save something. | ||||
|         const isNewSceneVersion = this.isNewSceneVersion(); | ||||
|         /** | ||||
|          * FIXME: however, we might want to make an exception, if viewport changed, since viewport | ||||
|          *        is desired to save? (add) and appState background, and some things | ||||
|          */ | ||||
|  | ||||
|         // upon updateScene, onchange is called, even though "nothing really changed" that is worth saving | ||||
|         const isNotInitialScene = this.currentSceneVersion !== this.SCENE_VERSION_INITIAL; | ||||
|  | ||||
|         const shouldSave = isNewSceneVersion && isNotInitialScene; | ||||
|  | ||||
|         if (shouldSave) { | ||||
|             this.updateSceneVersion(); | ||||
|             this.saveData(); | ||||
|         } else { | ||||
|             // do nothing | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     createExcalidrawReactApp() { | ||||
|         const React = window.React; | ||||
|         const Excalidraw = window.Excalidraw; | ||||
|  | ||||
|         const excalidrawRef = React.useRef(null); | ||||
|         this.excalidrawRef = excalidrawRef; | ||||
|         const excalidrawWrapperRef = React.useRef(null); | ||||
|         this.excalidrawWrapperRef = excalidrawWrapperRef; | ||||
|         const [dimensions, setDimensions] = React.useState({ | ||||
|             width: undefined, | ||||
|             height: undefined | ||||
|         }); | ||||
|  | ||||
|         const [viewModeEnabled, setViewModeEnabled] = React.useState(false); | ||||
|         const [zenModeEnabled, setZenModeEnabled] = React.useState(false); | ||||
|         const [gridModeEnabled, setGridModeEnabled] = React.useState(false); | ||||
|         const [synchronized, setSynchronized] = React.useState(true); | ||||
|          | ||||
|         React.useEffect(() => { | ||||
|             const dimensions = { | ||||
|                 width: excalidrawWrapperRef.current.getBoundingClientRect().width, | ||||
|                 height: excalidrawWrapperRef.current.getBoundingClientRect().height | ||||
|             }; | ||||
|             setDimensions(dimensions); | ||||
|  | ||||
|             const onResize = () => { | ||||
|                 const dimensions = { | ||||
|                     width: excalidrawWrapperRef.current.getBoundingClientRect().width, | ||||
|                     height: excalidrawWrapperRef.current.getBoundingClientRect().height | ||||
|                 }; | ||||
|                 setDimensions(dimensions); | ||||
|             }; | ||||
|              | ||||
|             window.addEventListener("resize", onResize); | ||||
|              | ||||
|             return () => window.removeEventListener("resize", onResize); | ||||
|         }, [excalidrawWrapperRef]); | ||||
|  | ||||
|         const onLinkOpen = React.useCallback((element, event) => { | ||||
|             const link = element.link; | ||||
|             const { nativeEvent } = event.detail; | ||||
|             const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey; | ||||
|             const isNewWindow = nativeEvent.shiftKey; | ||||
|             const isInternalLink = link.startsWith("/")  | ||||
|                 || link.includes(window.location.origin); | ||||
|                  | ||||
|             if (isInternalLink && !isNewTab && !isNewWindow) { | ||||
|                 // signal that we're handling the redirect ourselves | ||||
|                 event.preventDefault(); | ||||
|                 // do a custom redirect, such as passing to react-router | ||||
|                 // ... | ||||
|             } else { | ||||
|                 // open in same tab | ||||
|             } | ||||
|           }, []); | ||||
|  | ||||
|         return React.createElement( | ||||
|             React.Fragment, | ||||
|             null, | ||||
|             React.createElement( | ||||
|                 "div", | ||||
|                 { | ||||
|                     className: "excalidraw-wrapper", | ||||
|                     ref: excalidrawWrapperRef | ||||
|                 }, | ||||
|                 React.createElement(Excalidraw.default, { | ||||
|                     ref: excalidrawRef, | ||||
|                     width: dimensions.width, | ||||
|                     height: dimensions.height, | ||||
|                     // initialData: InitialData, | ||||
|                     onPaste: (data, event) => { | ||||
|                         this.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event); | ||||
|                     }, | ||||
|                     onChange: debounce(this.onChangeHandler, this.DEBOUNCE_TIME_ONCHANGEHANDLER), | ||||
|                     // onPointerUpdate: (payload) => console.log(payload), | ||||
|                     onCollabButtonClick: () => { | ||||
|                         window.alert("You clicked on collab button. No collaboration is implemented."); | ||||
|                     }, | ||||
|                     viewModeEnabled: viewModeEnabled, | ||||
|                     zenModeEnabled: zenModeEnabled, | ||||
|                     gridModeEnabled: gridModeEnabled, | ||||
|                     isCollaborating: false, | ||||
|                     detectScroll: false, | ||||
|                     handleKeyboardGlobally: false, | ||||
|                     autoFocus: true, | ||||
|                     onLinkOpen, | ||||
|                 }) | ||||
|             ) | ||||
|         ); | ||||
|     }     | ||||
|  | ||||
|     /** | ||||
|      * needed to ensure, that multipleOnChangeHandler calls do not trigger a safe. | ||||
|      * we compare the scene version as suggested in: | ||||
|      * https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329 | ||||
|      *  | ||||
|      * info: sceneVersions are not incrementing. it seems to be a pseudo-random number | ||||
|      */ | ||||
|      isNewSceneVersion() { | ||||
|         const sceneVersion = this.getSceneVersion(); | ||||
|          | ||||
|         return this.currentSceneVersion === this.SCENE_VERSION_INITIAL // initial scene version update | ||||
|             || this.currentSceneVersion !== sceneVersion // ensure scene changed | ||||
|         ; | ||||
|     } | ||||
|  | ||||
|     getSceneVersion() { | ||||
|         if (this.excalidrawRef) { | ||||
|             const elements = this.excalidrawRef.current.getSceneElements(); | ||||
|             const sceneVersion = window.Excalidraw.getSceneVersion(elements); | ||||
|             return sceneVersion; | ||||
|         } else { | ||||
|             return this.SCENE_VERSION_ERROR; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateSceneVersion() { | ||||
|         this.currentSceneVersion = this.getSceneVersion(); | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * logs to console.log with some predefined title | ||||
|      *  | ||||
|      * @param  {...any} args  | ||||
|      */ | ||||
|     log(...args) { | ||||
|         let title = ''; | ||||
|         if (this.note) { | ||||
|             title = this.note.title; | ||||
|         } else { | ||||
|             title = this.noteId + "nt/na"; | ||||
|         } | ||||
|  | ||||
|         console.log(title, "=", this.noteId, "==",  ...args); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * replaces exlicraw.com with own assets | ||||
|      *  | ||||
|      * workaround until https://github.com/excalidraw/excalidraw/pull/5065 is merged and published | ||||
|      * needed for v0.11.0 | ||||
|      *  | ||||
|      * @param {string} string  | ||||
|      * @returns  | ||||
|      */ | ||||
|     replaceExternalAssets = (string) => { | ||||
|         let result = string; | ||||
|         // exlidraw.com asset in react usage | ||||
|         result = result.replaceAll("https://excalidraw.com/", window.EXCALIDRAW_ASSET_PATH+"excalidraw-assets/"); | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
| @@ -38,6 +38,9 @@ export default class TypeWidget extends NoteContextAwareWidget { | ||||
|         return this.$widget.is(":visible") && this.noteContext?.ntxId === appContext.tabManager.activeNtxId; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {Promise|*} promise resolving content or directly the content | ||||
|      */  | ||||
|     getContent() {} | ||||
|  | ||||
|     focus() {} | ||||
|   | ||||
| @@ -11,7 +11,7 @@ function returnImage(req, res) { | ||||
|     if (!image) { | ||||
|         return res.sendStatus(404); | ||||
|     } | ||||
|     else if (image.type !== 'image') { | ||||
|     else if (!["image", "canvas"].includes(image.type)){ | ||||
|         return res.sendStatus(400); | ||||
|     } | ||||
|     else if (image.isDeleted || image.data === null) { | ||||
| @@ -19,10 +19,27 @@ function returnImage(req, res) { | ||||
|         return res.send(fs.readFileSync(RESOURCE_DIR + '/db/image-deleted.png')); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * special "image" type. the canvas is actually type application/json  | ||||
|      * to avoid bitrot and enable usage as referenced image the svg is included. | ||||
|      */ | ||||
|     if (image.type === 'canvas') { | ||||
|         const content = image.getContent(); | ||||
|         try { | ||||
|             const data = JSON.parse(content); | ||||
|              | ||||
|             const svg = data.svg || '<svg />' | ||||
|             res.set('Content-Type', "image/svg+xml"); | ||||
|             res.set("Cache-Control", "no-cache, no-store, must-revalidate"); | ||||
|             res.send(svg); | ||||
|         } catch(err) { | ||||
|             res.status(500).send("there was an error parsing excalidraw to svg"); | ||||
|         } | ||||
|     } else { | ||||
|         res.set('Content-Type', image.mime); | ||||
|         res.set("Cache-Control", "no-cache, no-store, must-revalidate"); | ||||
|  | ||||
|         res.send(image.getContent()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function uploadImage(req) { | ||||
|   | ||||
| @@ -83,6 +83,10 @@ function logoutFromProtectedSession() { | ||||
|     ws.sendMessageToAllClients({ type: 'protectedSessionLogout' }); | ||||
| } | ||||
|  | ||||
| function touchProtectedSession() { | ||||
|     protectedSessionService.touchProtectedSession(); | ||||
| } | ||||
|  | ||||
| function token(req) { | ||||
|     const password = req.body.password; | ||||
|  | ||||
| @@ -102,5 +106,6 @@ module.exports = { | ||||
|     loginSync, | ||||
|     loginToProtectedSession, | ||||
|     logoutFromProtectedSession, | ||||
|     touchProtectedSession, | ||||
|     token | ||||
| }; | ||||
|   | ||||
| @@ -285,6 +285,7 @@ function register(app) { | ||||
|     apiRoute(POST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote); | ||||
|     apiRoute(POST, '/api/special-notes/save-search-note', specialNotesRoute.saveSearchNote); | ||||
|  | ||||
|     // :filename is not used by trilium, but instead used for "save as" to assign a human readable filename | ||||
|     route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage); | ||||
|     route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware], imageRoute.uploadImage, apiResultHandler); | ||||
|     route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware], imageRoute.updateImage, apiResultHandler); | ||||
| @@ -358,6 +359,7 @@ function register(app) { | ||||
|     route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); | ||||
|     // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) | ||||
|     apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession); | ||||
|     apiRoute(POST, '/api/login/protected/touch', loginApiRoute.touchProtectedSession); | ||||
|     apiRoute(POST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession); | ||||
|  | ||||
|     route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler); | ||||
|   | ||||
| @@ -210,7 +210,7 @@ function BackendScriptApi(currentNote, apiParams) { | ||||
|      * @property {string} parentNoteId - MANDATORY | ||||
|      * @property {string} title - MANDATORY | ||||
|      * @property {string|buffer} content - MANDATORY | ||||
|      * @property {string} type - text, code, file, image, search, book, relation-map - MANDATORY | ||||
|      * @property {string} type - text, code, file, image, search, book, relation-map, canvas - MANDATORY | ||||
|      * @property {string} mime - value is derived from default mimes for type | ||||
|      * @property {boolean} isProtected - default is false | ||||
|      * @property {boolean} isExpanded - default is false | ||||
|   | ||||
| @@ -135,7 +135,6 @@ function fillAllEntityChanges() { | ||||
|         fillEntityChanges("branches", "branchId"); | ||||
|         fillEntityChanges("note_revisions", "noteRevisionId"); | ||||
|         fillEntityChanges("note_revision_contents", "noteRevisionId"); | ||||
|         fillEntityChanges("recent_notes", "noteId"); | ||||
|         fillEntityChanges("attributes", "attributeId"); | ||||
|         fillEntityChanges("etapi_tokens", "etapiTokenId"); | ||||
|         fillEntityChanges("options", "name", 'isSynced = 1'); | ||||
|   | ||||
| @@ -41,7 +41,7 @@ function exportSingleNote(taskContext, branch, format, res) { | ||||
|         extension = mimeTypes.extension(note.mime) || 'code'; | ||||
|         mime = note.mime; | ||||
|     } | ||||
|     else if (note.type === 'relation-map' || note.type === 'search') { | ||||
|     else if (note.type === 'relation-map' || note.type === 'canvas' || note.type === 'search') { | ||||
|         payload = content; | ||||
|         extension = 'json'; | ||||
|         mime = 'application/json'; | ||||
|   | ||||
| @@ -8,5 +8,6 @@ module.exports = [ | ||||
|     'relation-map',  | ||||
|     'book',  | ||||
|     'note-map', | ||||
|     'mermaid' | ||||
|     'mermaid', | ||||
|     'canvas' | ||||
| ]; | ||||
| @@ -53,7 +53,7 @@ function deriveMime(type, mime) { | ||||
|         mime = 'text/html'; | ||||
|     } else if (type === 'code' || type === 'mermaid') { | ||||
|         mime = 'text/plain'; | ||||
|     } else if (['relation-map', 'search'].includes(type)) { | ||||
|     } else if (['relation-map', 'search', 'canvas'].includes(type)) { | ||||
|         mime = 'application/json'; | ||||
|     } else if (['render', 'book'].includes(type)) { | ||||
|         mime = ''; | ||||
| @@ -84,7 +84,7 @@ function copyChildAttributes(parentNote, childNote) { | ||||
|  * - {string} parentNoteId | ||||
|  * - {string} title | ||||
|  * - {*} content | ||||
|  * - {string} type - text, code, file, image, search, book, relation-map, render | ||||
|  * - {string} type - text, code, file, image, search, book, relation-map, canvas, render | ||||
|  * | ||||
|  * Following are optional (have defaults) | ||||
|  * - {string} mime - value is derived from default mimes for type | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| const log = require('./log'); | ||||
| const dataEncryptionService = require('./data_encryption'); | ||||
| const options = require("./options"); | ||||
|  | ||||
| let dataKey = null; | ||||
|  | ||||
| @@ -54,6 +55,27 @@ function decryptString(cipherText) { | ||||
|     return dataEncryptionService.decryptString(getDataKey(), cipherText); | ||||
| } | ||||
|  | ||||
| let lastProtectedSessionOperationDate = null; | ||||
|  | ||||
| function touchProtectedSession() { | ||||
|     if (isProtectedSessionAvailable()) { | ||||
|         lastProtectedSessionOperationDate = Date.now(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| setInterval(() => { | ||||
|     const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); | ||||
|     if (isProtectedSessionAvailable() | ||||
|         && lastProtectedSessionOperationDate | ||||
|         && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { | ||||
|  | ||||
|         resetDataKey(); | ||||
|  | ||||
|         require('./ws').reloadFrontend(); | ||||
|     } | ||||
| }, 30000); | ||||
|  | ||||
|  | ||||
| module.exports = { | ||||
|     setDataKey, | ||||
|     resetDataKey, | ||||
| @@ -61,5 +83,6 @@ module.exports = { | ||||
|     encrypt, | ||||
|     decrypt, | ||||
|     decryptString, | ||||
|     decryptNotes | ||||
|     decryptNotes, | ||||
|     touchProtectedSession | ||||
| }; | ||||
|   | ||||
							
								
								
									
										116
									
								
								src/services/search/expressions/note_content_fulltext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/services/search/expressions/note_content_fulltext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const Expression = require('./expression'); | ||||
| const NoteSet = require('../note_set'); | ||||
| const log = require('../../log'); | ||||
| const becca = require('../../../becca/becca'); | ||||
| const protectedSessionService = require('../../protected_session'); | ||||
| const striptags = require('striptags'); | ||||
| const utils = require("../../utils"); | ||||
|  | ||||
| const ALLOWED_OPERATORS = ['*=*', '=', '*=', '=*', '%=']; | ||||
|  | ||||
| const cachedRegexes = {}; | ||||
|  | ||||
| function getRegex(str) { | ||||
|     if (!(str in cachedRegexes)) { | ||||
|         cachedRegexes[str] = new RegExp(str, 'ms'); // multiline, dot-all | ||||
|     } | ||||
|  | ||||
|     return cachedRegexes[str]; | ||||
| } | ||||
|  | ||||
| class NoteContentFulltextExp extends Expression { | ||||
|     constructor(operator, {tokens, raw, flatText}) { | ||||
|         super(); | ||||
|  | ||||
|         if (!ALLOWED_OPERATORS.includes(operator)) { | ||||
|             throw new Error(`Note content can be searched only with operators: ` + ALLOWED_OPERATORS.join(", ") + `, operator ${operator} given.`); | ||||
|         } | ||||
|  | ||||
|         this.operator = operator; | ||||
|         this.tokens = tokens; | ||||
|         this.raw = !!raw; | ||||
|         this.flatText = !!flatText; | ||||
|     } | ||||
|  | ||||
|     execute(inputNoteSet) { | ||||
|         const resultNoteSet = new NoteSet(); | ||||
|         const sql = require('../../sql'); | ||||
|  | ||||
|         for (let {noteId, type, mime, content, isProtected} of sql.iterateRows(` | ||||
|                 SELECT noteId, type, mime, content, isProtected | ||||
|                 FROM notes JOIN note_contents USING (noteId)  | ||||
|                 WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0`)) { | ||||
|  | ||||
|             if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if (isProtected) { | ||||
|                 if (!protectedSessionService.isProtectedSessionAvailable()) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 try { | ||||
|                     content = protectedSessionService.decryptString(content); | ||||
|                 } catch (e) { | ||||
|                     log.info(`Cannot decrypt content of note ${noteId}`); | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             content = this.preprocessContent(content, type, mime); | ||||
|  | ||||
|             if (this.tokens.length === 1) { | ||||
|                 const [token] = this.tokens; | ||||
|  | ||||
|                 if ((this.operator === '=' && token === content) | ||||
|                     || (this.operator === '*=' && content.endsWith(token)) | ||||
|                     || (this.operator === '=*' && content.startsWith(token)) | ||||
|                     || (this.operator === '*=*' && content.includes(token)) | ||||
|                     || (this.operator === '%=' && getRegex(token).test(content))) { | ||||
|  | ||||
|                     resultNoteSet.add(becca.notes[noteId]); | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 const nonMatchingToken = this.tokens.find(token => | ||||
|                     !content.includes(token) && | ||||
|                     ( | ||||
|                         // in case of default fulltext search we should consider both title, attrs and content | ||||
|                         // so e.g. "hello world" should match when "hello" is in title and "world" in content | ||||
|                         !this.flatText | ||||
|                         || !becca.notes[noteId].getFlatText().includes(token) | ||||
|                     ) | ||||
|                 ); | ||||
|  | ||||
|                 if (!nonMatchingToken) { | ||||
|                     resultNoteSet.add(becca.notes[noteId]); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return resultNoteSet; | ||||
|     } | ||||
|  | ||||
|     preprocessContent(content, type, mime) { | ||||
|         content = utils.normalize(content.toString()); | ||||
|  | ||||
|         if (type === 'text' && mime === 'text/html') { | ||||
|             if (!this.raw && content.length < 20000) { // striptags is slow for very large notes | ||||
|                 // allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 | ||||
|                 content = striptags(content, ['a']); | ||||
|  | ||||
|                 // at least the closing tag can be easily stripped | ||||
|                 content = content.replace(/<\/a>/ig, ""); | ||||
|             } | ||||
|  | ||||
|             content = content.replace(/ /g, ' '); | ||||
|         } | ||||
|  | ||||
|         return content.trim(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = NoteContentFulltextExp; | ||||
| @@ -1,89 +0,0 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const Expression = require('./expression'); | ||||
| const NoteSet = require('../note_set'); | ||||
| const log = require('../../log'); | ||||
| const becca = require('../../../becca/becca'); | ||||
| const protectedSessionService = require('../../protected_session'); | ||||
| const striptags = require('striptags'); | ||||
| const utils = require("../../utils"); | ||||
|  | ||||
| // FIXME: create common subclass with NoteContentUnprotectedFulltextExp to avoid duplication | ||||
| class NoteContentProtectedFulltextExp extends Expression { | ||||
|     constructor(operator, {tokens, raw, flatText}) { | ||||
|         super(); | ||||
|  | ||||
|         if (operator !== '*=*') { | ||||
|             throw new Error(`Note content can be searched only with *=* operator`); | ||||
|         } | ||||
|  | ||||
|         this.tokens = tokens; | ||||
|         this.raw = !!raw; | ||||
|         this.flatText = !!flatText; | ||||
|     } | ||||
|  | ||||
|     execute(inputNoteSet) { | ||||
|         const resultNoteSet = new NoteSet(); | ||||
|  | ||||
|         if (!protectedSessionService.isProtectedSessionAvailable()) { | ||||
|             return resultNoteSet; | ||||
|         } | ||||
|  | ||||
|         const sql = require('../../sql'); | ||||
|  | ||||
|         for (let {noteId, type, mime, content} of sql.iterateRows(` | ||||
|                 SELECT noteId, type, mime, content  | ||||
|                 FROM notes JOIN note_contents USING (noteId)  | ||||
|                 WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0 AND isProtected = 1`)) { | ||||
|  | ||||
|             if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             try { | ||||
|                 content = protectedSessionService.decryptString(content); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 log.info(`Cannot decrypt content of note ${noteId}`); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             content = this.preprocessContent(content, type, mime); | ||||
|  | ||||
|             const nonMatchingToken = this.tokens.find(token => | ||||
|                 !content.includes(token) && | ||||
|                 ( | ||||
|                     // in case of default fulltext search we should consider both title, attrs and content | ||||
|                     // so e.g. "hello world" should match when "hello" is in title and "world" in content | ||||
|                     !this.flatText | ||||
|                     || !becca.notes[noteId].getFlatText().includes(token) | ||||
|                 ) | ||||
|             ); | ||||
|  | ||||
|             if (!nonMatchingToken) { | ||||
|                 resultNoteSet.add(becca.notes[noteId]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return resultNoteSet; | ||||
|     } | ||||
|  | ||||
|     preprocessContent(content, type, mime) { | ||||
|         content = utils.normalize(content.toString()); | ||||
|  | ||||
|         if (type === 'text' && mime === 'text/html') { | ||||
|             if (!this.raw && content.length < 20000) { // striptags is slow for very large notes | ||||
|                 // allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 | ||||
|                 content = striptags(content, ['a']); | ||||
|  | ||||
|                 // at least the closing tag can be easily stripped | ||||
|                 content = content.replace(/<\/a>/ig, ""); | ||||
|             } | ||||
|  | ||||
|             content = content.replace(/ /g, ' '); | ||||
|         } | ||||
|         return content; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = NoteContentProtectedFulltextExp; | ||||
| @@ -1,75 +0,0 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const Expression = require('./expression'); | ||||
| const NoteSet = require('../note_set'); | ||||
| const becca = require('../../../becca/becca'); | ||||
| const striptags = require('striptags'); | ||||
| const utils = require("../../utils"); | ||||
|  | ||||
| // FIXME: create common subclass with NoteContentProtectedFulltextExp to avoid duplication | ||||
| class NoteContentUnprotectedFulltextExp extends Expression { | ||||
|     constructor(operator, {tokens, raw, flatText}) { | ||||
|         super(); | ||||
|  | ||||
|         if (operator !== '*=*') { | ||||
|             throw new Error(`Note content can be searched only with *=* operator`); | ||||
|         } | ||||
|  | ||||
|         this.tokens = tokens; | ||||
|         this.raw = !!raw; | ||||
|         this.flatText = !!flatText; | ||||
|     } | ||||
|  | ||||
|     execute(inputNoteSet) { | ||||
|         const resultNoteSet = new NoteSet(); | ||||
|  | ||||
|         const sql = require('../../sql'); | ||||
|  | ||||
|         for (let {noteId, type, mime, content} of sql.iterateRows(` | ||||
|                 SELECT noteId, type, mime, content  | ||||
|                 FROM notes JOIN note_contents USING (noteId)  | ||||
|                 WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0 AND isProtected = 0`)) { | ||||
|  | ||||
|             if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             content = this.preprocessContent(content, type, mime); | ||||
|  | ||||
|             const nonMatchingToken = this.tokens.find(token => | ||||
|                 !content.includes(token) && | ||||
|                 ( | ||||
|                     // in case of default fulltext search we should consider both title, attrs and content | ||||
|                     // so e.g. "hello world" should match when "hello" is in title and "world" in content | ||||
|                     !this.flatText | ||||
|                     || !becca.notes[noteId].getFlatText().includes(token) | ||||
|                 ) | ||||
|             ); | ||||
|  | ||||
|             if (!nonMatchingToken) { | ||||
|                 resultNoteSet.add(becca.notes[noteId]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return resultNoteSet; | ||||
|     } | ||||
|  | ||||
|     preprocessContent(content, type, mime) { | ||||
|         content = utils.normalize(content.toString()); | ||||
|  | ||||
|         if (type === 'text' && mime === 'text/html') { | ||||
|             if (!this.raw && content.length < 20000) { // striptags is slow for very large notes | ||||
|                 // allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 | ||||
|                 content = striptags(content, ['a']); | ||||
|  | ||||
|                 // at least the closing tag can be easily stripped | ||||
|                 content = content.replace(/<\/a>/ig, ""); | ||||
|             } | ||||
|  | ||||
|             content = content.replace(/ /g, ' '); | ||||
|         } | ||||
|         return content; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = NoteContentUnprotectedFulltextExp; | ||||
| @@ -1,3 +1,13 @@ | ||||
| const cachedRegexes = {}; | ||||
|  | ||||
| function getRegex(str) { | ||||
|     if (!(str in cachedRegexes)) { | ||||
|         cachedRegexes[str] = new RegExp(str); | ||||
|     } | ||||
|  | ||||
|     return cachedRegexes[str]; | ||||
| } | ||||
|  | ||||
| const stringComparators = { | ||||
|     "=": comparedValue => (val => val === comparedValue), | ||||
|     "!=": comparedValue => (val => val !== comparedValue), | ||||
| @@ -8,6 +18,7 @@ const stringComparators = { | ||||
|     "*=": comparedValue => (val => val && val.endsWith(comparedValue)), | ||||
|     "=*": comparedValue => (val => val && val.startsWith(comparedValue)), | ||||
|     "*=*": comparedValue => (val => val && val.includes(comparedValue)), | ||||
|     "%=": comparedValue => (val => val && !!getRegex(comparedValue).test(val)), | ||||
| }; | ||||
|  | ||||
| const numericComparators = { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ function lex(str) { | ||||
|     let currentWord = ''; | ||||
|  | ||||
|     function isSymbolAnOperator(chr) { | ||||
|         return ['=', '*', '>', '<', '!', "-", "+"].includes(chr); | ||||
|         return ['=', '*', '>', '<', '!', "-", "+", '%'].includes(chr); | ||||
|     } | ||||
|  | ||||
|     function isPreviousSymbolAnOperator() { | ||||
|   | ||||
| @@ -12,8 +12,7 @@ const PropertyComparisonExp = require('../expressions/property_comparison'); | ||||
| const AttributeExistsExp = require('../expressions/attribute_exists'); | ||||
| const LabelComparisonExp = require('../expressions/label_comparison'); | ||||
| const NoteFlatTextExp = require('../expressions/note_flat_text'); | ||||
| const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext'); | ||||
| const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext'); | ||||
| const NoteContentFulltextExp = require('../expressions/note_content_fulltext.js'); | ||||
| const OrderByAndLimitExp = require('../expressions/order_by_and_limit'); | ||||
| const AncestorExp = require("../expressions/ancestor"); | ||||
| const buildComparator = require('./build_comparator'); | ||||
| @@ -32,8 +31,7 @@ function getFulltext(tokens, searchContext) { | ||||
|     if (!searchContext.fastSearch) { | ||||
|         return new OrExp([ | ||||
|             new NoteFlatTextExp(tokens), | ||||
|             new NoteContentProtectedFulltextExp('*=*', {tokens, flatText: true}), | ||||
|             new NoteContentUnprotectedFulltextExp('*=*', {tokens, flatText: true}) | ||||
|             new NoteContentFulltextExp('*=*', {tokens, flatText: true}) | ||||
|         ]); | ||||
|     } | ||||
|     else { | ||||
| @@ -42,7 +40,7 @@ function getFulltext(tokens, searchContext) { | ||||
| } | ||||
|  | ||||
| function isOperator(str) { | ||||
|     return str.match(/^[!=<>*]+$/); | ||||
|     return str.match(/^[!=<>*%]+$/); | ||||
| } | ||||
|  | ||||
| function getExpression(tokens, searchContext, level = 0) { | ||||
| @@ -140,10 +138,7 @@ function getExpression(tokens, searchContext, level = 0) { | ||||
|  | ||||
|             i++; | ||||
|  | ||||
|             return new OrExp([ | ||||
|                 new NoteContentUnprotectedFulltextExp(operator, {tokens: [tokens[i].token], raw }), | ||||
|                 new NoteContentProtectedFulltextExp(operator, {tokens: [tokens[i].token], raw }) | ||||
|             ]); | ||||
|             return new NoteContentFulltextExp(operator, {tokens: [tokens[i].token], raw }); | ||||
|         } | ||||
|  | ||||
|         if (tokens[i].token === 'parents') { | ||||
| @@ -196,8 +191,7 @@ function getExpression(tokens, searchContext, level = 0) { | ||||
|  | ||||
|             return new OrExp([ | ||||
|                 new PropertyComparisonExp(searchContext, 'title', '*=*', tokens[i].token), | ||||
|                 new NoteContentProtectedFulltextExp('*=*', {tokens: [tokens[i].token]}), | ||||
|                 new NoteContentUnprotectedFulltextExp('*=*', {tokens: [tokens[i].token]}) | ||||
|                 new NoteContentFulltextExp('*=*', {tokens: [tokens[i].token]}) | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -168,7 +168,7 @@ const STRING_MIME_TYPES = [ | ||||
|  | ||||
| function isStringNote(type, mime) { | ||||
|     // render and book are string note in the sense that they are expected to contain empty string | ||||
|     return ["text", "code", "relation-map", "search", "render", "book", "mermaid"].includes(type) | ||||
|     return ["text", "code", "relation-map", "search", "render", "book", "mermaid", "canvas"].includes(type) | ||||
|         || mime.startsWith('text/') | ||||
|         || STRING_MIME_TYPES.includes(mime); | ||||
| } | ||||
| @@ -192,7 +192,7 @@ function formatDownloadTitle(filename, type, mime) { | ||||
|  | ||||
|     if (type === 'text') { | ||||
|         return filename + '.html'; | ||||
|     } else if (['relation-map', 'search'].includes(type)) { | ||||
|     } else if (['relation-map', 'canvas', 'search'].includes(type)) { | ||||
|         return filename + '.json'; | ||||
|     } else { | ||||
|         if (!mime) { | ||||
|   | ||||
							
								
								
									
										102
									
								
								src/share/canvas_share.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/share/canvas_share.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| /** | ||||
|  * this is used as a "standalone js" file and required by a shared note directly via script-tags | ||||
|  *  | ||||
|  * data input comes via window variable as follow | ||||
|  * const {elements, appState, files} = window.triliumExcalidraw; | ||||
|  */ | ||||
|  | ||||
| document.getElementById("excalidraw-app").style.height = appState.height+"px"; | ||||
|  | ||||
| const App = () => { | ||||
|     const excalidrawRef = React.useRef(null); | ||||
|     const excalidrawWrapperRef = React.useRef(null); | ||||
|     const [dimensions, setDimensions] = React.useState({ | ||||
|         width: undefined, | ||||
|         height: appState.height, | ||||
|     }); | ||||
|     const [viewModeEnabled, setViewModeEnabled] = React.useState(false); | ||||
|      | ||||
|     // ensure that assets are loaded from trilium | ||||
|      | ||||
|     /** | ||||
|      * resizing | ||||
|      */ | ||||
|     React.useEffect(() => { | ||||
|         const dimensions = { | ||||
|             width: excalidrawWrapperRef.current.getBoundingClientRect().width, | ||||
|             height: excalidrawWrapperRef.current.getBoundingClientRect().height | ||||
|         }; | ||||
|         setDimensions(dimensions); | ||||
|  | ||||
|         const onResize = () => { | ||||
|             const dimensions = { | ||||
|                 width: excalidrawWrapperRef.current.getBoundingClientRect().width, | ||||
|                 height: excalidrawWrapperRef.current.getBoundingClientRect().height | ||||
|             }; | ||||
|             setDimensions(dimensions); | ||||
|         }; | ||||
|          | ||||
|         window.addEventListener("resize", onResize); | ||||
|         // ensure that resize is also called for split creation and deletion | ||||
|         // not really the problem. problem is saved appState! | ||||
|         // self.$renderElement.addEventListener("resize", onResize); | ||||
|          | ||||
|         return () => window.removeEventListener("resize", onResize); | ||||
|     }, [excalidrawWrapperRef]); | ||||
|  | ||||
|     return React.createElement( | ||||
|         React.Fragment, | ||||
|         null, | ||||
|         React.createElement( | ||||
|             "div", | ||||
|             { | ||||
|                 className: "excalidraw-wrapper", | ||||
|                 ref: excalidrawWrapperRef | ||||
|             },         | ||||
|             React.createElement(Excalidraw.default, { | ||||
|                 ref: excalidrawRef, | ||||
|                 width: dimensions.width, | ||||
|                 height: dimensions.height, | ||||
|                 initialData: { | ||||
|                     elements, appState, files | ||||
|                 }, | ||||
|                 viewModeEnabled: !viewModeEnabled, | ||||
|                 zenModeEnabled: false, | ||||
|                 gridModeEnabled: false, | ||||
|                 isCollaborating: false, | ||||
|                 detectScroll: false, | ||||
|                 handleKeyboardGlobally: false, | ||||
|                 autoFocus: true, | ||||
|                 renderFooter: () => { | ||||
|                     return React.createElement( | ||||
|                         React.Fragment, | ||||
|                         null, | ||||
|                         React.createElement( | ||||
|                             "div", | ||||
|                             { | ||||
|                                 className: "excalidraw-top-right-ui excalidraw Island", | ||||
|                             }, | ||||
|                             React.createElement( | ||||
|                                 "label", | ||||
|                                 { | ||||
|                                     style: { | ||||
|                                         padding: "5px", | ||||
|                                     }, | ||||
|                                     className: "excalidraw Stack", | ||||
|                                 }, | ||||
|                                 React.createElement( | ||||
|                                     "button",  | ||||
|                                     { | ||||
|                                         onClick: () => setViewModeEnabled(!viewModeEnabled) | ||||
|                                     },  | ||||
|                                     viewModeEnabled ? " Enter simple view mode " : " Enter extended view mode " | ||||
|                                 ), | ||||
|                                 "" | ||||
|                             ), | ||||
|                         )); | ||||
|                 }, | ||||
|             }) | ||||
|         ) | ||||
|     ); | ||||
| }; | ||||
| ReactDOM.render(React.createElement(App), document.getElementById("excalidraw-app")); | ||||
| @@ -85,6 +85,38 @@ document.addEventListener("DOMContentLoaded", function() { | ||||
|     else if (note.type === 'book') { | ||||
|         isEmpty = true; | ||||
|     } | ||||
|     else if (note.type === 'canvas') { | ||||
|         header += `<script> | ||||
|                     window.EXCALIDRAW_ASSET_PATH = window.location.origin + "/node_modules/@excalidraw/excalidraw/dist/"; | ||||
|                    </script>`; | ||||
|         header += `<script src="../../node_modules/react/umd/react.production.min.js"></script>`; | ||||
|         header += `<script src="../../node_modules/react-dom/umd/react-dom.production.min.js"></script>`; | ||||
|         header += `<script src="../../node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"></script>`; | ||||
|         header += `<style type="text/css"> | ||||
|  | ||||
|             .excalidraw-wrapper { | ||||
|                 height: 100%; | ||||
|             } | ||||
|  | ||||
|             :root[dir="ltr"] | ||||
|             .excalidraw | ||||
|             .layer-ui__wrapper | ||||
|             .zen-mode-transition.App-menu_bottom--transition-left { | ||||
|                 transform: none; | ||||
|             } | ||||
|         </style>`; | ||||
|  | ||||
|         content = `<div> | ||||
|             <script> | ||||
|                 const {elements, appState, files} = JSON.parse(${JSON.stringify(content)}); | ||||
|                 window.triliumExcalidraw = {elements, appState, files} | ||||
|             </script> | ||||
|             <div id="excalidraw-app"></div> | ||||
|             <hr> | ||||
|             <a href="api/images/${note.noteId}/${note.title}?utc=${note.utcDateModified}">Get Image Link</a> | ||||
|             <script src="./canvas_share.js"></script> | ||||
|         </div>`; | ||||
|     } | ||||
|     else { | ||||
|         content = '<p>This note type cannot be displayed.</p>'; | ||||
|     } | ||||
| @@ -99,7 +131,3 @@ document.addEventListener("DOMContentLoaded", function() { | ||||
| module.exports = { | ||||
|     getContent | ||||
| }; | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| const express = require('express'); | ||||
| const path = require('path'); | ||||
|  | ||||
| const shaca = require("./shaca/shaca"); | ||||
| const shacaLoader = require("./shaca/shaca_loader"); | ||||
| const shareRoot = require("./share_root"); | ||||
| @@ -55,6 +58,8 @@ function register(router) { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     router.use('/share/canvas_share.js', express.static(path.join(__dirname, 'canvas_share.js'))); | ||||
|  | ||||
|     router.get(['/share', '/share/'], (req, res, next) => { | ||||
|         shacaLoader.ensureLoad(); | ||||
|  | ||||
| @@ -110,6 +115,7 @@ function register(router) { | ||||
|         res.send(note.getContent()); | ||||
|     }); | ||||
|  | ||||
|     // :filename is not used by trilium, but instead used for "save as" to assign a human readable filename | ||||
|     router.get('/share/api/images/:noteId/:filename', (req, res, next) => { | ||||
|         shacaLoader.ensureLoad(); | ||||
|  | ||||
| @@ -118,15 +124,31 @@ function register(router) { | ||||
|         if (!image) { | ||||
|             return res.status(404).send(`Note '${req.params.noteId}' not found`); | ||||
|         } | ||||
|         else if (image.type !== 'image') { | ||||
|             return res.status(400).send("Requested note is not an image"); | ||||
|         } | ||||
|         else if (!["image", "canvas"].includes(image.type)) { | ||||
|             return res.status(400).send("Requested note is not a shareable image"); | ||||
|         } else if (image.type === "canvas") { | ||||
|             /** | ||||
|              * special "image" type. the canvas is actually type application/json  | ||||
|              * to avoid bitrot and enable usage as referenced image the svg is included. | ||||
|              */ | ||||
|             const content = image.getContent(); | ||||
|             try { | ||||
|                 const data = JSON.parse(content); | ||||
|  | ||||
|                 const svg = data.svg || '<svg />'; | ||||
|                 addNoIndexHeader(image, res); | ||||
|                 res.set('Content-Type', "image/svg+xml"); | ||||
|                 res.set("Cache-Control", "no-cache, no-store, must-revalidate"); | ||||
|                 res.send(svg); | ||||
|             } catch(err) { | ||||
|                 res.status(500).send("there was an error parsing excalidraw to svg"); | ||||
|             } | ||||
|         } else { | ||||
|             // normal image | ||||
|             res.set('Content-Type', image.mime); | ||||
|             addNoIndexHeader(image, res); | ||||
|  | ||||
|         res.setHeader('Content-Type', image.mime); | ||||
|  | ||||
|             res.send(image.getContent()); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // used for PDF viewing | ||||
|   | ||||
| @@ -13,6 +13,7 @@ | ||||
|       <excludeFolder url="file://$MODULE_DIR$/libraries" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/docs" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/bin/better-sqlite3" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/data" /> | ||||
|     </content> | ||||
|     <orderEntry type="inheritedJdk" /> | ||||
|     <orderEntry type="sourceFolder" forTests="false" /> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user