mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 11:56:01 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into feature/in_app_help
This commit is contained in:
		@@ -15,3 +15,9 @@ indent_size = 2
 | 
				
			|||||||
indent_style = space
 | 
					indent_style = space
 | 
				
			||||||
insert_final_newline = true
 | 
					insert_final_newline = true
 | 
				
			||||||
trim_trailing_whitespace = true
 | 
					trim_trailing_whitespace = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*.yml]
 | 
				
			||||||
 | 
					indent_size = 2
 | 
				
			||||||
 | 
					indent_style = space
 | 
				
			||||||
 | 
					insert_final_newline = true
 | 
				
			||||||
 | 
					trim_trailing_whitespace = true
 | 
				
			||||||
							
								
								
									
										39
									
								
								.github/actions/build-electron/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								.github/actions/build-electron/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					inputs:
 | 
				
			||||||
 | 
					  os:
 | 
				
			||||||
 | 
					    description: "One of the supported platforms: macos, linux, windows"
 | 
				
			||||||
 | 
					    required: true
 | 
				
			||||||
 | 
					  arch:
 | 
				
			||||||
 | 
					    description: "The architecture to build for: x64, arm64"
 | 
				
			||||||
 | 
					    required: true
 | 
				
			||||||
 | 
					  extension:
 | 
				
			||||||
 | 
					    description: "Platform specific extensions to copy in the output: dmg, deb, rpm, exe"
 | 
				
			||||||
 | 
					    required: true
 | 
				
			||||||
 | 
					runs:
 | 
				
			||||||
 | 
					  using: composite
 | 
				
			||||||
 | 
					  steps:
 | 
				
			||||||
 | 
					    - name: Set up Python for appdmg to be installed
 | 
				
			||||||
 | 
					      if: ${{ inputs.os == 'macos' }}
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: brew install python-setuptools
 | 
				
			||||||
 | 
					    - name: Install rpm on Ubuntu for RPM package building
 | 
				
			||||||
 | 
					      if: ${{ inputs.os == 'linux' }}
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: sudo apt install rpm
 | 
				
			||||||
 | 
					    - name: Install dependencies
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: npm ci
 | 
				
			||||||
 | 
					    - name: Update build info
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: npm run update-build-info
 | 
				
			||||||
 | 
					    - name: Run electron-forge
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: npm run make-electron -- --arch=${{ inputs.arch }}
 | 
				
			||||||
 | 
					    - name: Prepare artifacts
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: |
 | 
				
			||||||
 | 
					        mkdir -p upload;
 | 
				
			||||||
 | 
					        for ext in ${{ inputs.extension }};
 | 
				
			||||||
 | 
					        do
 | 
				
			||||||
 | 
					          file=$(find out/make -name "*.$ext" -print -quit);
 | 
				
			||||||
 | 
					          cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.$ext";
 | 
				
			||||||
 | 
					        done
 | 
				
			||||||
							
								
								
									
										28
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					inputs:
 | 
				
			||||||
 | 
					  arch:
 | 
				
			||||||
 | 
					    description: "The architecture to build for: x64, arm64"
 | 
				
			||||||
 | 
					    required: true
 | 
				
			||||||
 | 
					runs:
 | 
				
			||||||
 | 
					  using: composite
 | 
				
			||||||
 | 
					  steps:
 | 
				
			||||||
 | 
					    - name: Set up node & dependencies
 | 
				
			||||||
 | 
					      uses: actions/setup-node@v4
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        node-version: 20
 | 
				
			||||||
 | 
					        cache: "npm"
 | 
				
			||||||
 | 
					    - name: Install dependencies
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: npm ci
 | 
				
			||||||
 | 
					    - name: Run Linux server build
 | 
				
			||||||
 | 
					      env:
 | 
				
			||||||
 | 
					        MATRIX_ARCH: ${{ inputs.arch }}
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: |
 | 
				
			||||||
 | 
					        npm run update-build-info
 | 
				
			||||||
 | 
					        ./bin/build-server.sh
 | 
				
			||||||
 | 
					    - name: Prepare artifacts
 | 
				
			||||||
 | 
					      shell: bash
 | 
				
			||||||
 | 
					      run: |
 | 
				
			||||||
 | 
					        mkdir -p upload
 | 
				
			||||||
 | 
					        file=$(find dist -name '*.tar.xz' -print -quit)
 | 
				
			||||||
 | 
					        cp "$file" "upload/TriliumNextNotes-linux-${{ inputs.arch }}-${{ github.ref_name }}.tar.xz"
 | 
				
			||||||
							
								
								
									
										55
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										55
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							@@ -23,10 +23,10 @@ jobs:
 | 
				
			|||||||
        os:
 | 
					        os:
 | 
				
			||||||
          - name: macos
 | 
					          - name: macos
 | 
				
			||||||
            image: macos-latest
 | 
					            image: macos-latest
 | 
				
			||||||
            extension: dmg
 | 
					            extension: [dmg, zip]
 | 
				
			||||||
          - name: linux
 | 
					          - name: linux
 | 
				
			||||||
            image: ubuntu-latest
 | 
					            image: ubuntu-latest
 | 
				
			||||||
            extension: deb
 | 
					            extension: [deb, rpm, zip]
 | 
				
			||||||
          - name: windows
 | 
					          - name: windows
 | 
				
			||||||
            image: windows-latest
 | 
					            image: windows-latest
 | 
				
			||||||
            extension: exe
 | 
					            extension: exe
 | 
				
			||||||
@@ -37,31 +37,12 @@ jobs:
 | 
				
			|||||||
        uses: actions/setup-node@v4
 | 
					        uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 20
 | 
					          node-version: 20
 | 
				
			||||||
      - name: Set up Python for appdmg to be installed
 | 
					      - name: Run the build
 | 
				
			||||||
        if: ${{ matrix.os.name == 'macos' }}
 | 
					        uses: ./.github/actions/build-electron
 | 
				
			||||||
        run: brew install python-setuptools
 | 
					        with:
 | 
				
			||||||
      - name: Install dependencies
 | 
					          os: ${{ matrix.os.name }}
 | 
				
			||||||
        run: npm ci
 | 
					          arch: ${{ matrix.arch }}
 | 
				
			||||||
      - name: Update build info
 | 
					          extension: ${{ matrix.os.extension }}
 | 
				
			||||||
        run: npm run update-build-info      
 | 
					 | 
				
			||||||
      - name: Run electron-forge
 | 
					 | 
				
			||||||
        run: npm run make-electron -- --arch=${{ matrix.arch }}
 | 
					 | 
				
			||||||
      - name: Prepare artifacts (Unix)
 | 
					 | 
				
			||||||
        if: runner.os != 'windows'
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir -p upload
 | 
					 | 
				
			||||||
          file=$(find out/make -name '*.zip' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-${{ github.ref_name }}.zip"
 | 
					 | 
				
			||||||
          file=$(find out/make -name '*.${{ matrix.os.extension }}' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-${{ github.ref_name }}.${{ matrix.os.extension }}"
 | 
					 | 
				
			||||||
      - name: Prepare artifacts (Windows)
 | 
					 | 
				
			||||||
        if: runner.os == 'windows'
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir upload
 | 
					 | 
				
			||||||
          $file = Get-ChildItem -Path out/make -Filter '*.zip' -Recurse | Select-Object -First 1
 | 
					 | 
				
			||||||
          Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-${{ github.ref_name }}.zip"
 | 
					 | 
				
			||||||
          $file = Get-ChildItem -Path out/make -Filter '*.${{ matrix.os.extension }}' -Recurse | Select-Object -First 1
 | 
					 | 
				
			||||||
          Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-${{ github.ref_name }}.${{ matrix.os.extension }}"
 | 
					 | 
				
			||||||
      - name: Publish artifacts
 | 
					      - name: Publish artifacts
 | 
				
			||||||
        uses: actions/upload-artifact@v4
 | 
					        uses: actions/upload-artifact@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
@@ -86,24 +67,10 @@ jobs:
 | 
				
			|||||||
    runs-on: ${{ matrix.runs-on }}
 | 
					    runs-on: ${{ matrix.runs-on }}
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - name: Set up node & dependencies
 | 
					      - name: Run the build
 | 
				
			||||||
        uses: actions/setup-node@v4
 | 
					        uses: ./.github/actions/build-server
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 20
 | 
					          arch: ${{ matrix.arch }}
 | 
				
			||||||
          cache: "npm"
 | 
					 | 
				
			||||||
      - name: Install dependencies
 | 
					 | 
				
			||||||
        run: npm ci
 | 
					 | 
				
			||||||
      - name: Run Linux server build
 | 
					 | 
				
			||||||
        env:
 | 
					 | 
				
			||||||
          MATRIX_ARCH: ${{ matrix.arch }}
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          npm run update-build-info
 | 
					 | 
				
			||||||
          ./bin/build-server.sh
 | 
					 | 
				
			||||||
      - name: Prepare artifacts
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir -p upload
 | 
					 | 
				
			||||||
          file=$(find dist -name '*.tar.xz' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
 | 
					 | 
				
			||||||
      - uses: actions/upload-artifact@v4
 | 
					      - uses: actions/upload-artifact@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: TriliumNextNotes linux server ${{ matrix.arch }}
 | 
					          name: TriliumNextNotes linux server ${{ matrix.arch }}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										107
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										107
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							@@ -20,10 +20,10 @@ jobs:
 | 
				
			|||||||
        os:
 | 
					        os:
 | 
				
			||||||
          - name: macos
 | 
					          - name: macos
 | 
				
			||||||
            image: macos-latest
 | 
					            image: macos-latest
 | 
				
			||||||
            extension: dmg
 | 
					            extension: [dmg, zip]
 | 
				
			||||||
          - name: linux
 | 
					          - name: linux
 | 
				
			||||||
            image: ubuntu-latest
 | 
					            image: ubuntu-latest
 | 
				
			||||||
            extension: deb
 | 
					            extension: [deb, rpm, zip]
 | 
				
			||||||
          - name: windows
 | 
					          - name: windows
 | 
				
			||||||
            image: windows-latest
 | 
					            image: windows-latest
 | 
				
			||||||
            extension: exe
 | 
					            extension: exe
 | 
				
			||||||
@@ -34,62 +34,26 @@ jobs:
 | 
				
			|||||||
        uses: actions/setup-node@v4
 | 
					        uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 20
 | 
					          node-version: 20
 | 
				
			||||||
      - name: Set up Python for appdmg to be installed
 | 
					 | 
				
			||||||
        if: ${{ matrix.os.name == 'macos' }}
 | 
					 | 
				
			||||||
        run: brew install python-setuptools
 | 
					 | 
				
			||||||
      - name: Install dependencies
 | 
					      - name: Install dependencies
 | 
				
			||||||
 | 
					        shell: bash
 | 
				
			||||||
        run: npm ci
 | 
					        run: npm ci
 | 
				
			||||||
      - name: Update build info
 | 
					 | 
				
			||||||
        run: npm run update-build-info
 | 
					 | 
				
			||||||
      - name: Update nightly version
 | 
					      - name: Update nightly version
 | 
				
			||||||
        run: npm run ci-update-nightly-version
 | 
					        run: npm run ci-update-nightly-version
 | 
				
			||||||
      - name: Run electron-forge
 | 
					      - name: Run the build
 | 
				
			||||||
        run: npm run make-electron -- --arch=${{ matrix.arch }}
 | 
					        uses: ./.github/actions/build-electron
 | 
				
			||||||
      - name: Prepare artifacts (Unix)
 | 
					 | 
				
			||||||
        if: runner.os != 'windows'
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir -p upload
 | 
					 | 
				
			||||||
          file=$(find out/make -name '*.zip' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.zip"
 | 
					 | 
				
			||||||
          file=$(find out/make -name '*.${{ matrix.os.extension }}' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }}"
 | 
					 | 
				
			||||||
      - name: Prepare artifacts (Windows)
 | 
					 | 
				
			||||||
        if: runner.os == 'windows'
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir upload
 | 
					 | 
				
			||||||
          $file = Get-ChildItem -Path out/make -Filter '*.zip' -Recurse | Select-Object -First 1
 | 
					 | 
				
			||||||
          Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.zip"
 | 
					 | 
				
			||||||
          $file = Get-ChildItem -Path out/make -Filter '*.${{ matrix.os.extension }}' -Recurse | Select-Object -First 1
 | 
					 | 
				
			||||||
          Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }}"
 | 
					 | 
				
			||||||
      - name: Publish artifacts
 | 
					 | 
				
			||||||
        uses: actions/upload-artifact@v4
 | 
					 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}
 | 
					          os: ${{ matrix.os.name }}
 | 
				
			||||||
          path: upload/*.zip
 | 
					          arch: ${{ matrix.arch }}
 | 
				
			||||||
          overwrite: true
 | 
					          extension: ${{ join(matrix.os.extension, ' ') }}
 | 
				
			||||||
      - name: Publish installer artifacts
 | 
					 | 
				
			||||||
        uses: actions/upload-artifact@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}
 | 
					 | 
				
			||||||
          path: upload/*.${{ matrix.os.extension }}
 | 
					 | 
				
			||||||
          overwrite: true
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Deploy release
 | 
					      - name: Publish release
 | 
				
			||||||
        uses: WebFreak001/deploy-nightly@v3.2.0
 | 
					        uses: softprops/action-gh-release@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          upload_url: ${{ env.GITHUB_UPLOAD_URL }}
 | 
					          draft: true
 | 
				
			||||||
          release_id: ${{ env.GITHUB_RELEASE_ID }}
 | 
					          fail_on_unmatched_files: true
 | 
				
			||||||
          asset_path: upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.zip # path to archive to upload
 | 
					          files: upload/*.*
 | 
				
			||||||
          asset_name: TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-nightly.zip # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash
 | 
					          tag_name: nightly
 | 
				
			||||||
          asset_content_type: application/zip # required by GitHub API
 | 
					          name: Nightly Build
 | 
				
			||||||
      - name: Deploy installer release
 | 
					 | 
				
			||||||
        uses: WebFreak001/deploy-nightly@v3.2.0
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          upload_url: ${{ env.GITHUB_UPLOAD_URL }}
 | 
					 | 
				
			||||||
          release_id: ${{ env.GITHUB_RELEASE_ID }}
 | 
					 | 
				
			||||||
          asset_path: upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }} # path to archive to upload
 | 
					 | 
				
			||||||
          asset_name: TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-nightly.${{ matrix.os.extension }} # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash
 | 
					 | 
				
			||||||
          asset_content_type: application/zip # required by GitHub API
 | 
					 | 
				
			||||||
  nightly-server:
 | 
					  nightly-server:
 | 
				
			||||||
    name: Deploy server nightly
 | 
					    name: Deploy server nightly
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
@@ -104,34 +68,17 @@ jobs:
 | 
				
			|||||||
    runs-on: ${{ matrix.runs-on }}
 | 
					    runs-on: ${{ matrix.runs-on }}
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - name: Set up node & dependencies
 | 
					 | 
				
			||||||
        uses: actions/setup-node@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          node-version: 20
 | 
					 | 
				
			||||||
          cache: "npm"
 | 
					 | 
				
			||||||
      - name: Install dependencies
 | 
					 | 
				
			||||||
        run: npm ci
 | 
					 | 
				
			||||||
      - name: Run Linux server build
 | 
					 | 
				
			||||||
        env:
 | 
					 | 
				
			||||||
          MATRIX_ARCH: ${{ matrix.arch }}
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          npm run update-build-info
 | 
					 | 
				
			||||||
          ./bin/build-server.sh
 | 
					 | 
				
			||||||
      - name: Prepare artifacts
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir -p upload
 | 
					 | 
				
			||||||
          file=$(find dist -name '*.tar.xz' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
 | 
					 | 
				
			||||||
      - uses: actions/upload-artifact@v4
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          name: TriliumNextNotes linux server ${{ matrix.arch }}
 | 
					 | 
				
			||||||
          path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Deploy release
 | 
					      - name: Run the build
 | 
				
			||||||
        uses: WebFreak001/deploy-nightly@v3.2.0
 | 
					        uses: ./.github/actions/build-server
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          upload_url: ${{ env.GITHUB_UPLOAD_URL }}
 | 
					          arch: ${{ matrix.arch }}
 | 
				
			||||||
          release_id: ${{ env.GITHUB_RELEASE_ID }}
 | 
					
 | 
				
			||||||
          asset_path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz # path to archive to upload
 | 
					      - name: Publish release
 | 
				
			||||||
          asset_name: TriliumNextNotes-linux-x64-nightly.zip # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash
 | 
					        uses: softprops/action-gh-release@v2
 | 
				
			||||||
          asset_content_type: application/zip # required by GitHub API
 | 
					        with:
 | 
				
			||||||
 | 
					          draft: true
 | 
				
			||||||
 | 
					          fail_on_unmatched_files: true
 | 
				
			||||||
 | 
					          files: upload/*.*
 | 
				
			||||||
 | 
					          tag_name: nightly
 | 
				
			||||||
 | 
					          name: Nightly Build
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										57
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										57
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -20,10 +20,10 @@ jobs:
 | 
				
			|||||||
        os:
 | 
					        os:
 | 
				
			||||||
          - name: macos
 | 
					          - name: macos
 | 
				
			||||||
            image: macos-latest
 | 
					            image: macos-latest
 | 
				
			||||||
            extension: dmg
 | 
					            extension: [dmg, zip]
 | 
				
			||||||
          - name: linux
 | 
					          - name: linux
 | 
				
			||||||
            image: ubuntu-latest
 | 
					            image: ubuntu-latest
 | 
				
			||||||
            extension: deb
 | 
					            extension: [deb, rpm, zip]
 | 
				
			||||||
          - name: windows
 | 
					          - name: windows
 | 
				
			||||||
            image: windows-latest
 | 
					            image: windows-latest
 | 
				
			||||||
            extension: exe
 | 
					            extension: exe
 | 
				
			||||||
@@ -34,31 +34,12 @@ jobs:
 | 
				
			|||||||
        uses: actions/setup-node@v4
 | 
					        uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 20
 | 
					          node-version: 20
 | 
				
			||||||
      - name: Set up Python for appdmg to be installed
 | 
					      - name: Run the build
 | 
				
			||||||
        if: ${{ matrix.os.name == 'macos' }}
 | 
					        uses: ./.github/actions/build-electron
 | 
				
			||||||
        run: brew install python-setuptools
 | 
					        with:
 | 
				
			||||||
      - name: Install dependencies
 | 
					          os: ${{ matrix.os.name }}
 | 
				
			||||||
        run: npm ci
 | 
					          arch: ${{ matrix.arch }}
 | 
				
			||||||
      - name: Update build info
 | 
					          extension: ${{ join(matrix.os.extension, ' ') }}
 | 
				
			||||||
        run: npm run update-build-info
 | 
					 | 
				
			||||||
      - name: Run electron-forge
 | 
					 | 
				
			||||||
        run: npm run make-electron -- --arch=${{ matrix.arch }}
 | 
					 | 
				
			||||||
      - name: Prepare artifacts (Unix)
 | 
					 | 
				
			||||||
        if: runner.os != 'windows'
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir -p upload
 | 
					 | 
				
			||||||
          file=$(find out/make -name '*.zip' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ matrix.os.name }}-${{ matrix.arch }}.zip"
 | 
					 | 
				
			||||||
          file=$(find out/make -name '*.${{ matrix.os.extension }}' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }}"
 | 
					 | 
				
			||||||
      - name: Prepare artifacts (Windows)
 | 
					 | 
				
			||||||
        if: runner.os == 'windows'
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir upload
 | 
					 | 
				
			||||||
          $file = Get-ChildItem -Path out/make -Filter '*.zip' -Recurse | Select-Object -First 1
 | 
					 | 
				
			||||||
          Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ github.ref_name }}-${{ matrix.os.name }}-${{ matrix.arch }}.zip"
 | 
					 | 
				
			||||||
          $file = Get-ChildItem -Path out/make -Filter '*.${{ matrix.os.extension }}' -Recurse | Select-Object -First 1
 | 
					 | 
				
			||||||
          Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ github.ref_name }}-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }}"      
 | 
					 | 
				
			||||||
      - name: Publish release
 | 
					      - name: Publish release
 | 
				
			||||||
        uses: softprops/action-gh-release@v2
 | 
					        uses: softprops/action-gh-release@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
@@ -79,24 +60,12 @@ jobs:
 | 
				
			|||||||
    runs-on: ${{ matrix.runs-on }}
 | 
					    runs-on: ${{ matrix.runs-on }}
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - name: Set up node & dependencies
 | 
					
 | 
				
			||||||
        uses: actions/setup-node@v4
 | 
					      - name: Run the build
 | 
				
			||||||
 | 
					        uses: ./.github/actions/build-server
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 20
 | 
					          arch: ${{ matrix.arch }}
 | 
				
			||||||
          cache: "npm"
 | 
					
 | 
				
			||||||
      - name: Install dependencies
 | 
					 | 
				
			||||||
        run: npm ci
 | 
					 | 
				
			||||||
      - name: Run Linux server build
 | 
					 | 
				
			||||||
        env:
 | 
					 | 
				
			||||||
          MATRIX_ARCH: ${{ matrix.arch }}
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          npm run update-build-info
 | 
					 | 
				
			||||||
          ./bin/build-server.sh
 | 
					 | 
				
			||||||
      - name: Prepare artifacts
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          mkdir -p upload
 | 
					 | 
				
			||||||
          file=$(find dist -name '*.tar.xz' -print -quit)
 | 
					 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
 | 
					 | 
				
			||||||
      - name: Publish release
 | 
					      - name: Publish release
 | 
				
			||||||
        uses: softprops/action-gh-release@v2
 | 
					        uses: softprops/action-gh-release@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,17 @@
 | 
				
			|||||||
[Desktop Entry]
 | 
					[Desktop Entry]
 | 
				
			||||||
<% if (productName) { %>Name=<%= productName %>
 | 
					<%=
 | 
				
			||||||
<% } %><% if (description) { %>Comment=<%= description %>
 | 
					Object.entries({
 | 
				
			||||||
<% } %><% if (genericName) { %>GenericName=<%= genericName %>
 | 
					    "Name": productName,
 | 
				
			||||||
<% } %><% if (name) { %>Exec=<%= name %> %U
 | 
					    "Comment": description,
 | 
				
			||||||
Icon=<%= name %>
 | 
					    "GenericName": genericName,
 | 
				
			||||||
<% } %>Type=Application
 | 
					    "Exec": name ? `${name} %U` : undefined,
 | 
				
			||||||
StartupNotify=true
 | 
					    "Icon": name,
 | 
				
			||||||
<% if (productName) { %>StartupWMClass=<%= productName %>
 | 
					    "Type": "Application",
 | 
				
			||||||
<% } if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;
 | 
					    "StartupNotify": "true",
 | 
				
			||||||
<% } %><% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;
 | 
					    "StartupWMClass": productName,
 | 
				
			||||||
<% } %>
 | 
					    "Categories": categories?.length ? `${categories.join(";")};` : undefined,
 | 
				
			||||||
 | 
					    "MimeType": mimeType?.length ? `${mimeType.join(";")};` : undefined
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					.map(line => line[1] ? line.join("=") : undefined)
 | 
				
			||||||
 | 
					.filter(line => !!line)
 | 
				
			||||||
 | 
					.join("\n")%>
 | 
				
			||||||
@@ -26,6 +26,8 @@ electronDl({ saveAs: true });
 | 
				
			|||||||
// needed for excalidraw export https://github.com/zadam/trilium/issues/4271
 | 
					// needed for excalidraw export https://github.com/zadam/trilium/issues/4271
 | 
				
			||||||
electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features");
 | 
					electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					electron.app.userAgentFallback = `${electron.app.getName()} ${electron.app.getVersion()}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Quit when all windows are closed, except on macOS. There, it's common
 | 
					// Quit when all windows are closed, except on macOS. There, it's common
 | 
				
			||||||
// for applications and their menu bar to stay active until the user quits
 | 
					// for applications and their menu bar to stay active until the user quits
 | 
				
			||||||
// explicitly with Cmd + Q.
 | 
					// explicitly with Cmd + Q.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,12 @@ const fs = require("fs-extra");
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const APP_NAME = "TriliumNext Notes";
 | 
					const APP_NAME = "TriliumNext Notes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const extraResourcesForPlatform = getExtraResourcesForPlatform();
 | 
				
			||||||
 | 
					const baseLinuxMakerConfigOptions = {
 | 
				
			||||||
 | 
					  icon: "./images/app-icons/png/128x128.png",
 | 
				
			||||||
 | 
					  desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs"),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    packagerConfig: {
 | 
					    packagerConfig: {
 | 
				
			||||||
        executableName: "trilium",
 | 
					        executableName: "trilium",
 | 
				
			||||||
@@ -12,7 +18,7 @@ module.exports = {
 | 
				
			|||||||
        icon: "./images/app-icons/icon",
 | 
					        icon: "./images/app-icons/icon",
 | 
				
			||||||
        extraResource: [
 | 
					        extraResource: [
 | 
				
			||||||
            // Moved to root
 | 
					            // Moved to root
 | 
				
			||||||
            ...getExtraResourcesForPlatform(),
 | 
					            ...extraResourcesForPlatform,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
 | 
					            // Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
 | 
				
			||||||
            "translations/",
 | 
					            "translations/",
 | 
				
			||||||
@@ -20,22 +26,18 @@ module.exports = {
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
        afterComplete: [
 | 
					        afterComplete: [
 | 
				
			||||||
            (buildPath, _electronVersion, platform, _arch, callback) => {
 | 
					            (buildPath, _electronVersion, platform, _arch, callback) => {
 | 
				
			||||||
                const extraResources = getExtraResourcesForPlatform();
 | 
					                for (const resource of extraResourcesForPlatform) {
 | 
				
			||||||
                for (const resource of extraResources) {
 | 
					 | 
				
			||||||
                    const baseName = path.basename(resource);
 | 
					                    const baseName = path.basename(resource);
 | 
				
			||||||
                    let sourcePath;
 | 
					 | 
				
			||||||
                    if (platform === "darwin") {
 | 
					 | 
				
			||||||
                        sourcePath = path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName);
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        sourcePath = path.join(buildPath, "resources", baseName);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    let destPath;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (baseName !== "256x256.png") {
 | 
					                    // prettier-ignore
 | 
				
			||||||
                        destPath = path.join(buildPath, baseName);
 | 
					                    const sourcePath = (platform === "darwin")
 | 
				
			||||||
                    } else {
 | 
					                        ? path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName)
 | 
				
			||||||
                        destPath = path.join(buildPath, "icon.png");
 | 
					                        : path.join(buildPath, "resources", baseName);
 | 
				
			||||||
                    }
 | 
					
 | 
				
			||||||
 | 
					                    // prettier-ignore
 | 
				
			||||||
 | 
					                    const destPath = (baseName !== "256x256.png")
 | 
				
			||||||
 | 
					                        ? path.join(buildPath, baseName)
 | 
				
			||||||
 | 
					                        : path.join(buildPath, "icon.png");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // Copy files from resources folder to root
 | 
					                    // Copy files from resources folder to root
 | 
				
			||||||
                    fs.move(sourcePath, destPath)
 | 
					                    fs.move(sourcePath, destPath)
 | 
				
			||||||
@@ -53,8 +55,15 @@ module.exports = {
 | 
				
			|||||||
            name: "@electron-forge/maker-deb",
 | 
					            name: "@electron-forge/maker-deb",
 | 
				
			||||||
            config: {
 | 
					            config: {
 | 
				
			||||||
                options: {
 | 
					                options: {
 | 
				
			||||||
                    icon: "./images/app-icons/png/128x128.png",
 | 
					                  ...baseLinuxMakerConfigOptions
 | 
				
			||||||
                    desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs")
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: "@electron-forge/maker-rpm",
 | 
				
			||||||
 | 
					            config: {
 | 
				
			||||||
 | 
					                options: {
 | 
				
			||||||
 | 
					                  ...baseLinuxMakerConfigOptions
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@@ -91,21 +100,20 @@ module.exports = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getExtraResourcesForPlatform() {
 | 
					function getExtraResourcesForPlatform() {
 | 
				
			||||||
    let resources = ["dump-db/", "./bin/tpl/anonymize-database.sql"];
 | 
					    const resources = ["dump-db/", "./bin/tpl/anonymize-database.sql"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getScriptRessources = () => {
 | 
				
			||||||
        const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"];
 | 
					        const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"];
 | 
				
			||||||
 | 
					        const scriptExt = (process.platform === "win32") ? "bat" : "sh";
 | 
				
			||||||
 | 
					        return scripts.map(script => `./bin/tpl/${script}.${scriptExt}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch (process.platform) {
 | 
					    switch (process.platform) {
 | 
				
			||||||
        case "win32":
 | 
					        case "win32":
 | 
				
			||||||
            for (const script of scripts) {
 | 
					            resources.push(...getScriptRessources())
 | 
				
			||||||
                resources.push(`./bin/tpl/${script}.bat`);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
        case "darwin":
 | 
					 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
        case "linux":
 | 
					        case "linux":
 | 
				
			||||||
            resources.push("images/app-icons/png/256x256.png");
 | 
					            resources.push(...getScriptRessources(), "images/app-icons/png/256x256.png");
 | 
				
			||||||
            for (const script of scripts) {
 | 
					 | 
				
			||||||
                resources.push(`./bin/tpl/${script}.sh`);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
        default:
 | 
					        default:
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										374
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										374
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "trilium",
 | 
					  "name": "trilium",
 | 
				
			||||||
  "version": "0.91.5",
 | 
					  "version": "0.91.6",
 | 
				
			||||||
  "lockfileVersion": 3,
 | 
					  "lockfileVersion": 3,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "packages": {
 | 
					  "packages": {
 | 
				
			||||||
    "": {
 | 
					    "": {
 | 
				
			||||||
      "name": "trilium",
 | 
					      "name": "trilium",
 | 
				
			||||||
      "version": "0.91.5",
 | 
					      "version": "0.91.6",
 | 
				
			||||||
      "license": "AGPL-3.0-only",
 | 
					      "license": "AGPL-3.0-only",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@braintree/sanitize-url": "7.1.1",
 | 
					        "@braintree/sanitize-url": "7.1.1",
 | 
				
			||||||
@@ -86,7 +86,6 @@
 | 
				
			|||||||
        "sanitize-filename": "1.6.3",
 | 
					        "sanitize-filename": "1.6.3",
 | 
				
			||||||
        "sanitize-html": "2.14.0",
 | 
					        "sanitize-html": "2.14.0",
 | 
				
			||||||
        "sax": "1.4.1",
 | 
					        "sax": "1.4.1",
 | 
				
			||||||
        "semver": "7.7.0",
 | 
					 | 
				
			||||||
        "serve-favicon": "2.5.0",
 | 
					        "serve-favicon": "2.5.0",
 | 
				
			||||||
        "session-file-store": "1.5.0",
 | 
					        "session-file-store": "1.5.0",
 | 
				
			||||||
        "source-map-support": "0.5.21",
 | 
					        "source-map-support": "0.5.21",
 | 
				
			||||||
@@ -109,11 +108,12 @@
 | 
				
			|||||||
        "@electron-forge/cli": "7.6.1",
 | 
					        "@electron-forge/cli": "7.6.1",
 | 
				
			||||||
        "@electron-forge/maker-deb": "7.6.1",
 | 
					        "@electron-forge/maker-deb": "7.6.1",
 | 
				
			||||||
        "@electron-forge/maker-dmg": "7.6.1",
 | 
					        "@electron-forge/maker-dmg": "7.6.1",
 | 
				
			||||||
 | 
					        "@electron-forge/maker-rpm": "7.6.1",
 | 
				
			||||||
        "@electron-forge/maker-squirrel": "7.6.1",
 | 
					        "@electron-forge/maker-squirrel": "7.6.1",
 | 
				
			||||||
        "@electron-forge/maker-zip": "7.6.1",
 | 
					        "@electron-forge/maker-zip": "7.6.1",
 | 
				
			||||||
        "@electron-forge/plugin-auto-unpack-natives": "7.6.1",
 | 
					        "@electron-forge/plugin-auto-unpack-natives": "7.6.1",
 | 
				
			||||||
        "@electron/rebuild": "3.7.1",
 | 
					        "@electron/rebuild": "3.7.1",
 | 
				
			||||||
        "@playwright/test": "1.50.0",
 | 
					        "@playwright/test": "1.50.1",
 | 
				
			||||||
        "@types/archiver": "6.0.3",
 | 
					        "@types/archiver": "6.0.3",
 | 
				
			||||||
        "@types/better-sqlite3": "7.6.12",
 | 
					        "@types/better-sqlite3": "7.6.12",
 | 
				
			||||||
        "@types/bootstrap": "5.2.10",
 | 
					        "@types/bootstrap": "5.2.10",
 | 
				
			||||||
@@ -136,12 +136,11 @@
 | 
				
			|||||||
        "@types/leaflet-gpx": "1.3.7",
 | 
					        "@types/leaflet-gpx": "1.3.7",
 | 
				
			||||||
        "@types/mime-types": "2.1.4",
 | 
					        "@types/mime-types": "2.1.4",
 | 
				
			||||||
        "@types/multer": "1.4.12",
 | 
					        "@types/multer": "1.4.12",
 | 
				
			||||||
        "@types/node": "22.12.0",
 | 
					        "@types/node": "22.13.1",
 | 
				
			||||||
        "@types/react": "18.3.18",
 | 
					        "@types/react": "18.3.18",
 | 
				
			||||||
        "@types/safe-compare": "1.1.2",
 | 
					        "@types/safe-compare": "1.1.2",
 | 
				
			||||||
        "@types/sanitize-html": "2.13.0",
 | 
					        "@types/sanitize-html": "2.13.0",
 | 
				
			||||||
        "@types/sax": "1.2.7",
 | 
					        "@types/sax": "1.2.7",
 | 
				
			||||||
        "@types/semver": "7.5.8",
 | 
					 | 
				
			||||||
        "@types/serve-favicon": "2.5.7",
 | 
					        "@types/serve-favicon": "2.5.7",
 | 
				
			||||||
        "@types/session-file-store": "1.2.5",
 | 
					        "@types/session-file-store": "1.2.5",
 | 
				
			||||||
        "@types/source-map-support": "0.5.10",
 | 
					        "@types/source-map-support": "0.5.10",
 | 
				
			||||||
@@ -151,7 +150,7 @@
 | 
				
			|||||||
        "@types/ws": "8.5.14",
 | 
					        "@types/ws": "8.5.14",
 | 
				
			||||||
        "@types/xml2js": "0.4.14",
 | 
					        "@types/xml2js": "0.4.14",
 | 
				
			||||||
        "@types/yargs": "17.0.33",
 | 
					        "@types/yargs": "17.0.33",
 | 
				
			||||||
        "@vitest/coverage-v8": "3.0.4",
 | 
					        "@vitest/coverage-v8": "3.0.5",
 | 
				
			||||||
        "cross-env": "7.0.3",
 | 
					        "cross-env": "7.0.3",
 | 
				
			||||||
        "electron": "34.0.2",
 | 
					        "electron": "34.0.2",
 | 
				
			||||||
        "esm": "3.2.25",
 | 
					        "esm": "3.2.25",
 | 
				
			||||||
@@ -166,7 +165,7 @@
 | 
				
			|||||||
        "tsx": "4.19.2",
 | 
					        "tsx": "4.19.2",
 | 
				
			||||||
        "typedoc": "0.27.6",
 | 
					        "typedoc": "0.27.6",
 | 
				
			||||||
        "typescript": "5.7.3",
 | 
					        "typescript": "5.7.3",
 | 
				
			||||||
        "vitest": "3.0.4",
 | 
					        "vitest": "3.0.5",
 | 
				
			||||||
        "webpack": "5.97.1",
 | 
					        "webpack": "5.97.1",
 | 
				
			||||||
        "webpack-cli": "6.0.1",
 | 
					        "webpack-cli": "6.0.1",
 | 
				
			||||||
        "webpack-dev-middleware": "7.4.2"
 | 
					        "webpack-dev-middleware": "7.4.2"
 | 
				
			||||||
@@ -782,6 +781,23 @@
 | 
				
			|||||||
        "graceful-fs": "^4.1.6"
 | 
					        "graceful-fs": "^4.1.6"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@electron-forge/maker-rpm": {
 | 
				
			||||||
 | 
					      "version": "7.6.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@electron-forge/maker-rpm/-/maker-rpm-7.6.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-BShfmto+XTSA01pkZp10r2ktyruVfI24sGC+y4az1vbqkmX2qN9j0Xr+G/ZMHsm76XHju0N/e1Y2pqqu2JM8/A==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@electron-forge/maker-base": "7.6.1",
 | 
				
			||||||
 | 
					        "@electron-forge/shared-types": "7.6.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 16.4.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "optionalDependencies": {
 | 
				
			||||||
 | 
					        "electron-installer-redhat": "^3.2.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@electron-forge/maker-squirrel": {
 | 
					    "node_modules/@electron-forge/maker-squirrel": {
 | 
				
			||||||
      "version": "7.6.1",
 | 
					      "version": "7.6.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@electron-forge/maker-squirrel/-/maker-squirrel-7.6.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@electron-forge/maker-squirrel/-/maker-squirrel-7.6.1.tgz",
 | 
				
			||||||
@@ -2880,13 +2896,13 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@playwright/test": {
 | 
					    "node_modules/@playwright/test": {
 | 
				
			||||||
      "version": "1.50.0",
 | 
					      "version": "1.50.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==",
 | 
					      "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "Apache-2.0",
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "playwright": "1.50.0"
 | 
					        "playwright": "1.50.1"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "playwright": "cli.js"
 | 
					        "playwright": "cli.js"
 | 
				
			||||||
@@ -3909,9 +3925,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/node": {
 | 
					    "node_modules/@types/node": {
 | 
				
			||||||
      "version": "22.12.0",
 | 
					      "version": "22.13.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
 | 
					      "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "undici-types": "~6.20.0"
 | 
					        "undici-types": "~6.20.0"
 | 
				
			||||||
@@ -4002,13 +4018,6 @@
 | 
				
			|||||||
        "@types/node": "*"
 | 
					        "@types/node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/semver": {
 | 
					 | 
				
			||||||
      "version": "7.5.8",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
 | 
					 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "license": "MIT"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@types/send": {
 | 
					    "node_modules/@types/send": {
 | 
				
			||||||
      "version": "0.17.4",
 | 
					      "version": "0.17.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
 | 
				
			||||||
@@ -4163,9 +4172,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@vitest/coverage-v8": {
 | 
					    "node_modules/@vitest/coverage-v8": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==",
 | 
					      "integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
@@ -4186,8 +4195,8 @@
 | 
				
			|||||||
        "url": "https://opencollective.com/vitest"
 | 
					        "url": "https://opencollective.com/vitest"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "peerDependencies": {
 | 
					      "peerDependencies": {
 | 
				
			||||||
        "@vitest/browser": "3.0.4",
 | 
					        "@vitest/browser": "3.0.5",
 | 
				
			||||||
        "vitest": "3.0.4"
 | 
					        "vitest": "3.0.5"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "peerDependenciesMeta": {
 | 
					      "peerDependenciesMeta": {
 | 
				
			||||||
        "@vitest/browser": {
 | 
					        "@vitest/browser": {
 | 
				
			||||||
@@ -4196,14 +4205,14 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@vitest/expect": {
 | 
					    "node_modules/@vitest/expect": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==",
 | 
					      "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@vitest/spy": "3.0.4",
 | 
					        "@vitest/spy": "3.0.5",
 | 
				
			||||||
        "@vitest/utils": "3.0.4",
 | 
					        "@vitest/utils": "3.0.5",
 | 
				
			||||||
        "chai": "^5.1.2",
 | 
					        "chai": "^5.1.2",
 | 
				
			||||||
        "tinyrainbow": "^2.0.0"
 | 
					        "tinyrainbow": "^2.0.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@@ -4212,13 +4221,13 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@vitest/mocker": {
 | 
					    "node_modules/@vitest/mocker": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==",
 | 
					      "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@vitest/spy": "3.0.4",
 | 
					        "@vitest/spy": "3.0.5",
 | 
				
			||||||
        "estree-walker": "^3.0.3",
 | 
					        "estree-walker": "^3.0.3",
 | 
				
			||||||
        "magic-string": "^0.30.17"
 | 
					        "magic-string": "^0.30.17"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@@ -4239,9 +4248,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@vitest/pretty-format": {
 | 
					    "node_modules/@vitest/pretty-format": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==",
 | 
					      "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
@@ -4252,13 +4261,13 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@vitest/runner": {
 | 
					    "node_modules/@vitest/runner": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==",
 | 
					      "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@vitest/utils": "3.0.4",
 | 
					        "@vitest/utils": "3.0.5",
 | 
				
			||||||
        "pathe": "^2.0.2"
 | 
					        "pathe": "^2.0.2"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "funding": {
 | 
					      "funding": {
 | 
				
			||||||
@@ -4273,13 +4282,13 @@
 | 
				
			|||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@vitest/snapshot": {
 | 
					    "node_modules/@vitest/snapshot": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==",
 | 
					      "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@vitest/pretty-format": "3.0.4",
 | 
					        "@vitest/pretty-format": "3.0.5",
 | 
				
			||||||
        "magic-string": "^0.30.17",
 | 
					        "magic-string": "^0.30.17",
 | 
				
			||||||
        "pathe": "^2.0.2"
 | 
					        "pathe": "^2.0.2"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@@ -4295,9 +4304,9 @@
 | 
				
			|||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@vitest/spy": {
 | 
					    "node_modules/@vitest/spy": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==",
 | 
					      "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
@@ -4308,13 +4317,13 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@vitest/utils": {
 | 
					    "node_modules/@vitest/utils": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==",
 | 
					      "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@vitest/pretty-format": "3.0.4",
 | 
					        "@vitest/pretty-format": "3.0.5",
 | 
				
			||||||
        "loupe": "^3.1.2",
 | 
					        "loupe": "^3.1.2",
 | 
				
			||||||
        "tinyrainbow": "^2.0.0"
 | 
					        "tinyrainbow": "^2.0.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@@ -8029,6 +8038,211 @@
 | 
				
			|||||||
        "appdmg": "^0.6.4"
 | 
					        "appdmg": "^0.6.4"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat": {
 | 
				
			||||||
 | 
					      "version": "3.4.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/electron-installer-redhat/-/electron-installer-redhat-3.4.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "darwin",
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@malept/cross-spawn-promise": "^1.0.0",
 | 
				
			||||||
 | 
					        "debug": "^4.1.1",
 | 
				
			||||||
 | 
					        "electron-installer-common": "^0.10.2",
 | 
				
			||||||
 | 
					        "fs-extra": "^9.0.0",
 | 
				
			||||||
 | 
					        "lodash": "^4.17.15",
 | 
				
			||||||
 | 
					        "word-wrap": "^1.2.3",
 | 
				
			||||||
 | 
					        "yargs": "^16.0.2"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "electron-installer-redhat": "src/cli.js"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/@malept/cross-spawn-promise": {
 | 
				
			||||||
 | 
					      "version": "1.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "funding": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "individual",
 | 
				
			||||||
 | 
					          "url": "https://github.com/sponsors/malept"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "tidelift",
 | 
				
			||||||
 | 
					          "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "cross-spawn": "^7.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/ansi-regex": {
 | 
				
			||||||
 | 
					      "version": "5.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/cliui": {
 | 
				
			||||||
 | 
					      "version": "7.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "ISC",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "string-width": "^4.2.0",
 | 
				
			||||||
 | 
					        "strip-ansi": "^6.0.0",
 | 
				
			||||||
 | 
					        "wrap-ansi": "^7.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/emoji-regex": {
 | 
				
			||||||
 | 
					      "version": "8.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/fs-extra": {
 | 
				
			||||||
 | 
					      "version": "9.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "at-least-node": "^1.0.0",
 | 
				
			||||||
 | 
					        "graceful-fs": "^4.2.0",
 | 
				
			||||||
 | 
					        "jsonfile": "^6.0.1",
 | 
				
			||||||
 | 
					        "universalify": "^2.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=10"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/is-fullwidth-code-point": {
 | 
				
			||||||
 | 
					      "version": "3.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/jsonfile": {
 | 
				
			||||||
 | 
					      "version": "6.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "universalify": "^2.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "optionalDependencies": {
 | 
				
			||||||
 | 
					        "graceful-fs": "^4.1.6"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/string-width": {
 | 
				
			||||||
 | 
					      "version": "4.2.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "emoji-regex": "^8.0.0",
 | 
				
			||||||
 | 
					        "is-fullwidth-code-point": "^3.0.0",
 | 
				
			||||||
 | 
					        "strip-ansi": "^6.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/strip-ansi": {
 | 
				
			||||||
 | 
					      "version": "6.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "ansi-regex": "^5.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/wrap-ansi": {
 | 
				
			||||||
 | 
					      "version": "7.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "ansi-styles": "^4.0.0",
 | 
				
			||||||
 | 
					        "string-width": "^4.1.0",
 | 
				
			||||||
 | 
					        "strip-ansi": "^6.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=10"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/yargs": {
 | 
				
			||||||
 | 
					      "version": "16.2.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "cliui": "^7.0.2",
 | 
				
			||||||
 | 
					        "escalade": "^3.1.1",
 | 
				
			||||||
 | 
					        "get-caller-file": "^2.0.5",
 | 
				
			||||||
 | 
					        "require-directory": "^2.1.1",
 | 
				
			||||||
 | 
					        "string-width": "^4.2.0",
 | 
				
			||||||
 | 
					        "y18n": "^5.0.5",
 | 
				
			||||||
 | 
					        "yargs-parser": "^20.2.2"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=10"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/electron-installer-redhat/node_modules/yargs-parser": {
 | 
				
			||||||
 | 
					      "version": "20.2.9",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "ISC",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=10"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/electron-is-accelerator": {
 | 
					    "node_modules/electron-is-accelerator": {
 | 
				
			||||||
      "version": "0.1.2",
 | 
					      "version": "0.1.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz",
 | 
				
			||||||
@@ -13320,13 +13534,13 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/playwright": {
 | 
					    "node_modules/playwright": {
 | 
				
			||||||
      "version": "1.50.0",
 | 
					      "version": "1.50.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==",
 | 
					      "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "Apache-2.0",
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "playwright-core": "1.50.0"
 | 
					        "playwright-core": "1.50.1"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "playwright": "cli.js"
 | 
					        "playwright": "cli.js"
 | 
				
			||||||
@@ -13339,9 +13553,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/playwright-core": {
 | 
					    "node_modules/playwright-core": {
 | 
				
			||||||
      "version": "1.50.0",
 | 
					      "version": "1.50.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==",
 | 
					      "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "Apache-2.0",
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
@@ -14721,9 +14935,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/semver": {
 | 
					    "node_modules/semver": {
 | 
				
			||||||
      "version": "7.7.0",
 | 
					      "version": "7.7.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
 | 
					      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
 | 
				
			||||||
      "license": "ISC",
 | 
					      "license": "ISC",
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "semver": "bin/semver.js"
 | 
					        "semver": "bin/semver.js"
 | 
				
			||||||
@@ -16856,9 +17070,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/vite-node": {
 | 
					    "node_modules/vite-node": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==",
 | 
					      "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
@@ -17350,19 +17564,19 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/vitest": {
 | 
					    "node_modules/vitest": {
 | 
				
			||||||
      "version": "3.0.4",
 | 
					      "version": "3.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==",
 | 
					      "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@vitest/expect": "3.0.4",
 | 
					        "@vitest/expect": "3.0.5",
 | 
				
			||||||
        "@vitest/mocker": "3.0.4",
 | 
					        "@vitest/mocker": "3.0.5",
 | 
				
			||||||
        "@vitest/pretty-format": "^3.0.4",
 | 
					        "@vitest/pretty-format": "^3.0.5",
 | 
				
			||||||
        "@vitest/runner": "3.0.4",
 | 
					        "@vitest/runner": "3.0.5",
 | 
				
			||||||
        "@vitest/snapshot": "3.0.4",
 | 
					        "@vitest/snapshot": "3.0.5",
 | 
				
			||||||
        "@vitest/spy": "3.0.4",
 | 
					        "@vitest/spy": "3.0.5",
 | 
				
			||||||
        "@vitest/utils": "3.0.4",
 | 
					        "@vitest/utils": "3.0.5",
 | 
				
			||||||
        "chai": "^5.1.2",
 | 
					        "chai": "^5.1.2",
 | 
				
			||||||
        "debug": "^4.4.0",
 | 
					        "debug": "^4.4.0",
 | 
				
			||||||
        "expect-type": "^1.1.0",
 | 
					        "expect-type": "^1.1.0",
 | 
				
			||||||
@@ -17374,7 +17588,7 @@
 | 
				
			|||||||
        "tinypool": "^1.0.2",
 | 
					        "tinypool": "^1.0.2",
 | 
				
			||||||
        "tinyrainbow": "^2.0.0",
 | 
					        "tinyrainbow": "^2.0.0",
 | 
				
			||||||
        "vite": "^5.0.0 || ^6.0.0",
 | 
					        "vite": "^5.0.0 || ^6.0.0",
 | 
				
			||||||
        "vite-node": "3.0.4",
 | 
					        "vite-node": "3.0.5",
 | 
				
			||||||
        "why-is-node-running": "^2.3.0"
 | 
					        "why-is-node-running": "^2.3.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
@@ -17390,8 +17604,8 @@
 | 
				
			|||||||
        "@edge-runtime/vm": "*",
 | 
					        "@edge-runtime/vm": "*",
 | 
				
			||||||
        "@types/debug": "^4.1.12",
 | 
					        "@types/debug": "^4.1.12",
 | 
				
			||||||
        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
 | 
					        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
 | 
				
			||||||
        "@vitest/browser": "3.0.4",
 | 
					        "@vitest/browser": "3.0.5",
 | 
				
			||||||
        "@vitest/ui": "3.0.4",
 | 
					        "@vitest/ui": "3.0.5",
 | 
				
			||||||
        "happy-dom": "*",
 | 
					        "happy-dom": "*",
 | 
				
			||||||
        "jsdom": "*"
 | 
					        "jsdom": "*"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								package.json
									
									
									
									
									
								
							@@ -2,7 +2,7 @@
 | 
				
			|||||||
  "name": "trilium",
 | 
					  "name": "trilium",
 | 
				
			||||||
  "productName": "TriliumNext Notes",
 | 
					  "productName": "TriliumNext Notes",
 | 
				
			||||||
  "description": "Build your personal knowledge base with TriliumNext Notes",
 | 
					  "description": "Build your personal knowledge base with TriliumNext Notes",
 | 
				
			||||||
  "version": "0.91.5",
 | 
					  "version": "0.91.6",
 | 
				
			||||||
  "license": "AGPL-3.0-only",
 | 
					  "license": "AGPL-3.0-only",
 | 
				
			||||||
  "main": "./dist/electron-main.js",
 | 
					  "main": "./dist/electron-main.js",
 | 
				
			||||||
  "author": {
 | 
					  "author": {
 | 
				
			||||||
@@ -131,7 +131,6 @@
 | 
				
			|||||||
    "sanitize-filename": "1.6.3",
 | 
					    "sanitize-filename": "1.6.3",
 | 
				
			||||||
    "sanitize-html": "2.14.0",
 | 
					    "sanitize-html": "2.14.0",
 | 
				
			||||||
    "sax": "1.4.1",
 | 
					    "sax": "1.4.1",
 | 
				
			||||||
    "semver": "7.7.0",
 | 
					 | 
				
			||||||
    "serve-favicon": "2.5.0",
 | 
					    "serve-favicon": "2.5.0",
 | 
				
			||||||
    "session-file-store": "1.5.0",
 | 
					    "session-file-store": "1.5.0",
 | 
				
			||||||
    "source-map-support": "0.5.21",
 | 
					    "source-map-support": "0.5.21",
 | 
				
			||||||
@@ -151,11 +150,12 @@
 | 
				
			|||||||
    "@electron-forge/cli": "7.6.1",
 | 
					    "@electron-forge/cli": "7.6.1",
 | 
				
			||||||
    "@electron-forge/maker-deb": "7.6.1",
 | 
					    "@electron-forge/maker-deb": "7.6.1",
 | 
				
			||||||
    "@electron-forge/maker-dmg": "7.6.1",
 | 
					    "@electron-forge/maker-dmg": "7.6.1",
 | 
				
			||||||
 | 
					    "@electron-forge/maker-rpm": "7.6.1",
 | 
				
			||||||
    "@electron-forge/maker-squirrel": "7.6.1",
 | 
					    "@electron-forge/maker-squirrel": "7.6.1",
 | 
				
			||||||
    "@electron-forge/maker-zip": "7.6.1",
 | 
					    "@electron-forge/maker-zip": "7.6.1",
 | 
				
			||||||
    "@electron-forge/plugin-auto-unpack-natives": "7.6.1",
 | 
					    "@electron-forge/plugin-auto-unpack-natives": "7.6.1",
 | 
				
			||||||
    "@electron/rebuild": "3.7.1",
 | 
					    "@electron/rebuild": "3.7.1",
 | 
				
			||||||
    "@playwright/test": "1.50.0",
 | 
					    "@playwright/test": "1.50.1",
 | 
				
			||||||
    "@types/archiver": "6.0.3",
 | 
					    "@types/archiver": "6.0.3",
 | 
				
			||||||
    "@types/better-sqlite3": "7.6.12",
 | 
					    "@types/better-sqlite3": "7.6.12",
 | 
				
			||||||
    "@types/bootstrap": "5.2.10",
 | 
					    "@types/bootstrap": "5.2.10",
 | 
				
			||||||
@@ -178,12 +178,11 @@
 | 
				
			|||||||
    "@types/leaflet-gpx": "1.3.7",
 | 
					    "@types/leaflet-gpx": "1.3.7",
 | 
				
			||||||
    "@types/mime-types": "2.1.4",
 | 
					    "@types/mime-types": "2.1.4",
 | 
				
			||||||
    "@types/multer": "1.4.12",
 | 
					    "@types/multer": "1.4.12",
 | 
				
			||||||
    "@types/node": "22.12.0",
 | 
					    "@types/node": "22.13.1",
 | 
				
			||||||
    "@types/react": "18.3.18",
 | 
					    "@types/react": "18.3.18",
 | 
				
			||||||
    "@types/safe-compare": "1.1.2",
 | 
					    "@types/safe-compare": "1.1.2",
 | 
				
			||||||
    "@types/sanitize-html": "2.13.0",
 | 
					    "@types/sanitize-html": "2.13.0",
 | 
				
			||||||
    "@types/sax": "1.2.7",
 | 
					    "@types/sax": "1.2.7",
 | 
				
			||||||
    "@types/semver": "7.5.8",
 | 
					 | 
				
			||||||
    "@types/serve-favicon": "2.5.7",
 | 
					    "@types/serve-favicon": "2.5.7",
 | 
				
			||||||
    "@types/session-file-store": "1.2.5",
 | 
					    "@types/session-file-store": "1.2.5",
 | 
				
			||||||
    "@types/source-map-support": "0.5.10",
 | 
					    "@types/source-map-support": "0.5.10",
 | 
				
			||||||
@@ -193,7 +192,7 @@
 | 
				
			|||||||
    "@types/ws": "8.5.14",
 | 
					    "@types/ws": "8.5.14",
 | 
				
			||||||
    "@types/xml2js": "0.4.14",
 | 
					    "@types/xml2js": "0.4.14",
 | 
				
			||||||
    "@types/yargs": "17.0.33",
 | 
					    "@types/yargs": "17.0.33",
 | 
				
			||||||
    "@vitest/coverage-v8": "3.0.4",
 | 
					    "@vitest/coverage-v8": "3.0.5",
 | 
				
			||||||
    "cross-env": "7.0.3",
 | 
					    "cross-env": "7.0.3",
 | 
				
			||||||
    "electron": "34.0.2",
 | 
					    "electron": "34.0.2",
 | 
				
			||||||
    "esm": "3.2.25",
 | 
					    "esm": "3.2.25",
 | 
				
			||||||
@@ -208,7 +207,7 @@
 | 
				
			|||||||
    "tsx": "4.19.2",
 | 
					    "tsx": "4.19.2",
 | 
				
			||||||
    "typedoc": "0.27.6",
 | 
					    "typedoc": "0.27.6",
 | 
				
			||||||
    "typescript": "5.7.3",
 | 
					    "typescript": "5.7.3",
 | 
				
			||||||
    "vitest": "3.0.4",
 | 
					    "vitest": "3.0.5",
 | 
				
			||||||
    "webpack": "5.97.1",
 | 
					    "webpack": "5.97.1",
 | 
				
			||||||
    "webpack-cli": "6.0.1",
 | 
					    "webpack-cli": "6.0.1",
 | 
				
			||||||
    "webpack-dev-middleware": "7.4.2"
 | 
					    "webpack-dev-middleware": "7.4.2"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,6 @@ import noteTooltipService from "./services/note_tooltip.js";
 | 
				
			|||||||
import bundleService from "./services/bundle.js";
 | 
					import bundleService from "./services/bundle.js";
 | 
				
			||||||
import toastService from "./services/toast.js";
 | 
					import toastService from "./services/toast.js";
 | 
				
			||||||
import noteAutocompleteService from "./services/note_autocomplete.js";
 | 
					import noteAutocompleteService from "./services/note_autocomplete.js";
 | 
				
			||||||
import macInit from "./services/mac_init.js";
 | 
					 | 
				
			||||||
import electronContextMenu from "./menus/electron_context_menu.js";
 | 
					import electronContextMenu from "./menus/electron_context_menu.js";
 | 
				
			||||||
import glob from "./services/glob.js";
 | 
					import glob from "./services/glob.js";
 | 
				
			||||||
import { t } from "./services/i18n.js";
 | 
					import { t } from "./services/i18n.js";
 | 
				
			||||||
@@ -35,8 +34,6 @@ if (utils.isElectron()) {
 | 
				
			|||||||
    initOnElectron();
 | 
					    initOnElectron();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
macInit.init();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
noteTooltipService.setupGlobalTooltip();
 | 
					noteTooltipService.setupGlobalTooltip();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
noteAutocompleteService.init();
 | 
					noteAutocompleteService.init();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,26 +0,0 @@
 | 
				
			|||||||
/**
 | 
					 | 
				
			||||||
 * Mac specific initialization
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
import utils from "./utils.js";
 | 
					 | 
				
			||||||
import shortcutService from "./shortcuts.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function init() {
 | 
					 | 
				
			||||||
    if (utils.isElectron() && utils.isMac()) {
 | 
					 | 
				
			||||||
        shortcutService.bindGlobalShortcut("meta+c", () => exec("copy"));
 | 
					 | 
				
			||||||
        shortcutService.bindGlobalShortcut("meta+v", () => exec("paste"));
 | 
					 | 
				
			||||||
        shortcutService.bindGlobalShortcut("meta+x", () => exec("cut"));
 | 
					 | 
				
			||||||
        shortcutService.bindGlobalShortcut("meta+a", () => exec("selectAll"));
 | 
					 | 
				
			||||||
        shortcutService.bindGlobalShortcut("meta+z", () => exec("undo"));
 | 
					 | 
				
			||||||
        shortcutService.bindGlobalShortcut("meta+y", () => exec("redo"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function exec(cmd: string) {
 | 
					 | 
				
			||||||
    document.execCommand(cmd);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
    init
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@@ -1,9 +1,6 @@
 | 
				
			|||||||
import utils from "./services/utils.js";
 | 
					import utils from "./services/utils.js";
 | 
				
			||||||
import macInit from "./services/mac_init.js";
 | 
					 | 
				
			||||||
import ko from "knockout";
 | 
					import ko from "knockout";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
macInit.init();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TriliumNextTODO: properly make use of below types
 | 
					// TriliumNextTODO: properly make use of below types
 | 
				
			||||||
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
 | 
					// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
 | 
				
			||||||
// type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop";
 | 
					// type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,6 +67,10 @@ const TPL = `
 | 
				
			|||||||
        .attr-detail input[readonly] {
 | 
					        .attr-detail input[readonly] {
 | 
				
			||||||
            background-color: var(--accented-background-color) !important;
 | 
					            background-color: var(--accented-background-color) !important;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .attr-edit-table td {
 | 
				
			||||||
 | 
					            padding: 4px 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
 | 
					    <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
 | 
				
			||||||
@@ -97,8 +101,13 @@ const TPL = `
 | 
				
			|||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
        <tr class="attr-row-promoted"
 | 
					        <tr class="attr-row-promoted"
 | 
				
			||||||
            title="${t("attribute_detail.promoted_title")}">
 | 
					            title="${t("attribute_detail.promoted_title")}">
 | 
				
			||||||
            <th>${t("attribute_detail.promoted")}</th>
 | 
					            <th></th>
 | 
				
			||||||
            <td><input type="checkbox" class="attr-input-promoted form-check" /></td>
 | 
					            <td>
 | 
				
			||||||
 | 
					                <label class="tn-checkbox">
 | 
				
			||||||
 | 
					                    <input type="checkbox" class="attr-input-promoted form-check" />
 | 
				
			||||||
 | 
					                    ${t("attribute_detail.promoted")}
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
        <tr class="attr-row-promoted-alias">
 | 
					        <tr class="attr-row-promoted-alias">
 | 
				
			||||||
            <th title="${t("attribute_detail.promoted_alias_title")}">${t("attribute_detail.promoted_alias")}</th>
 | 
					            <th title="${t("attribute_detail.promoted_alias_title")}">${t("attribute_detail.promoted_alias")}</th>
 | 
				
			||||||
@@ -149,8 +158,13 @@ const TPL = `
 | 
				
			|||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
        <tr title="${t("attribute_detail.inheritable_title")}">
 | 
					        <tr title="${t("attribute_detail.inheritable_title")}">
 | 
				
			||||||
            <th>${t("attribute_detail.inheritable")}</th>
 | 
					            <th></th>
 | 
				
			||||||
            <td><input type="checkbox" class="attr-input-inheritable form-check" /></td>
 | 
					            <td>
 | 
				
			||||||
 | 
					                <label class="tn-checkbox">
 | 
				
			||||||
 | 
					                    <input type="checkbox" class="attr-input-inheritable form-check" />
 | 
				
			||||||
 | 
					                    ${t("attribute_detail.inheritable")}
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,8 +77,8 @@ const TPL = `
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <div class="attribute-list-editor" tabindex="200"></div>
 | 
					    <div class="attribute-list-editor" tabindex="200"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="bx bx-save save-attributes-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
 | 
					    <div class="bx bx-save save-attributes-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
 | 
				
			||||||
    <div class="bx bx-plus add-new-attribute-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
 | 
					    <div class="bx bx-plus add-new-attribute-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="attribute-errors" style="display: none;"></div>
 | 
					    <div class="attribute-errors" style="display: none;"></div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,16 +25,22 @@ const TPL = `
 | 
				
			|||||||
                    ${t("include_note.box_size_prompt")}
 | 
					                    ${t("include_note.box_size_prompt")}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    <div class="form-check">
 | 
					                    <div class="form-check">
 | 
				
			||||||
 | 
					                        <label class="form-check-label tn-radio">
 | 
				
			||||||
                            <input class="form-check-input" type="radio" name="include-note-box-size" value="small">
 | 
					                            <input class="form-check-input" type="radio" name="include-note-box-size" value="small">
 | 
				
			||||||
                        <label class="form-check-label">${t("include_note.box_size_small")}</label>
 | 
					                            ${t("include_note.box_size_small")}
 | 
				
			||||||
 | 
					                        </label>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <div class="form-check">
 | 
					                    <div class="form-check">
 | 
				
			||||||
 | 
					                        <label class="form-check-label tn-radio">
 | 
				
			||||||
                            <input class="form-check-input" type="radio" name="include-note-box-size" value="medium" checked>
 | 
					                            <input class="form-check-input" type="radio" name="include-note-box-size" value="medium" checked>
 | 
				
			||||||
                        <label class="form-check-label">${t("include_note.box_size_medium")}</label>
 | 
					                            ${t("include_note.box_size_medium")}
 | 
				
			||||||
 | 
					                        </label>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <div class="form-check">
 | 
					                    <div class="form-check">
 | 
				
			||||||
 | 
					                        <label class="form-check-label tn-radio">
 | 
				
			||||||
                            <input class="form-check-input" type="radio" name="include-note-box-size" value="full">
 | 
					                            <input class="form-check-input" type="radio" name="include-note-box-size" value="full">
 | 
				
			||||||
                        <label class="form-check-label">${t("include_note.box_size_full")}</label>
 | 
					                            ${t("include_note.box_size_full")}
 | 
				
			||||||
 | 
					                        </label>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="modal-footer">
 | 
					                <div class="modal-footer">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,7 @@ const TPL = `
 | 
				
			|||||||
        <span class="editability-active-desc">${t("editability_select.auto")}</span>
 | 
					        <span class="editability-active-desc">${t("editability_select.auto")}</span>
 | 
				
			||||||
        <span class="caret"></span>
 | 
					        <span class="caret"></span>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
    <div class="editability-dropdown dropdown-menu dropdown-menu-right">
 | 
					    <div class="editability-dropdown dropdown-menu dropdown-menu-right tn-dropdown-list">
 | 
				
			||||||
        <a class="dropdown-item" href="#" data-editability="auto">
 | 
					        <a class="dropdown-item" href="#" data-editability="auto">
 | 
				
			||||||
            <span class="check">✓</span>
 | 
					            <span class="check">✓</span>
 | 
				
			||||||
            ${t("editability_select.auto")}
 | 
					            ${t("editability_select.auto")}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,11 +33,15 @@ const TPL = `
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .find-widget-found-wrapper {
 | 
					        .find-widget-found-wrapper {
 | 
				
			||||||
            font-weight: bold;
 | 
					            justify-content: center;
 | 
				
			||||||
 | 
					            min-width: 60px;
 | 
				
			||||||
 | 
					            padding: 0 4px;
 | 
				
			||||||
 | 
					            font-size: .85em;
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .find-widget-search-term-input-group, .replace-widget-replacetext-input {
 | 
					        .find-widget-search-term-input-group, .replace-widget-replacetext-input {
 | 
				
			||||||
            max-width: 300px;
 | 
					            max-width: 350px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .find-widget-spacer {
 | 
					        .find-widget-spacer {
 | 
				
			||||||
@@ -49,6 +53,13 @@ const TPL = `
 | 
				
			|||||||
        <div class="input-group find-widget-search-term-input-group">
 | 
					        <div class="input-group find-widget-search-term-input-group">
 | 
				
			||||||
            <input type="text" class="form-control find-widget-search-term-input" placeholder="${t("find.find_placeholder")}">
 | 
					            <input type="text" class="form-control find-widget-search-term-input" placeholder="${t("find.find_placeholder")}">
 | 
				
			||||||
            <button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button>
 | 
					            <button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button>
 | 
				
			||||||
 | 
					            <div class="find-widget-found-wrapper input-group-text">
 | 
				
			||||||
 | 
					                <span>
 | 
				
			||||||
 | 
					                    <span class="find-widget-current-found">0</span>
 | 
				
			||||||
 | 
					                    /
 | 
				
			||||||
 | 
					                    <span class="find-widget-total-found">0</span>
 | 
				
			||||||
 | 
					                <span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
            <button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button>
 | 
					            <button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,11 +77,7 @@ const TPL = `
 | 
				
			|||||||
            </label>
 | 
					            </label>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="find-widget-found-wrapper">
 | 
					
 | 
				
			||||||
            <span class="find-widget-current-found">0</span>
 | 
					 | 
				
			||||||
            /
 | 
					 | 
				
			||||||
            <span class="find-widget-total-found">0</span>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="find-widget-spacer"></div>
 | 
					        <div class="find-widget-spacer"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,7 @@ const TPL = `
 | 
				
			|||||||
        <span class="note-type-desc"></span>
 | 
					        <span class="note-type-desc"></span>
 | 
				
			||||||
        <span class="caret"></span>
 | 
					        <span class="caret"></span>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
    <div class="note-type-dropdown dropdown-menu dropdown-menu-left"></div>
 | 
					    <div class="note-type-dropdown dropdown-menu dropdown-menu-left tn-dropdown-list"></div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@ const TPL = `
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="links-wrapper"></div>
 | 
					    <div class="links-wrapper use-tn-links"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="attachment-wrapper"></div>
 | 
					    <div class="attachment-wrapper"></div>
 | 
				
			||||||
</div>`;
 | 
					</div>`;
 | 
				
			||||||
@@ -57,7 +57,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        this.$linksWrapper.empty().append(
 | 
					        this.$linksWrapper.empty().append(
 | 
				
			||||||
            t("attachment_detail.owning_note"),
 | 
					            t("attachment_detail.owning_note"),
 | 
				
			||||||
            await linkService.createLink(this.noteId),
 | 
					            (await linkService.createLink(this.noteId)),
 | 
				
			||||||
            t("attachment_detail.you_can_also_open"),
 | 
					            t("attachment_detail.you_can_also_open"),
 | 
				
			||||||
            await linkService.createLink(this.noteId, {
 | 
					            await linkService.createLink(this.noteId, {
 | 
				
			||||||
                title: t("attachment_detail.list_of_all_attachments"),
 | 
					                title: t("attachment_detail.list_of_all_attachments"),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ const TPL = `
 | 
				
			|||||||
    <hr />
 | 
					    <hr />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="side-checkbox">
 | 
					    <div class="side-checkbox">
 | 
				
			||||||
        <label class="form-check">
 | 
					        <label class="form-check tn-checkbox">
 | 
				
			||||||
            <input type="checkbox" class="native-title-bar form-check-input" />
 | 
					            <input type="checkbox" class="native-title-bar form-check-input" />
 | 
				
			||||||
            <strong>${t("electron_integration.native-title-bar")}</strong>
 | 
					            <strong>${t("electron_integration.native-title-bar")}</strong>
 | 
				
			||||||
            <p>${t("electron_integration.native-title-bar-description")}</p>
 | 
					            <p>${t("electron_integration.native-title-bar-description")}</p>
 | 
				
			||||||
@@ -25,7 +25,7 @@ const TPL = `
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="side-checkbox">
 | 
					    <div class="side-checkbox">
 | 
				
			||||||
        <label class="form-check">
 | 
					        <label class="form-check tn-checkbox">
 | 
				
			||||||
            <input type="checkbox" class="background-effects form-check-input" />
 | 
					            <input type="checkbox" class="background-effects form-check-input" />
 | 
				
			||||||
            <strong>${t("electron_integration.background-effects")}</strong>
 | 
					            <strong>${t("electron_integration.background-effects")}</strong>
 | 
				
			||||||
            <p>${t("electron_integration.background-effects-description")}</p>
 | 
					            <p>${t("electron_integration.background-effects-description")}</p>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ const TPL = `
 | 
				
			|||||||
<div class="options-section">
 | 
					<div class="options-section">
 | 
				
			||||||
    <h4>${t("tray.title")}</h4>
 | 
					    <h4>${t("tray.title")}</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <label>
 | 
					    <label class="tn-checkbox">
 | 
				
			||||||
        <input type="checkbox" class="tray-enabled">
 | 
					        <input type="checkbox" class="tray-enabled">
 | 
				
			||||||
        ${t("tray.enable_tray")}
 | 
					        ${t("tray.enable_tray")}
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ const TPL_ELECTRON = `
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <p>${t("spellcheck.restart-required")}</p>
 | 
					    <p>${t("spellcheck.restart-required")}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <label>
 | 
					    <label class="tn-checkbox">
 | 
				
			||||||
        <input type="checkbox" class="spell-check-enabled">
 | 
					        <input type="checkbox" class="spell-check-enabled">
 | 
				
			||||||
        ${t("spellcheck.enable")}
 | 
					        ${t("spellcheck.enable")}
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,12 @@ const TPL = `
 | 
				
			|||||||
        width: 300px;
 | 
					        width: 300px;
 | 
				
			||||||
        margin: 30px auto auto;
 | 
					        margin: 30px auto auto;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .protected-session-password-component input,
 | 
				
			||||||
 | 
					    .protected-session-password-component button {
 | 
				
			||||||
 | 
					        margin-top: 12px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <form class="protected-session-password-form">
 | 
					    <form class="protected-session-password-form">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@import url(./forms.css);
 | 
					@import url(./forms.css);
 | 
				
			||||||
@import url(./shell.css);
 | 
					@import url(./shell.css);
 | 
				
			||||||
 | 
					@import url(./ribbon.css);
 | 
				
			||||||
@import url(./settings.css);
 | 
					@import url(./settings.css);
 | 
				
			||||||
 | 
					@import url(./notes/empty.css);
 | 
				
			||||||
@import url(./notes/text.css);
 | 
					@import url(./notes/text.css);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@font-face {
 | 
					@font-face {
 | 
				
			||||||
@@ -64,3 +66,29 @@
 | 
				
			|||||||
    /* Theme capabilities */
 | 
					    /* Theme capabilities */
 | 
				
			||||||
    --tab-note-icons: true;
 | 
					    --tab-note-icons: true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Note search suggestions
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* List body */
 | 
				
			||||||
 | 
					.jump-to-note-dialog .jump-to-note-results .aa-suggestions,
 | 
				
			||||||
 | 
					.note-detail-empty .aa-suggestions {
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* List item */
 | 
				
			||||||
 | 
					.jump-to-note-dialog .aa-suggestions div,
 | 
				
			||||||
 | 
					.note-detail-empty .aa-suggestions div {
 | 
				
			||||||
 | 
					    border-radius: 6px;
 | 
				
			||||||
 | 
					    padding: 6px 12px;
 | 
				
			||||||
 | 
					    color: var(--menu-text-color);
 | 
				
			||||||
 | 
					    cursor: default;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Selected list item */
 | 
				
			||||||
 | 
					.jump-to-note-dialog .aa-suggestions div.aa-cursor,
 | 
				
			||||||
 | 
					.note-detail-empty .aa-suggestions div.aa-cursor {
 | 
				
			||||||
 | 
					    background: var(--hover-item-background-color);
 | 
				
			||||||
 | 
					    color: var(--hover-item-text-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -78,10 +78,11 @@ button.btn.btn-success kbd {
 | 
				
			|||||||
 * Icon buttons
 | 
					 * Icon buttons
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:root .icon-action:not(.global-menu-button) {
 | 
					:root .icon-action:not(.global-menu-button),
 | 
				
			||||||
 | 
					:root .tn-tool-button {
 | 
				
			||||||
    width: var(--icon-button-size);
 | 
					    width: var(--icon-button-size);
 | 
				
			||||||
    height: var(--icon-button-size);
 | 
					    height: var(--icon-button-size);
 | 
				
			||||||
    border: unset;
 | 
					    border: unset !important;
 | 
				
			||||||
    border-radius: 8px;
 | 
					    border-radius: 8px;
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
    text-align: center;
 | 
					    text-align: center;
 | 
				
			||||||
@@ -89,7 +90,8 @@ button.btn.btn-success kbd {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* The "x" icon button */
 | 
					/* The "x" icon button */
 | 
				
			||||||
:root .icon-action.bx-x {
 | 
					:root .icon-action.bx-x,
 | 
				
			||||||
 | 
					:root .tn-tool-button.bx-x {
 | 
				
			||||||
    --icon-button-hover-background: var(--tab-close-button-hover-background);
 | 
					    --icon-button-hover-background: var(--tab-close-button-hover-background);
 | 
				
			||||||
    --icon-button-hover-color: var(--tab-close-button-hover-color);
 | 
					    --icon-button-hover-color: var(--tab-close-button-hover-color);
 | 
				
			||||||
    --icon-button-size: 24px;
 | 
					    --icon-button-size: 24px;
 | 
				
			||||||
@@ -99,23 +101,28 @@ button.btn.btn-success kbd {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* The icon */
 | 
					/* The icon */
 | 
				
			||||||
:root .icon-action:not(.global-menu-button)::before {
 | 
					:root .icon-action:not(.global-menu-button)::before,
 | 
				
			||||||
 | 
					:root .tn-tool-button::before {
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
    line-height: var(--icon-button-size);
 | 
					    line-height: var(--icon-button-size);
 | 
				
			||||||
    font-size: calc(var(--icon-button-size) * var(--icon-button-icon-ratio));
 | 
					    font-size: calc(var(--icon-button-size) * var(--icon-button-icon-ratio));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:root .icon-action:not(.global-menu-button):hover,
 | 
					:root .icon-action:not(.global-menu-button):hover,
 | 
				
			||||||
:root .icon-action:not(.global-menu-button).show {
 | 
					:root .icon-action:not(.global-menu-button).show,
 | 
				
			||||||
 | 
					:root .tn-tool-button:hover,
 | 
				
			||||||
 | 
					:root .tn-tool-button.show {
 | 
				
			||||||
    background: var(--icon-button-hover-background);
 | 
					    background: var(--icon-button-hover-background);
 | 
				
			||||||
    color: var(--icon-button-hover-color);
 | 
					    color: var(--icon-button-hover-color);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:root .icon-action:not(.global-menu-button):active::before {
 | 
					:root .icon-action:not(.global-menu-button):active::before,
 | 
				
			||||||
 | 
					:root .tn-tool-button:active::before {
 | 
				
			||||||
    transform: scale(.85);
 | 
					    transform: scale(.85);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:root .icon-action:not(.global-menu-button):focus-visible {
 | 
					:root .icon-action:not(.global-menu-button):focus-visible,
 | 
				
			||||||
 | 
					:root .tn-tool-button:focus-visible {
 | 
				
			||||||
    outline: 2px solid var(--input-focus-outline-color);
 | 
					    outline: 2px solid var(--input-focus-outline-color);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -216,6 +223,7 @@ input::selection,
 | 
				
			|||||||
.input-group button,
 | 
					.input-group button,
 | 
				
			||||||
.input-group a {
 | 
					.input-group a {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
    --bs-border-width: 0;
 | 
					    --bs-border-width: 0;
 | 
				
			||||||
    --accented-background-color: transparent;
 | 
					    --accented-background-color: transparent;
 | 
				
			||||||
    background: transparent;
 | 
					    background: transparent;
 | 
				
			||||||
@@ -228,19 +236,26 @@ input::selection,
 | 
				
			|||||||
.input-group button:hover,
 | 
					.input-group button:hover,
 | 
				
			||||||
.input-group a:hover {
 | 
					.input-group a:hover {
 | 
				
			||||||
    --muted-text-color: var(--input-action-button-hover);
 | 
					    --muted-text-color: var(--input-action-button-hover);
 | 
				
			||||||
 | 
					    color: var(--input-action-button-hover);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.input-group button:focus-visible,
 | 
					.input-group button:focus-visible,
 | 
				
			||||||
.input-group a:focus-visible {
 | 
					.input-group a:focus-visible {
 | 
				
			||||||
 | 
					    box-shadow: unset;
 | 
				
			||||||
    outline: transparent;
 | 
					    outline: transparent;
 | 
				
			||||||
    border: transparent;
 | 
					    border: transparent;
 | 
				
			||||||
    text-shadow: 0 0 3px var(--input-action-button-hover);
 | 
					    text-shadow: 0 0 3px var(--input-action-button-hover);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.input-group button:active {
 | 
				
			||||||
 | 
					    background: transparent !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.input-group a.disabled {
 | 
					.input-group a.disabled {
 | 
				
			||||||
    opacity: .5;
 | 
					    opacity: .5;
 | 
				
			||||||
    /* Workaround to set the "background" property. */
 | 
					    /* Workaround to set the "background" property. */
 | 
				
			||||||
    --button-disabled-background-color: transparent;
 | 
					    --button-disabled-background-color: transparent;
 | 
				
			||||||
 | 
					    --button-disabled-text-color: var(--input-action-button-color);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.input-group .input-clearer-button {
 | 
					.input-group .input-clearer-button {
 | 
				
			||||||
@@ -273,6 +288,7 @@ input::selection,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
select,
 | 
					select,
 | 
				
			||||||
select.form-select,
 | 
					select.form-select,
 | 
				
			||||||
 | 
					select.form-control,
 | 
				
			||||||
.select-button.dropdown-toggle.btn {
 | 
					.select-button.dropdown-toggle.btn {
 | 
				
			||||||
    outline: 3px solid transparent;
 | 
					    outline: 3px solid transparent;
 | 
				
			||||||
    outline-offset: 6px;
 | 
					    outline-offset: 6px;
 | 
				
			||||||
@@ -285,6 +301,7 @@ select.form-select,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
select:hover,
 | 
					select:hover,
 | 
				
			||||||
select.form-select:hover,
 | 
					select.form-select:hover,
 | 
				
			||||||
 | 
					select.form-control:hover,
 | 
				
			||||||
.select-button.dropdown-toggle.btn:hover {
 | 
					.select-button.dropdown-toggle.btn:hover {
 | 
				
			||||||
    background-color: var(--input-hover-background);
 | 
					    background-color: var(--input-hover-background);
 | 
				
			||||||
    color: var(--input-hover-color);
 | 
					    color: var(--input-hover-color);
 | 
				
			||||||
@@ -297,6 +314,7 @@ button.select-button.dropdown-toggle.btn:active {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
select:focus,
 | 
					select:focus,
 | 
				
			||||||
select.form-select:focus,
 | 
					select.form-select:focus,
 | 
				
			||||||
 | 
					select.form-control:focus,
 | 
				
			||||||
.select-button.dropdown-toggle.btn:focus {
 | 
					.select-button.dropdown-toggle.btn:focus {
 | 
				
			||||||
    box-shadow: unset;
 | 
					    box-shadow: unset;
 | 
				
			||||||
    outline: 3px solid var(--input-focus-outline-color);
 | 
					    outline: 3px solid var(--input-focus-outline-color);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								src/public/stylesheets/theme-next/notes/empty.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/public/stylesheets/theme-next/notes/empty.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					/* The container */
 | 
				
			||||||
 | 
					div.note-detail-empty {
 | 
				
			||||||
 | 
					    max-width: 70%;
 | 
				
			||||||
 | 
					    margin: 50px auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The search results list */
 | 
				
			||||||
 | 
					.note-detail-empty span.aa-dropdown-menu {
 | 
				
			||||||
 | 
					    margin-top: 1em;
 | 
				
			||||||
 | 
					    border: unset;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -126,3 +126,41 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
 | 
				
			|||||||
    border: 0 !important;
 | 
					    border: 0 !important;
 | 
				
			||||||
    border-top: 1px solid var(--main-border-color) !important;
 | 
					    border-top: 1px solid var(--main-border-color) !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Table caption */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ck-content .table > figcaption {
 | 
				
			||||||
 | 
					    background: var(--accented-background-color);
 | 
				
			||||||
 | 
					    color: var(--main-text-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Search in text panel
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.find-replace-widget > div {
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.find-replace-widget > div + div {
 | 
				
			||||||
 | 
					    padding-top: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.find-replace-widget div.find-widget-found-wrapper {
 | 
				
			||||||
 | 
					    min-width: 50px;
 | 
				
			||||||
 | 
					    font-style: normal;
 | 
				
			||||||
 | 
					    font-weight: normal;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 /* The up / down buttons of the "Find in text" input */
 | 
				
			||||||
 | 
					.find-replace-widget .input-group button {
 | 
				
			||||||
 | 
					    font-size: 1.3em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.find-replace-widget .form-check {
 | 
				
			||||||
 | 
					    padding-left: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.find-replace-widget .form-check .form-check-input {
 | 
				
			||||||
 | 
					    margin-left: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								src/public/stylesheets/theme-next/ribbon.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/public/stylesheets/theme-next/ribbon.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Owned attributes
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.attribute-list .add-new-attribute-button,
 | 
				
			||||||
 | 
					.attribute-list .save-attributes-button  {
 | 
				
			||||||
 | 
					    bottom: .3em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.attribute-list .save-attributes-button {
 | 
				
			||||||
 | 
					    right: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Similar notes
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:root .similar-notes-widget a {
 | 
				
			||||||
 | 
					    color: var(--cmd-button-text-color);
 | 
				
			||||||
 | 
					    background: var(--cmd-button-background-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:root .similar-notes-widget a:hover {
 | 
				
			||||||
 | 
					    color: var(--cmd-button-hover-text-color);
 | 
				
			||||||
 | 
					    background: var(--cmd-button-hover-background-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 
 | 
				
			||||||
 | 
					 * Note info
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.note-info-widget-table th {
 | 
				
			||||||
 | 
					    opacity: .65;
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:root .note-info-widget-table button.calculate-button {
 | 
				
			||||||
 | 
					    min-width: 0;
 | 
				
			||||||
 | 
					    padding: 4px 10px !important;
 | 
				
			||||||
 | 
					    font-size: .8em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -227,6 +227,12 @@ body.layout-horizontal > .horizontal {
 | 
				
			|||||||
    --hover-item-background-color: transparent;
 | 
					    --hover-item-background-color: transparent;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#launcher-pane.horizontal .global-menu-button .global-menu-button-update-available {
 | 
				
			||||||
 | 
					    right: -23px;
 | 
				
			||||||
 | 
					    bottom: -22px;
 | 
				
			||||||
 | 
					    transform: scale(0.85);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tooltip .tooltip-arrow {
 | 
					.tooltip .tooltip-arrow {
 | 
				
			||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -945,6 +951,9 @@ body.mobile .note-title {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Menus
 | 
					 * Menus
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Note: apply the "tn-dropdown-list" class for scrollable dropdown menus. Submenus are not
 | 
				
			||||||
 | 
					 * supported when this class is used.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown-menu {
 | 
					.dropdown-menu {
 | 
				
			||||||
@@ -953,6 +962,10 @@ body.mobile .note-title {
 | 
				
			|||||||
    font-size: 0.9rem !important;
 | 
					    font-size: 0.9rem !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dropdown-menu::-webkit-scrollbar-track {
 | 
				
			||||||
 | 
					    background: var(--menu-background-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
body.mobile .dropdown-menu {
 | 
					body.mobile .dropdown-menu {
 | 
				
			||||||
    backdrop-filter: var(--dropdown-backdrop-filter);
 | 
					    backdrop-filter: var(--dropdown-backdrop-filter);
 | 
				
			||||||
    border-radius: var(--dropdown-border-radius);
 | 
					    border-radius: var(--dropdown-border-radius);
 | 
				
			||||||
@@ -976,6 +989,14 @@ body.desktop .dropdown-menu::before {
 | 
				
			|||||||
    z-index: -1;
 | 
					    z-index: -1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body.desktop .dropdown-menu.tn-dropdown-list {
 | 
				
			||||||
 | 
					    backdrop-filter: var(--dropdown-backdrop-filter);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body.desktop .dropdown-menu.tn-dropdown-list::before {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
body.desktop .dropdown-submenu .dropdown-menu::before {
 | 
					body.desktop .dropdown-submenu .dropdown-menu::before {
 | 
				
			||||||
    content: unset;
 | 
					    content: unset;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1237,25 +1258,6 @@ body .calendar-dropdown-widget .calendar-body a:hover {
 | 
				
			|||||||
    background: transparent !important;
 | 
					    background: transparent !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* List body */
 | 
					 | 
				
			||||||
.jump-to-note-dialog .jump-to-note-results .aa-suggestions {
 | 
					 | 
				
			||||||
    padding: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* List item */
 | 
					 | 
				
			||||||
.jump-to-note-dialog .aa-suggestions div {
 | 
					 | 
				
			||||||
    border-radius: 6px;
 | 
					 | 
				
			||||||
    padding: 6px 12px;
 | 
					 | 
				
			||||||
    color: var(--menu-text-color);
 | 
					 | 
				
			||||||
    cursor: default;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Selected list item */
 | 
					 | 
				
			||||||
.jump-to-note-dialog .aa-suggestions div.aa-cursor {
 | 
					 | 
				
			||||||
    background: var(--hover-item-background-color);
 | 
					 | 
				
			||||||
    color: var(--hover-item-text-color);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Recent changes list
 | 
					 * Recent changes list
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@@ -1657,7 +1659,17 @@ body .calendar-dropdown-widget .calendar-body a:hover {
 | 
				
			|||||||
    padding-left: 12px;
 | 
					    padding-left: 12px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.note-actions {
 | 
				
			||||||
 | 
					    --menu-item-icon-vert-offset: -2.5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Promoted attributes */
 | 
					/* Promoted attributes */
 | 
				
			||||||
.promoted-attribute-cell div.input-group {
 | 
					.promoted-attribute-cell div.input-group {
 | 
				
			||||||
    margin: 1px 0;    
 | 
					    margin: 1px 0;    
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Delete notes preview dialog */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.delete-notes-list .note-path {
 | 
				
			||||||
 | 
					    padding-left: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -216,6 +216,7 @@ function deleteBranch(req: Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function setPrefix(req: Request) {
 | 
					function setPrefix(req: Request) {
 | 
				
			||||||
    const branchId = req.params.branchId;
 | 
					    const branchId = req.params.branchId;
 | 
				
			||||||
 | 
					    //TriliumNextTODO: req.body arrives as string, so req.body.prefix will be undefined – did the code below ever even work?
 | 
				
			||||||
    const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
 | 
					    const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const branch = becca.getBranchOrThrow(branchId);
 | 
					    const branch = becca.getBranchOrThrow(branchId);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ import eventService from "./events.js";
 | 
				
			|||||||
import cls from "../services/cls.js";
 | 
					import cls from "../services/cls.js";
 | 
				
			||||||
import protectedSessionService from "../services/protected_session.js";
 | 
					import protectedSessionService from "../services/protected_session.js";
 | 
				
			||||||
import log from "../services/log.js";
 | 
					import log from "../services/log.js";
 | 
				
			||||||
import { newEntityId, isString, unescapeHtml, quoteRegex, toMap } from "../services/utils.js";
 | 
					import { newEntityId, unescapeHtml, quoteRegex, toMap } from "../services/utils.js";
 | 
				
			||||||
import revisionService from "./revisions.js";
 | 
					import revisionService from "./revisions.js";
 | 
				
			||||||
import request from "./request.js";
 | 
					import request from "./request.js";
 | 
				
			||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
@@ -737,10 +737,10 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment
 | 
				
			|||||||
      const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
 | 
					      const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const { attachmentId, role, mime, title, position, content } of attachments) {
 | 
					        for (const { attachmentId, role, mime, title, position, content } of attachments) {
 | 
				
			||||||
            if (attachmentId || !(title in existingAttachmentsByTitle)) {
 | 
					            const existingAttachment = existingAttachmentsByTitle.get(title);
 | 
				
			||||||
 | 
					            if (attachmentId || !existingAttachment) {
 | 
				
			||||||
                note.saveAttachment({ attachmentId, role, mime, title, content, position });
 | 
					                note.saveAttachment({ attachmentId, role, mime, title, content, position });
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                const existingAttachment = existingAttachmentsByTitle[title];
 | 
					 | 
				
			||||||
                existingAttachment.role = role;
 | 
					                existingAttachment.role = role;
 | 
				
			||||||
                existingAttachment.mime = mime;
 | 
					                existingAttachment.mime = mime;
 | 
				
			||||||
                existingAttachment.position = position;
 | 
					                existingAttachment.position = position;
 | 
				
			||||||
@@ -887,7 +887,7 @@ async function asyncPostProcessContent(note: BNote, content: string | Buffer) {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (note.hasStringContent() && !isString(content)) {
 | 
					    if (note.hasStringContent() && typeof content !== "string") {
 | 
				
			||||||
        content = content.toString();
 | 
					        content = content.toString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { describe, it, expect } from "vitest";
 | 
				
			||||||
 | 
					import { processMindmapContent } from "./note_content_fulltext.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("processMindmapContent", () => {
 | 
				
			||||||
 | 
					    it("supports empty JSON", () => {
 | 
				
			||||||
 | 
					        expect(processMindmapContent("{}")).toEqual("");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it("supports blank text / invalid JSON", () => {
 | 
				
			||||||
 | 
					        expect(processMindmapContent("")).toEqual("");
 | 
				
			||||||
 | 
					        expect(processMindmapContent(`{ "node": " }`)).toEqual("");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -131,52 +131,7 @@ class NoteContentFulltextExp extends Expression {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            content = content.replace(/ /g, " ");
 | 
					            content = content.replace(/ /g, " ");
 | 
				
			||||||
        } else if (type === "mindMap" && mime === "application/json") {
 | 
					        } else if (type === "mindMap" && mime === "application/json") {
 | 
				
			||||||
            let mindMapcontent = JSON.parse(content);
 | 
					            content = processMindmapContent(content);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Define interfaces for the JSON structure
 | 
					 | 
				
			||||||
            interface MindmapNode {
 | 
					 | 
				
			||||||
                id: string;
 | 
					 | 
				
			||||||
                topic: string;
 | 
					 | 
				
			||||||
                children: MindmapNode[]; // Recursive structure
 | 
					 | 
				
			||||||
                direction?: number;
 | 
					 | 
				
			||||||
                expanded?: boolean;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            interface MindmapData {
 | 
					 | 
				
			||||||
                nodedata: MindmapNode;
 | 
					 | 
				
			||||||
                arrows: any[]; // If you know the structure, replace `any` with the correct type
 | 
					 | 
				
			||||||
                summaries: any[];
 | 
					 | 
				
			||||||
                direction: number;
 | 
					 | 
				
			||||||
                theme: {
 | 
					 | 
				
			||||||
                    name: string;
 | 
					 | 
				
			||||||
                    type: string;
 | 
					 | 
				
			||||||
                    palette: string[];
 | 
					 | 
				
			||||||
                    cssvar: Record<string, string>; // Object with string keys and string values
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Recursive function to collect all topics
 | 
					 | 
				
			||||||
            function collectTopics(node: MindmapNode): string[] {
 | 
					 | 
				
			||||||
                // Collect the current node's topic
 | 
					 | 
				
			||||||
                let topics = [node.topic];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // If the node has children, collect topics recursively
 | 
					 | 
				
			||||||
                if (node.children && node.children.length > 0) {
 | 
					 | 
				
			||||||
                    for (const child of node.children) {
 | 
					 | 
				
			||||||
                        topics = topics.concat(collectTopics(child));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return topics;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Start extracting from the root node
 | 
					 | 
				
			||||||
            const topicsArray = collectTopics(mindMapcontent.nodedata);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Combine topics into a single string
 | 
					 | 
				
			||||||
            const topicsString = topicsArray.join(", ");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            content = normalize(topicsString.toString());
 | 
					 | 
				
			||||||
        } else if (type === "canvas" && mime === "application/json") {
 | 
					        } else if (type === "canvas" && mime === "application/json") {
 | 
				
			||||||
            interface Element {
 | 
					            interface Element {
 | 
				
			||||||
                type: string;
 | 
					                type: string;
 | 
				
			||||||
@@ -215,4 +170,63 @@ class NoteContentFulltextExp extends Expression {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function processMindmapContent(content: string) {
 | 
				
			||||||
 | 
					    let mindMapcontent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        mindMapcontent = JSON.parse(content);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					        return "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Define interfaces for the JSON structure
 | 
				
			||||||
 | 
					    interface MindmapNode {
 | 
				
			||||||
 | 
					        id: string;
 | 
				
			||||||
 | 
					        topic: string;
 | 
				
			||||||
 | 
					        children: MindmapNode[]; // Recursive structure
 | 
				
			||||||
 | 
					        direction?: number;
 | 
				
			||||||
 | 
					        expanded?: boolean;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    interface MindmapData {
 | 
				
			||||||
 | 
					        nodedata: MindmapNode;
 | 
				
			||||||
 | 
					        arrows: any[]; // If you know the structure, replace `any` with the correct type
 | 
				
			||||||
 | 
					        summaries: any[];
 | 
				
			||||||
 | 
					        direction: number;
 | 
				
			||||||
 | 
					        theme: {
 | 
				
			||||||
 | 
					            name: string;
 | 
				
			||||||
 | 
					            type: string;
 | 
				
			||||||
 | 
					            palette: string[];
 | 
				
			||||||
 | 
					            cssvar: Record<string, string>; // Object with string keys and string values
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Recursive function to collect all topics
 | 
				
			||||||
 | 
					    function collectTopics(node?: MindmapNode): string[] {
 | 
				
			||||||
 | 
					        if (!node) {
 | 
				
			||||||
 | 
					            return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Collect the current node's topic
 | 
				
			||||||
 | 
					        let topics = [node.topic];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // If the node has children, collect topics recursively
 | 
				
			||||||
 | 
					        if (node.children && node.children.length > 0) {
 | 
				
			||||||
 | 
					            for (const child of node.children) {
 | 
				
			||||||
 | 
					                topics = topics.concat(collectTopics(child));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return topics;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Start extracting from the root node
 | 
				
			||||||
 | 
					    const topicsArray = collectTopics(mindMapcontent.nodedata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Combine topics into a single string
 | 
				
			||||||
 | 
					    const topicsString = topicsArray.join(", ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return normalize(topicsString.toString());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default NoteContentFulltextExp;
 | 
					export default NoteContentFulltextExp;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,61 +0,0 @@
 | 
				
			|||||||
import { expect, describe, it } from "vitest";
 | 
					 | 
				
			||||||
import { formatDownloadTitle } from "./utils.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [
 | 
					 | 
				
			||||||
    // empty fileName tests
 | 
					 | 
				
			||||||
    [["", "text", ""], "untitled.html"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["", "canvas", ""], "untitled.json"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["", null, ""], "untitled"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // json extension from type tests
 | 
					 | 
				
			||||||
    [["test_file", "canvas", ""], "test_file.json"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file", "relationMap", ""], "test_file.json"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file", "search", ""], "test_file.json"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // extension based on mime type
 | 
					 | 
				
			||||||
    [["test_file", null, "text/csv"], "test_file.csv"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file_wo_ext", "image", "image/svg+xml"], "test_file_wo_ext.svg"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file_wo_ext", "file", "application/json"], "test_file_wo_ext.json"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file_w_fake_ext.ext", "image", "image/svg+xml"], "test_file_w_fake_ext.ext.svg"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file_w_correct_ext.svg", "image", "image/svg+xml"], "test_file_w_correct_ext.svg"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file_w_correct_ext.svgz", "image", "image/svg+xml"], "test_file_w_correct_ext.svgz"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file.zip", "file", "application/zip"], "test_file.zip"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file", "file", "application/zip"], "test_file.zip"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // application/octet-stream tests
 | 
					 | 
				
			||||||
    [["test_file", "file", "application/octet-stream"], "test_file"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file.zip", "file", "application/octet-stream"], "test_file.zip"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test_file.unknown", null, "application/octet-stream"], "test_file.unknown"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // sanitized filename tests
 | 
					 | 
				
			||||||
    [["test/file", null, "application/octet-stream"], "testfile"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [["test:file.zip", "file", "application/zip"], "testfile.zip"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [[":::", "file", "application/zip"], ".zip"],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [[":::a", "file", "application/zip"], "a.zip"]
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe("utils/formatDownloadTitle unit tests", () => {
 | 
					 | 
				
			||||||
    testCases.forEach((testCase) => {
 | 
					 | 
				
			||||||
        return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
 | 
					 | 
				
			||||||
            const [value, expected] = testCase;
 | 
					 | 
				
			||||||
            const actual = formatDownloadTitle(...value);
 | 
					 | 
				
			||||||
            expect(actual).toStrictEqual(expected);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
							
								
								
									
										630
									
								
								src/services/utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										630
									
								
								src/services/utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,630 @@
 | 
				
			|||||||
 | 
					import { describe, it, expect } from "vitest";
 | 
				
			||||||
 | 
					import utils from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#newEntityId", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it("should return a string with a length of 12", () => {
 | 
				
			||||||
 | 
					    const result = utils.newEntityId();
 | 
				
			||||||
 | 
					    expect(result).toBeTypeOf("string");
 | 
				
			||||||
 | 
					    expect(result).toHaveLength(12);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#randomString", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it("should return a string with a length as per argument", () => {
 | 
				
			||||||
 | 
					    const stringLength = 5;
 | 
				
			||||||
 | 
					    const result = utils.randomString(stringLength);
 | 
				
			||||||
 | 
					    expect(result).toBeTypeOf("string");
 | 
				
			||||||
 | 
					    expect(result).toHaveLength(stringLength);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TriliumNextTODO: should use mocks and assert that functions get called
 | 
				
			||||||
 | 
					describe("#randomSecureToken", () => {
 | 
				
			||||||
 | 
					    // base64 -> 4 * (bytes/3) length -> if padding and rounding up is ignored for simplicity
 | 
				
			||||||
 | 
					    // https://stackoverflow.com/a/13378842
 | 
				
			||||||
 | 
					    const byteToBase64Length = (bytes: number) => 4 * (bytes / 3);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it("should return a string and use 32 bytes by default", () => {
 | 
				
			||||||
 | 
					        const result = utils.randomSecureToken();
 | 
				
			||||||
 | 
					        expect(result).toBeTypeOf("string");
 | 
				
			||||||
 | 
					        expect(result.length).toBeGreaterThanOrEqual(byteToBase64Length(32));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it("should return a string and use passed byte length", () => {
 | 
				
			||||||
 | 
					        const bytes = 16;
 | 
				
			||||||
 | 
					        const result = utils.randomSecureToken(bytes);
 | 
				
			||||||
 | 
					        expect(result).toBeTypeOf("string");
 | 
				
			||||||
 | 
					        expect(result.length).toBeGreaterThanOrEqual(byteToBase64Length(bytes));
 | 
				
			||||||
 | 
					        expect(result.length).toBeLessThan(44); // default argument uses 32 bytes -> which translates to 44 base64 legal chars
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TriliumNextTODO: should use mocks and assert that functions get called
 | 
				
			||||||
 | 
					describe.todo("#md5", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TriliumNextTODO: should use mocks and assert that functions get called
 | 
				
			||||||
 | 
					describe.todo("#hashedBlobId", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TriliumNextTODO: should use mocks and assert that functions get called
 | 
				
			||||||
 | 
					describe.todo("#toBase64", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TriliumNextTODO: should use mocks and assert that functions get called
 | 
				
			||||||
 | 
					describe.todo("#fromBase64", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TriliumNextTODO: should use mocks and assert that functions get called
 | 
				
			||||||
 | 
					describe.todo("#hmac", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TriliumNextTODO: should use mocks and assert that functions get called
 | 
				
			||||||
 | 
					describe.todo("#hash", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#isEmptyOrWhitespace", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const testCases: TestCase<typeof utils.isEmptyOrWhitespace>[] = [
 | 
				
			||||||
 | 
					    ["w/ 'null' it should return true", [null], true],
 | 
				
			||||||
 | 
					    ["w/ 'null' it should return true", [null], true],
 | 
				
			||||||
 | 
					    ["w/ undefined it should return true", [undefined], true],
 | 
				
			||||||
 | 
					    ["w/ empty string '' it should return true", [""], true],
 | 
				
			||||||
 | 
					    ["w/ single whitespace string ' ' it should return true", [" "], true],
 | 
				
			||||||
 | 
					    ["w/ multiple whitespace string '   ' it should return true", ["  "], true],
 | 
				
			||||||
 | 
					    ["w/ non-empty string ' t  ' it should return false", [" t  "], false],
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					    const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					    it(desc, () => {
 | 
				
			||||||
 | 
					      const result = utils.isEmptyOrWhitespace(...fnParams);
 | 
				
			||||||
 | 
					      expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#sanitizeSqlIdentifier", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const testCases: TestCase<typeof utils.sanitizeSqlIdentifier>[] = [
 | 
				
			||||||
 | 
					    ["w/ 'test' it should not strip anything", ["test"], "test"],
 | 
				
			||||||
 | 
					    ["w/ 'test123' it should not strip anything", ["test123"], "test123"],
 | 
				
			||||||
 | 
					    ["w/ 'tEst_TeSt' it should not strip anything", ["tEst_TeSt"], "tEst_TeSt"],
 | 
				
			||||||
 | 
					    ["w/ 'test_test' it should not strip '_'", ["test_test"], "test_test"],
 | 
				
			||||||
 | 
					    ["w/ 'test-' it should strip the '-'", ["test-"], "test"],
 | 
				
			||||||
 | 
					    ["w/ 'test-test' it should strip the '-'", ["test-test"], "testtest"],
 | 
				
			||||||
 | 
					    ["w/ 'test; --test' it should strip the '; --'", ["test; --test"], "testtest"],
 | 
				
			||||||
 | 
					    ["w/ 'test test' it should strip the ' '", ["test test"], "testtest"],
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					    const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					    it(desc, () => {
 | 
				
			||||||
 | 
					      const result = utils.sanitizeSqlIdentifier(...fnParams);
 | 
				
			||||||
 | 
					      expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#escapeHtml", () => {
 | 
				
			||||||
 | 
					    it("should re-export 'escape-html' npm module as escapeHtml", () => {
 | 
				
			||||||
 | 
					        expect(utils.escapeHtml).toBeTypeOf("function");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#unescapeHtml", () => {
 | 
				
			||||||
 | 
					    it("should re-export 'unescape' npm module as unescapeHtml", () => {
 | 
				
			||||||
 | 
					        expect(utils.unescapeHtml).toBeTypeOf("function");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#toObject", () => {
 | 
				
			||||||
 | 
					    it("should return an object with keys and value being set from the supplied Function", () => {
 | 
				
			||||||
 | 
					        type TestListEntry = { testPropA: string, testPropB: string };
 | 
				
			||||||
 | 
					        type TestListFn = (testListEntry: TestListEntry) => [string, string];
 | 
				
			||||||
 | 
					        const testList: [TestListEntry, TestListEntry] = [{ testPropA: "keyA", testPropB: "valueA" }, { testPropA: "keyB", testPropB: "valueB" }];
 | 
				
			||||||
 | 
					        const fn: TestListFn = (testListEntry: TestListEntry) => [testListEntry.testPropA + "_fn", testListEntry.testPropB + "_fn"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const result = utils.toObject(testList, fn);
 | 
				
			||||||
 | 
					        expect(result).toStrictEqual({
 | 
				
			||||||
 | 
					            "keyA_fn": "valueA_fn",
 | 
				
			||||||
 | 
					            "keyB_fn": "valueB_fn"
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#stripTags", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //prettier-ignore
 | 
				
			||||||
 | 
					    const htmlWithNewlines =
 | 
				
			||||||
 | 
					`<p>abc
 | 
				
			||||||
 | 
					def</p>
 | 
				
			||||||
 | 
					<p>ghi</p>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof utils.stripTags>[] = [
 | 
				
			||||||
 | 
					        ["should strip all tags and only return the content, leaving new lines and spaces in tact", [htmlWithNewlines], "abc\ndef\nghi"],
 | 
				
			||||||
 | 
					        //TriliumNextTODO: should this actually insert a space between content to prevent concatenated text?
 | 
				
			||||||
 | 
					        ["should strip all tags and only return the content", ["<h1>abc</h1><p>def</p>"], "abcdef"],
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        it(desc, () => {
 | 
				
			||||||
 | 
					          const result = utils.stripTags(...fnParams);
 | 
				
			||||||
 | 
					          expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe.todo("#escapeRegExp", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe.todo("#crash", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#getContentDisposition", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const defaultFallBackDisposition = `file; filename="file"; filename*=UTF-8''file`;
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof utils.getContentDisposition>[] = [
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "when passed filename is empty, it should fallback to default value 'file'", 
 | 
				
			||||||
 | 
					            [" "],
 | 
				
			||||||
 | 
					            defaultFallBackDisposition
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "when passed filename '..' would cause sanitized filename to be empty, it should fallback to default value 'file'", 
 | 
				
			||||||
 | 
					            [".."],
 | 
				
			||||||
 | 
					            defaultFallBackDisposition
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        // COM1 is a Windows specific "illegal filename" that sanitize filename strips away
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "when passed filename 'COM1' would cause sanitized filename to be empty, it should fallback to default value 'file'",
 | 
				
			||||||
 | 
					            ["COM1"],
 | 
				
			||||||
 | 
					            defaultFallBackDisposition
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "sanitized passed filename should be returned URIEncoded",
 | 
				
			||||||
 | 
					            ["test file.csv"],
 | 
				
			||||||
 | 
					            `file; filename="test%20file.csv"; filename*=UTF-8''test%20file.csv`
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        it(desc, () => {
 | 
				
			||||||
 | 
					            const result = utils.getContentDisposition(...fnParams);
 | 
				
			||||||
 | 
					            expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#isStringNote", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof utils.isStringNote>[] = [
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ 'undefined' note type, but a string mime type, it should return true",
 | 
				
			||||||
 | 
					            [undefined, "application/javascript"],
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ non-string note type, it should return false",
 | 
				
			||||||
 | 
					            ["image", "image/jpeg"],
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ string note type (text), it should return true",
 | 
				
			||||||
 | 
					            ["text", "text/html"],
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ string note type (code), it should return true",
 | 
				
			||||||
 | 
					            ["code", "application/json"],
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ non-string note type (file), but string mime type, it should return true",
 | 
				
			||||||
 | 
					            ["file", "application/json"],
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ non-string note type (file), but mime type starting with 'text/', it should return true",
 | 
				
			||||||
 | 
					            ["file", "text/html"],
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        it(desc, () => {
 | 
				
			||||||
 | 
					            const result = utils.isStringNote(...fnParams);
 | 
				
			||||||
 | 
					            expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe.todo("#quoteRegex", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe.todo("#replaceAll", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#removeTextFileExtension", () => {
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof utils.removeTextFileExtension>[] = [
 | 
				
			||||||
 | 
					        ["w/ 'test.md' it should strip '.md'", ["test.md"], "test"],
 | 
				
			||||||
 | 
					        ["w/ 'test.markdown' it should strip '.markdown'", ["test.markdown"], "test"],
 | 
				
			||||||
 | 
					        ["w/ 'test.html' it should strip '.html'", ["test.html"], "test"],
 | 
				
			||||||
 | 
					        ["w/ 'test.htm' it should strip '.htm'", ["test.htm"], "test"],
 | 
				
			||||||
 | 
					        ["w/ 'test.zip' it should NOT strip '.zip'", ["test.zip"], "test.zip"],
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        it(desc, () => {
 | 
				
			||||||
 | 
					            const result = utils.removeTextFileExtension(...fnParams);
 | 
				
			||||||
 | 
					            expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#getNoteTitle", () => {
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof utils.getNoteTitle>[] = [
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when file has no spaces, and no special file extension, it should return the filename unaltered",
 | 
				
			||||||
 | 
					          ["test.json", true, undefined],
 | 
				
			||||||
 | 
					          "test.json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when replaceUnderscoresWithSpaces is false, it should keep the underscores in the title",
 | 
				
			||||||
 | 
					          ["test_file.json", false, undefined],
 | 
				
			||||||
 | 
					          "test_file.json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when replaceUnderscoresWithSpaces is true, it should replace the underscores in the title",
 | 
				
			||||||
 | 
					          ["test_file.json", true, undefined],
 | 
				
			||||||
 | 
					          "test file.json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when filePath ends with one of the extra handled endings (.md), it should strip the file extension from the title",
 | 
				
			||||||
 | 
					          ["test_file.md", false, undefined],
 | 
				
			||||||
 | 
					          "test_file"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when filePath ends with one of the extra handled endings (.md) and replaceUnderscoresWithSpaces is true, it should strip the file extension from the title and replace underscores",
 | 
				
			||||||
 | 
					          ["test_file.md", true, undefined],
 | 
				
			||||||
 | 
					          "test file"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when filepath contains a full path, it should only return the basename of the file",
 | 
				
			||||||
 | 
					          ["Trilium Demo/Scripting examples/Statistics/Most cloned notes/template.zip", true, undefined],
 | 
				
			||||||
 | 
					          "template.zip"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when filepath contains a full path and has extra handled ending (.html), it should only return the basename of the file and strip the file extension",
 | 
				
			||||||
 | 
					          ["Trilium Demo/Scripting examples/Statistics/Most cloned notes/template.html", true, undefined],
 | 
				
			||||||
 | 
					          "template"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when a noteMeta object is passed, it should use the title from the noteMeta, if present",
 | 
				
			||||||
 | 
					          //@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
 | 
				
			||||||
 | 
					          ["test_file.md", true, { title: "some other title"}],
 | 
				
			||||||
 | 
					          "some other title"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          "when a noteMeta object is passed, but the title prop is empty, it should try to handle the filename as if no noteMeta was passed",
 | 
				
			||||||
 | 
					          //@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
 | 
				
			||||||
 | 
					          ["test_file.md", true, { title: ""}],
 | 
				
			||||||
 | 
					          "test file"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "when a noteMeta object is passed, but the title prop is empty, it should try to handle the filename as if no noteMeta was passed",
 | 
				
			||||||
 | 
					            //@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
 | 
				
			||||||
 | 
					            ["test_file.json", false, { title: " "}],
 | 
				
			||||||
 | 
					            "test_file.json"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        it(desc, () => {
 | 
				
			||||||
 | 
					            const result = utils.getNoteTitle(...fnParams);
 | 
				
			||||||
 | 
					            expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#timeLimit", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it("when promise execution does NOT exceed timeout, it should resolve with promises' value", async () => {
 | 
				
			||||||
 | 
					        const resolvedValue = `resolved: ${new Date().toISOString()}`;
 | 
				
			||||||
 | 
					        const testPromise = new Promise((res, rej) => {
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                return res(resolvedValue);
 | 
				
			||||||
 | 
					            }, 200);
 | 
				
			||||||
 | 
					            //rej("rejected!");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await expect(utils.timeLimit(testPromise, 1_000)).resolves.toBe(resolvedValue);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it("when promise execution rejects within timeout, it should return the original promises' rejected value, not the custom set one", async () => {
 | 
				
			||||||
 | 
					        const rejectedValue = `rejected: ${new Date().toISOString()}`;
 | 
				
			||||||
 | 
					        const testPromise = new Promise((res, rej) => {
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                //return res("resolved");
 | 
				
			||||||
 | 
					                rej(rejectedValue);
 | 
				
			||||||
 | 
					            }, 100);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await expect(utils.timeLimit(testPromise, 200, "Custom Error")).rejects.toThrow(rejectedValue)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it("when promise execution exceeds the set timeout, and 'errorMessage' is NOT set, it should reject the promise and display default error message", async () => {
 | 
				
			||||||
 | 
					        const testPromise = new Promise((res, rej) => {
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                return res("resolved");
 | 
				
			||||||
 | 
					            }, 500);
 | 
				
			||||||
 | 
					            //rej("rejected!");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await expect(utils.timeLimit(testPromise, 200)).rejects.toThrow(`Process exceeded time limit 200`)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it("when promise execution exceeds the set timeout, and 'errorMessage' is set, it should reject the promise and display set error message", async () => {
 | 
				
			||||||
 | 
					        const customErrorMsg = "Custom Error";
 | 
				
			||||||
 | 
					        const testPromise = new Promise((res, rej) => {
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                return res("resolved");
 | 
				
			||||||
 | 
					            }, 500);
 | 
				
			||||||
 | 
					            //rej("rejected!");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await expect(utils.timeLimit(testPromise, 200, customErrorMsg)).rejects.toThrow(customErrorMsg)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
 | 
				
			||||||
 | 
					    it("when the passed promise is not a promise but 'undefined', it should return 'undefined'", async () => {
 | 
				
			||||||
 | 
					        //@ts-expect-error - passing in illegal type 'undefined'
 | 
				
			||||||
 | 
					        expect(utils.timeLimit(undefined, 200)).toBe(undefined)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
 | 
				
			||||||
 | 
					    it("when the passed promise is not a promise, it should return the passed value", async () => {
 | 
				
			||||||
 | 
					        //@ts-expect-error - passing in illegal type 'object'
 | 
				
			||||||
 | 
					        expect(utils.timeLimit({test: 1}, 200)).toStrictEqual({test: 1})
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#deferred", () => {
 | 
				
			||||||
 | 
					    it("should return a promise", () => {
 | 
				
			||||||
 | 
					        const result = utils.deferred();
 | 
				
			||||||
 | 
					        expect(result).toBeInstanceOf(Promise)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    // TriliumNextTODO: Add further tests!
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#removeDiacritic", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof utils.removeDiacritic>[] = [
 | 
				
			||||||
 | 
					        ["w/ 'Äpfel' it should replace the 'Ä'", ["Äpfel"], "Apfel"],
 | 
				
			||||||
 | 
					        ["w/ 'Été' it should replace the 'É' and 'é'", ["Été"], "Ete"],
 | 
				
			||||||
 | 
					        ["w/ 'Fête' it should replace the 'ê'", ["Fête"], "Fete"],
 | 
				
			||||||
 | 
					        ["w/ 'Αλφαβήτα' it should replace the 'ή'", ["Αλφαβήτα"], "Αλφαβητα"],
 | 
				
			||||||
 | 
					        ["w/ '' (empty string) it should return empty string", [""], ""],
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        it(desc, () => {
 | 
				
			||||||
 | 
					            const result = utils.removeDiacritic(...fnParams);
 | 
				
			||||||
 | 
					            expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#normalize", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof utils.normalize>[] = [
 | 
				
			||||||
 | 
					        ["w/ 'Äpfel' it should replace the 'Ä' and return lowercased", ["Äpfel"], "apfel"],
 | 
				
			||||||
 | 
					        ["w/ 'Été' it should replace the 'É' and 'é' and return lowercased", ["Été"], "ete"],
 | 
				
			||||||
 | 
					        ["w/ 'FêTe' it should replace the 'ê' and return lowercased", ["FêTe"], "fete"],
 | 
				
			||||||
 | 
					        ["w/ 'ΑλΦαβήΤα' it should replace the 'ή' and return lowercased", ["ΑλΦαβήΤα"], "αλφαβητα"],
 | 
				
			||||||
 | 
					        ["w/ '' (empty string) it should return empty string", [""], ""],
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        it(desc, () => {
 | 
				
			||||||
 | 
					            const result = utils.normalize(...fnParams);
 | 
				
			||||||
 | 
					            expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#toMap", () => {
 | 
				
			||||||
 | 
					    it("should return an instace of Map, with the correct size and keys, when supplied with a list and existing keys", () => {
 | 
				
			||||||
 | 
					        const testList = [{title: "test", propA: "text", propB: 123 }, {title: "test2", propA: "prop2", propB: 456 }];
 | 
				
			||||||
 | 
					        const result = utils.toMap(testList, "title");
 | 
				
			||||||
 | 
					        expect(result).toBeInstanceOf(Map);
 | 
				
			||||||
 | 
					        expect(result.size).toBe(2);
 | 
				
			||||||
 | 
					        expect(Array.from(result.keys())).toStrictEqual(["test", "test2"]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    it("should return an instace of Map, with an empty size, when the supplied list does not contain the supplied key", () => {
 | 
				
			||||||
 | 
					        const testList = [{title: "test", propA: "text", propB: 123 }, {title: "test2", propA: "prop2", propB: 456 }];
 | 
				
			||||||
 | 
					        //@ts-expect-error - key is non-existing on supplied list type
 | 
				
			||||||
 | 
					        const result = utils.toMap(testList, "nonExistingKey");
 | 
				
			||||||
 | 
					        expect(result).toBeInstanceOf(Map);
 | 
				
			||||||
 | 
					        expect(result.size).toBe(0);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    it.fails("should correctly handle duplicate keys? (currently it will overwrite the entry, so returned size will be 1 instead of 2)", () => {
 | 
				
			||||||
 | 
					        const testList = [{title: "testDupeTitle", propA: "text", propB: 123 }, {title: "testDupeTitle", propA: "prop2", propB: 456 }];
 | 
				
			||||||
 | 
					        const result = utils.toMap(testList, "title");
 | 
				
			||||||
 | 
					        expect(result).toBeInstanceOf(Map);
 | 
				
			||||||
 | 
					        expect(result.size).toBe(2);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#envToBoolean", () => {
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof utils.envToBoolean>[] = [
 | 
				
			||||||
 | 
					        ["w/ 'true' it should return boolean 'true'", ["true"], true],
 | 
				
			||||||
 | 
					        ["w/ 'True' it should return boolean 'true'", ["True"], true],
 | 
				
			||||||
 | 
					        ["w/ 'TRUE' it should return boolean 'true'", ["TRUE"], true],
 | 
				
			||||||
 | 
					        ["w/ 'true ' it should return boolean 'true'", ["true "], true],
 | 
				
			||||||
 | 
					        ["w/ 'false' it should return boolean 'false'", ["false"], false],
 | 
				
			||||||
 | 
					        ["w/ 'False' it should return boolean 'false'", ["False"], false],
 | 
				
			||||||
 | 
					        ["w/ 'FALSE' it should return boolean 'false'", ["FALSE"], false],
 | 
				
			||||||
 | 
					        ["w/ 'false ' it should return boolean 'false'", ["false "], false],
 | 
				
			||||||
 | 
					        ["w/ 'whatever' (non-boolean string) it should return undefined", ["whatever"], undefined],
 | 
				
			||||||
 | 
					        ["w/ '-' (non-boolean string) it should return undefined", ["-"], undefined],
 | 
				
			||||||
 | 
					        ["w/ '' (empty string) it should return undefined", [""], undefined],
 | 
				
			||||||
 | 
					        ["w/ ' ' (white space string) it should return undefined", [" "], undefined],
 | 
				
			||||||
 | 
					        ["w/ undefined it should return undefined", [undefined], undefined],
 | 
				
			||||||
 | 
					        //@ts-expect-error - pass wrong type as param
 | 
				
			||||||
 | 
					        ["w/ number 1 it should return undefined", [1], undefined],
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach(testCase => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        it(desc, () => {
 | 
				
			||||||
 | 
					            const result = utils.envToBoolean(...fnParams);
 | 
				
			||||||
 | 
					            expect(result).toStrictEqual(expected);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe.todo("#getResourceDir", () => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#isElectron", () => {
 | 
				
			||||||
 | 
					    it("should export a boolean", () => {
 | 
				
			||||||
 | 
					        expect(utils.isElectron).toBeTypeOf("boolean");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#isMac", () => {
 | 
				
			||||||
 | 
					    it("should export a boolean", () => {
 | 
				
			||||||
 | 
					        expect(utils.isMac).toBeTypeOf("boolean");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#isWindows", () => {
 | 
				
			||||||
 | 
					    it("should export a boolean", () => {
 | 
				
			||||||
 | 
					        expect(utils.isWindows).toBeTypeOf("boolean");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#isDev", () => {
 | 
				
			||||||
 | 
					    it("should export a boolean", () => {
 | 
				
			||||||
 | 
					        expect(utils.isDev).toBeTypeOf("boolean");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#formatDownloadTitle", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //prettier-ignore
 | 
				
			||||||
 | 
					    const testCases: [fnValue: Parameters<typeof utils.formatDownloadTitle>, expectedValue: ReturnType<typeof utils.formatDownloadTitle>][] = [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // empty fileName tests
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["", "text", ""],
 | 
				
			||||||
 | 
					            "untitled.html"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["", "canvas", ""],
 | 
				
			||||||
 | 
					            "untitled.json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["", null, ""],
 | 
				
			||||||
 | 
					            "untitled"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // json extension from type tests
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file", "canvas", ""],
 | 
				
			||||||
 | 
					            "test_file.json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file", "relationMap", ""],
 | 
				
			||||||
 | 
					            "test_file.json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file", "search", ""],
 | 
				
			||||||
 | 
					            "test_file.json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // extension based on mime type
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file", null, "text/csv"],
 | 
				
			||||||
 | 
					            "test_file.csv"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file_wo_ext", "image", "image/svg+xml"],
 | 
				
			||||||
 | 
					            "test_file_wo_ext.svg"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file_wo_ext", "file", "application/json"],
 | 
				
			||||||
 | 
					            "test_file_wo_ext.json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file_w_fake_ext.ext", "image", "image/svg+xml"],
 | 
				
			||||||
 | 
					            "test_file_w_fake_ext.ext.svg"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file_w_correct_ext.svg", "image", "image/svg+xml"],
 | 
				
			||||||
 | 
					            "test_file_w_correct_ext.svg"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file_w_correct_ext.svgz", "image", "image/svg+xml"],
 | 
				
			||||||
 | 
					            "test_file_w_correct_ext.svgz"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file.zip", "file", "application/zip"],
 | 
				
			||||||
 | 
					            "test_file.zip"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file", "file", "application/zip"],
 | 
				
			||||||
 | 
					            "test_file.zip"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // application/octet-stream tests
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file", "file", "application/octet-stream"],
 | 
				
			||||||
 | 
					            "test_file"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file.zip", "file", "application/octet-stream"],
 | 
				
			||||||
 | 
					            "test_file.zip"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test_file.unknown", null, "application/octet-stream"],
 | 
				
			||||||
 | 
					            "test_file.unknown"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // sanitized filename tests
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test/file", null, "application/octet-stream"],
 | 
				
			||||||
 | 
					            "testfile"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            ["test:file.zip", "file", "application/zip"],
 | 
				
			||||||
 | 
					            "testfile.zip"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            [":::", "file", "application/zip"],
 | 
				
			||||||
 | 
					            ".zip"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            [":::a", "file", "application/zip"],
 | 
				
			||||||
 | 
					            "a.zip"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach((testCase) => {
 | 
				
			||||||
 | 
					        const [fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        return it(`With args '${JSON.stringify(fnParams)}', it should return '${expected}'`, () => {
 | 
				
			||||||
 | 
					            const actual = utils.formatDownloadTitle(...fnParams);
 | 
				
			||||||
 | 
					            expect(actual).toStrictEqual(expected);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -9,6 +9,7 @@ import mimeTypes from "mime-types";
 | 
				
			|||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
import { fileURLToPath } from "url";
 | 
					import { fileURLToPath } from "url";
 | 
				
			||||||
import { dirname, join } from "path";
 | 
					import { dirname, join } from "path";
 | 
				
			||||||
 | 
					import type NoteMeta from "./meta/note_meta.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const randtoken = generator({ source: "crypto" });
 | 
					const randtoken = generator({ source: "crypto" });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,21 +72,18 @@ export function hash(text: string) {
 | 
				
			|||||||
    return crypto.createHash("sha1").update(text).digest("base64");
 | 
					    return crypto.createHash("sha1").update(text).digest("base64");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isEmptyOrWhitespace(str: string) {
 | 
					export function isEmptyOrWhitespace(str: string | null | undefined) {
 | 
				
			||||||
    return str === null || str.match(/^ *$/) !== null;
 | 
					    if (!str) return true;
 | 
				
			||||||
 | 
					    return str.match(/^ *$/) !== null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function sanitizeSqlIdentifier(str: string) {
 | 
					export function sanitizeSqlIdentifier(str: string) {
 | 
				
			||||||
    return str.replace(/[^A-Za-z0-9_]/g, "");
 | 
					    return str.replace(/[^A-Za-z0-9_]/g, "");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function escapeHtml(str: string) {
 | 
					export const escapeHtml = escape;
 | 
				
			||||||
    return escape(str);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function unescapeHtml(str: string) {
 | 
					export const unescapeHtml = unescape;
 | 
				
			||||||
    return unescape(str);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function toObject<T, K extends string | number | symbol, V>(array: T[], fn: (item: T) => [K, V]): Record<K, V> {
 | 
					export function toObject<T, K extends string | number | symbol, V>(array: T[], fn: (item: T) => [K, V]): Record<K, V> {
 | 
				
			||||||
    const obj: Record<K, V> = {} as Record<K, V>; // TODO: unsafe?
 | 
					    const obj: Record<K, V> = {} as Record<K, V>; // TODO: unsafe?
 | 
				
			||||||
@@ -103,29 +101,6 @@ export function stripTags(text: string) {
 | 
				
			|||||||
    return text.replace(/<(?:.|\n)*?>/gm, "");
 | 
					    return text.replace(/<(?:.|\n)*?>/gm, "");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function union<T extends string | number | symbol>(a: T[], b: T[]): T[] {
 | 
					 | 
				
			||||||
    const obj: Record<T, T> = {} as Record<T, T>; // TODO: unsafe?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (let i = a.length - 1; i >= 0; i--) {
 | 
					 | 
				
			||||||
        obj[a[i]] = a[i];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (let i = b.length - 1; i >= 0; i--) {
 | 
					 | 
				
			||||||
        obj[b[i]] = b[i];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const res: T[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const k in obj) {
 | 
					 | 
				
			||||||
        if (obj.hasOwnProperty(k)) {
 | 
					 | 
				
			||||||
            // <-- optional
 | 
					 | 
				
			||||||
            res.push(obj[k]);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return res;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function escapeRegExp(str: string) {
 | 
					export function escapeRegExp(str: string) {
 | 
				
			||||||
    return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
 | 
					    return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -138,27 +113,18 @@ export async function crash() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function sanitizeFilenameForHeader(filename: string) {
 | 
					 | 
				
			||||||
    let sanitizedFilename = sanitize(filename);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (sanitizedFilename.trim().length === 0) {
 | 
					 | 
				
			||||||
        sanitizedFilename = "file";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return encodeURIComponent(sanitizedFilename);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function getContentDisposition(filename: string) {
 | 
					export function getContentDisposition(filename: string) {
 | 
				
			||||||
    const sanitizedFilename = sanitizeFilenameForHeader(filename);
 | 
					    const sanitizedFilename = sanitize(filename).trim() || "file";
 | 
				
			||||||
 | 
					    const uriEncodedFilename = encodeURIComponent(sanitizedFilename);
 | 
				
			||||||
    return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`;
 | 
					    return `file; filename="${uriEncodedFilename}"; filename*=UTF-8''${uriEncodedFilename}`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// render and book are string note in the sense that they are expected to contain empty string
 | 
				
			||||||
 | 
					const STRING_NOTE_TYPES = new Set(["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"]);
 | 
				
			||||||
const STRING_MIME_TYPES = new Set(["application/javascript", "application/x-javascript", "application/json", "application/x-sql", "image/svg+xml"]);
 | 
					const STRING_MIME_TYPES = new Set(["application/javascript", "application/x-javascript", "application/json", "application/x-sql", "image/svg+xml"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isStringNote(type: string | undefined, mime: string) {
 | 
					export function isStringNote(type: string | undefined, mime: string) {
 | 
				
			||||||
    // render and book are string note in the sense that they are expected to contain empty string
 | 
					    return (type && STRING_NOTE_TYPES.has(type)) || mime.startsWith("text/") || STRING_MIME_TYPES.has(mime);
 | 
				
			||||||
    return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type)) || mime.startsWith("text/") || STRING_MIME_TYPES.has(mime);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function quoteRegex(url: string) {
 | 
					export function quoteRegex(url: string) {
 | 
				
			||||||
@@ -211,26 +177,23 @@ export function removeTextFileExtension(filePath: string) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: { title?: string }) {
 | 
					export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: NoteMeta) {
 | 
				
			||||||
    if (noteMeta?.title) {
 | 
					    const trimmedNoteMeta = noteMeta?.title?.trim();
 | 
				
			||||||
        return noteMeta.title;
 | 
					    if (trimmedNoteMeta) return trimmedNoteMeta;
 | 
				
			||||||
    } else {
 | 
					
 | 
				
			||||||
    const basename = path.basename(removeTextFileExtension(filePath));
 | 
					    const basename = path.basename(removeTextFileExtension(filePath));
 | 
				
			||||||
        if (replaceUnderscoresWithSpaces) {
 | 
					    return replaceUnderscoresWithSpaces ? basename.replace(/_/g, " ").trim() : basename;
 | 
				
			||||||
            return basename.replace(/_/g, " ").trim();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return basename;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string): Promise<T> {
 | 
					export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string): Promise<T> {
 | 
				
			||||||
 | 
					    // TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
 | 
				
			||||||
    if (!promise || !promise.then) {
 | 
					    if (!promise || !promise.then) {
 | 
				
			||||||
        // it's not actually a promise
 | 
					        // it's not actually a promise
 | 
				
			||||||
        return promise;
 | 
					        return promise;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // better stack trace if created outside of promise
 | 
					    // better stack trace if created outside of promise
 | 
				
			||||||
    const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
 | 
					    const errorTimeLimit = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new Promise((res, rej) => {
 | 
					    return new Promise((res, rej) => {
 | 
				
			||||||
        let resolved = false;
 | 
					        let resolved = false;
 | 
				
			||||||
@@ -245,7 +208,7 @@ export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        setTimeout(() => {
 | 
					        setTimeout(() => {
 | 
				
			||||||
            if (!resolved) {
 | 
					            if (!resolved) {
 | 
				
			||||||
                rej(error);
 | 
					                rej(errorTimeLimit);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }, limitMs);
 | 
					        }, limitMs);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -284,20 +247,18 @@ export function normalize(str: string) {
 | 
				
			|||||||
    return removeDiacritic(str).toLowerCase();
 | 
					    return removeDiacritic(str).toLowerCase();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function toMap<T extends Record<string, any>>(list: T[], key: keyof T): Record<string, T> {
 | 
					export function toMap<T extends Record<string, any>>(list: T[], key: keyof T) {
 | 
				
			||||||
    const map: Record<string, T> = {};
 | 
					    const map = new Map<string, T>();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const el of list) {
 | 
					    for (const el of list) {
 | 
				
			||||||
        map[el[key]] = el;
 | 
					        const keyForMap = el[key];
 | 
				
			||||||
 | 
					        if (!keyForMap) continue;
 | 
				
			||||||
 | 
					        // TriliumNextTODO: do we need to handle the case when the same key is used?
 | 
				
			||||||
 | 
					        // currently this will overwrite the existing entry in the map
 | 
				
			||||||
 | 
					        map.set(keyForMap, el);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return map;
 | 
					    return map;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isString(x: any) {
 | 
					 | 
				
			||||||
    return Object.prototype.toString.call(x) === "[object String]";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// try to turn 'true' and 'false' strings from process.env variables into boolean values or undefined
 | 
					// try to turn 'true' and 'false' strings from process.env variables into boolean values or undefined
 | 
				
			||||||
export function envToBoolean(val: string | undefined) {
 | 
					export function envToBoolean(val: string | undefined) {
 | 
				
			||||||
    if (val === undefined || typeof val !== "string") return undefined;
 | 
					    if (val === undefined || typeof val !== "string") return undefined;
 | 
				
			||||||
@@ -317,48 +278,87 @@ export function envToBoolean(val: string | undefined) {
 | 
				
			|||||||
 * @returns the resource dir.
 | 
					 * @returns the resource dir.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function getResourceDir() {
 | 
					export function getResourceDir() {
 | 
				
			||||||
    if (isElectron && !isDev) {
 | 
					    if (isElectron && !isDev) return process.resourcesPath;
 | 
				
			||||||
        return process.resourcesPath;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
    return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
 | 
					    return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: Deduplicate with src/public/app/services/utils.ts
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Compares two semantic version strings.
 | 
				
			||||||
 | 
					 * Returns:
 | 
				
			||||||
 | 
					 *   1  if v1 is greater than v2
 | 
				
			||||||
 | 
					 *   0  if v1 is equal to v2
 | 
				
			||||||
 | 
					 *   -1 if v1 is less than v2
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param v1 First version string
 | 
				
			||||||
 | 
					 * @param v2 Second version string
 | 
				
			||||||
 | 
					 * @returns
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function compareVersions(v1: string, v2: string): number {
 | 
				
			||||||
 | 
					    // Remove 'v' prefix and everything after dash if present
 | 
				
			||||||
 | 
					    v1 = v1.replace(/^v/, "").split("-")[0];
 | 
				
			||||||
 | 
					    v2 = v2.replace(/^v/, "").split("-")[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const v1parts = v1.split(".").map(Number);
 | 
				
			||||||
 | 
					    const v2parts = v2.split(".").map(Number);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Pad shorter version with zeros
 | 
				
			||||||
 | 
					    while (v1parts.length < 3) v1parts.push(0);
 | 
				
			||||||
 | 
					    while (v2parts.length < 3) v2parts.push(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Compare major version
 | 
				
			||||||
 | 
					    if (v1parts[0] !== v2parts[0]) {
 | 
				
			||||||
 | 
					        return v1parts[0] > v2parts[0] ? 1 : -1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Compare minor version
 | 
				
			||||||
 | 
					    if (v1parts[1] !== v2parts[1]) {
 | 
				
			||||||
 | 
					        return v1parts[1] > v2parts[1] ? 1 : -1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Compare patch version
 | 
				
			||||||
 | 
					    if (v1parts[2] !== v2parts[2]) {
 | 
				
			||||||
 | 
					        return v1parts[2] > v2parts[2] ? 1 : -1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    randomSecureToken,
 | 
					    compareVersions,
 | 
				
			||||||
    randomString,
 | 
					    crash,
 | 
				
			||||||
 | 
					    deferred,
 | 
				
			||||||
 | 
					    envToBoolean,
 | 
				
			||||||
 | 
					    escapeHtml,
 | 
				
			||||||
 | 
					    escapeRegExp,
 | 
				
			||||||
 | 
					    formatDownloadTitle,
 | 
				
			||||||
 | 
					    fromBase64,
 | 
				
			||||||
 | 
					    getContentDisposition,
 | 
				
			||||||
 | 
					    getNoteTitle,
 | 
				
			||||||
 | 
					    getResourceDir,
 | 
				
			||||||
 | 
					    hash,
 | 
				
			||||||
 | 
					    hashedBlobId,
 | 
				
			||||||
 | 
					    hmac,
 | 
				
			||||||
 | 
					    isDev,
 | 
				
			||||||
 | 
					    isElectron,
 | 
				
			||||||
 | 
					    isEmptyOrWhitespace,
 | 
				
			||||||
 | 
					    isMac,
 | 
				
			||||||
 | 
					    isStringNote,
 | 
				
			||||||
 | 
					    isWindows,
 | 
				
			||||||
    md5,
 | 
					    md5,
 | 
				
			||||||
    newEntityId,
 | 
					    newEntityId,
 | 
				
			||||||
    toBase64,
 | 
					 | 
				
			||||||
    fromBase64,
 | 
					 | 
				
			||||||
    hmac,
 | 
					 | 
				
			||||||
    isElectron,
 | 
					 | 
				
			||||||
    hash,
 | 
					 | 
				
			||||||
    isEmptyOrWhitespace,
 | 
					 | 
				
			||||||
    sanitizeSqlIdentifier,
 | 
					 | 
				
			||||||
    escapeHtml,
 | 
					 | 
				
			||||||
    unescapeHtml,
 | 
					 | 
				
			||||||
    toObject,
 | 
					 | 
				
			||||||
    stripTags,
 | 
					 | 
				
			||||||
    union,
 | 
					 | 
				
			||||||
    escapeRegExp,
 | 
					 | 
				
			||||||
    crash,
 | 
					 | 
				
			||||||
    getContentDisposition,
 | 
					 | 
				
			||||||
    isStringNote,
 | 
					 | 
				
			||||||
    quoteRegex,
 | 
					 | 
				
			||||||
    replaceAll,
 | 
					 | 
				
			||||||
    getNoteTitle,
 | 
					 | 
				
			||||||
    removeTextFileExtension,
 | 
					 | 
				
			||||||
    formatDownloadTitle,
 | 
					 | 
				
			||||||
    timeLimit,
 | 
					 | 
				
			||||||
    deferred,
 | 
					 | 
				
			||||||
    removeDiacritic,
 | 
					 | 
				
			||||||
    normalize,
 | 
					    normalize,
 | 
				
			||||||
    hashedBlobId,
 | 
					    quoteRegex,
 | 
				
			||||||
 | 
					    randomSecureToken,
 | 
				
			||||||
 | 
					    randomString,
 | 
				
			||||||
 | 
					    removeDiacritic,
 | 
				
			||||||
 | 
					    removeTextFileExtension,
 | 
				
			||||||
 | 
					    replaceAll,
 | 
				
			||||||
 | 
					    sanitizeSqlIdentifier,
 | 
				
			||||||
 | 
					    stripTags,
 | 
				
			||||||
 | 
					    timeLimit,
 | 
				
			||||||
 | 
					    toBase64,
 | 
				
			||||||
    toMap,
 | 
					    toMap,
 | 
				
			||||||
    isString,
 | 
					    toObject,
 | 
				
			||||||
    getResourceDir,
 | 
					    unescapeHtml
 | 
				
			||||||
    isMac,
 | 
					 | 
				
			||||||
    isWindows,
 | 
					 | 
				
			||||||
    envToBoolean
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,16 +57,24 @@
 | 
				
			|||||||
            <form data-bind="submit: selectSetupType">
 | 
					            <form data-bind="submit: selectSetupType">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="radio" style="margin-bottom: 15px;">
 | 
					            <div class="radio" style="margin-bottom: 15px;">
 | 
				
			||||||
                  <label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType"">
 | 
					                <label class="tn-radio">
 | 
				
			||||||
                      <%= t("setup.new-document") %></label>
 | 
					                    <input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
 | 
				
			||||||
 | 
					                    <%= t("setup.new-document") %>
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="radio" style="margin-bottom: 15px;">
 | 
					            <div class="radio" style="margin-bottom: 15px;">
 | 
				
			||||||
                  <label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
 | 
					                <label class="tn-radio">
 | 
				
			||||||
                      <%= t("setup.sync-from-desktop") %></label>
 | 
					                    <input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
 | 
				
			||||||
 | 
					                    <%= t("setup.sync-from-desktop") %>
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="radio" style="margin-bottom: 15px;">
 | 
					            <div class="radio" style="margin-bottom: 15px;">
 | 
				
			||||||
                  <label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType"">
 | 
					                <label class="tn-radio">
 | 
				
			||||||
                      <%= t("setup.sync-from-server") %></label>
 | 
					                    <input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
 | 
				
			||||||
 | 
					                    <%= t("setup.sync-from-server") %>
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <button type="submit" data-bind="disable: !setupTypeSelected()" class="btn btn-primary"><%= t("setup.next") %></button>
 | 
					              <button type="submit" data-bind="disable: !setupTypeSelected()" class="btn btn-primary"><%= t("setup.next") %></button>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								src/www.ts
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/www.ts
									
									
									
									
									
								
							@@ -12,7 +12,8 @@ import ws from "./services/ws.js";
 | 
				
			|||||||
import utils from "./services/utils.js";
 | 
					import utils from "./services/utils.js";
 | 
				
			||||||
import port from "./services/port.js";
 | 
					import port from "./services/port.js";
 | 
				
			||||||
import host from "./services/host.js";
 | 
					import host from "./services/host.js";
 | 
				
			||||||
import semver from "semver";
 | 
					
 | 
				
			||||||
 | 
					const MINIMUM_NODE_VERSION = "20.0.0";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// setup basic error handling even before requiring dependencies, since those can produce errors as well
 | 
					// setup basic error handling even before requiring dependencies, since those can produce errors as well
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,8 +33,12 @@ function exit() {
 | 
				
			|||||||
process.on("SIGINT", exit);
 | 
					process.on("SIGINT", exit);
 | 
				
			||||||
process.on("SIGTERM", exit);
 | 
					process.on("SIGTERM", exit);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (!semver.satisfies(process.version, ">=10.5.0")) {
 | 
					if (utils.compareVersions(process.versions.node, MINIMUM_NODE_VERSION) < 0) {
 | 
				
			||||||
    console.error("Trilium only supports node.js 10.5 and later");
 | 
					    console.error();
 | 
				
			||||||
 | 
					    console.error(`The Trilium server requires Node.js ${MINIMUM_NODE_VERSION} and later in order to start.\n`);
 | 
				
			||||||
 | 
					    console.error(`\tCurrent version:\t${process.versions.node}`);
 | 
				
			||||||
 | 
					    console.error(`\tExpected version:\t${MINIMUM_NODE_VERSION}`);
 | 
				
			||||||
 | 
					    console.error();
 | 
				
			||||||
    process.exit(1);
 | 
					    process.exit(1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user