diff --git a/.editorconfig b/.editorconfig index c0aba9b74..c965ea8c0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,9 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true +[*.sh] +end_of_line = lf + [{server,translation}.json] charset = utf-8 end_of_line = lf diff --git a/.env b/.env new file mode 100644 index 000000000..ff859e622 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NODE_OPTIONS=--max_old_space_size=4096 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index e9d640721..7b00e7d63 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,17 +1,21 @@ +# Mark files as auto-generated to simplify reviews. package-lock.json linguist-generated=true **/package-lock.json linguist-generated=true +apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/** linguist-generated -apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/** linguist-generated=true +# Ignore from GitHub language stats. apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/**/*.html eol=lf +apps/server/src/assets/doc_notes/** linguist-vendored=true +apps/edit-docs/demo/** linguist-vendored=true +docs/** linguist-vendored=true +# Normalize line endings. docs/**/*.md eol=lf docs/**/*.json eol=lf - demo/**/*.html eol=lf demo/**/*.json eol=lf demo/**/*.svg eol=lf demo/**/*.txt eol=lf demo/**/*.js eol=lf demo/**/*.css eol=lf - -apps/client/src/libraries/** linguist-vendored \ No newline at end of file +*.sh eol=lf diff --git a/.github/actions/build-electron/action.yml b/.github/actions/build-electron/action.yml index 9e5b1670b..93772d7d9 100644 --- a/.github/actions/build-electron/action.yml +++ b/.github/actions/build-electron/action.yml @@ -85,7 +85,7 @@ runs: APPLE_ID: ${{ env.APPLE_ID }} APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }} WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }} - TRILIUM_ARTIFACT_NAME_HINT: TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }} + TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }} run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }} # Add DMG signing step diff --git a/.github/actions/build-server/action.yml b/.github/actions/build-server/action.yml index c3c6288bc..b0ab05212 100644 --- a/.github/actions/build-server/action.yml +++ b/.github/actions/build-server/action.yml @@ -30,4 +30,4 @@ runs: mkdir -p upload file=$(find ./apps/server/out -name '*.tar.xz' -print -quit) name=${{ github.ref_name }} - cp "$file" "upload/TriliumNextNotes-Server-${name//\//-}-${{ inputs.os }}-${{ inputs.arch }}.tar.xz" + cp "$file" "upload/TriliumNotes-Server-${name//\//-}-${{ inputs.os }}-${{ inputs.arch }}.tar.xz" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 25cd18724..3a241d807 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,9 +13,9 @@ name: "CodeQL Advanced" on: push: - branches: [ "develop" ] + branches: [ "main" ] pull_request: - branches: [ "develop" ] + branches: [ "main" ] schedule: - cron: '20 7 * * 0' diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 6497ebb96..e9d3964c6 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,9 +1,9 @@ name: Dev on: push: - branches: [ develop ] + branches: [ main ] pull_request: - branches: [ develop ] + branches: [ main ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -12,8 +12,8 @@ concurrency: env: GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io - IMAGE_NAME: ${{ github.repository_owner }}/notes - TEST_TAG: ${{ github.repository_owner }}/notes:test + IMAGE_NAME: ${{ github.repository}} + TEST_TAG: ${{ github.repository}}:test permissions: pull-requests: write # for PR comments @@ -39,76 +39,7 @@ jobs: - uses: nrwl/nx-set-shas@v4 - name: Check affected - run: pnpm nx affected -t build rebuild-deps - - report-electron-size: - name: Report Electron size - runs-on: ubuntu-latest - needs: - - check-affected - steps: - - name: Checkout the repository - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - name: Set up node & dependencies - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'pnpm' - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run the build - uses: ./.github/actions/build-electron - with: - os: linux - arch: x64 - shell: bash - forge_platform: linux - - - name: Run the Electron size report - uses: ./.github/actions/report-size - with: - paths: 'upload/**/*' - onlyDiff: 'true' - branch: 'develop' - header: 'Electron size report' - unit: "MB" - ghToken: ${{ secrets.GITHUB_TOKEN }} - report-server-size: - name: Report server size - runs-on: ubuntu-latest - needs: - - check-affected - steps: - - name: Checkout the repository - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - name: Set up node & dependencies - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: "pnpm" - - - run: pnpm install --frozen-lockfile - - - name: Run the build - uses: ./.github/actions/build-server - with: - os: linux - arch: x64 - - - name: Run the server size report - uses: ./.github/actions/report-size - with: - paths: 'upload/**/*' - onlyDiff: 'true' - branch: 'develop' - header: 'Server size report' - unit: "MB" - ghToken: ${{ secrets.GITHUB_TOKEN }} + run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build test_dev: name: Test development @@ -128,7 +59,7 @@ jobs: - run: pnpm install --frozen-lockfile - name: Run the unit tests - run: pnpm run test + run: pnpm run test:all build_docker: name: Build Docker image @@ -143,7 +74,15 @@ jobs: run: pnpm install --frozen-lockfile - name: Update build info run: pnpm run chore:update-build-info - - name: Trigger build + - name: Trigger client build + run: pnpm nx run client:build + - name: Send client bundle stats to RelativeCI + if: false + uses: relative-ci/agent-action@v3 + with: + webpackStatsFile: ./apps/client/dist/webpack-stats.json + key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }} + - name: Trigger server build run: pnpm nx run server:build - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v6 diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index 84bea09e2..40c5149c7 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -1,7 +1,7 @@ on: push: branches: - - "develop" + - "main" - "feature/update**" - "feature/server_esm**" paths-ignore: @@ -14,8 +14,8 @@ on: env: GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io - IMAGE_NAME: ${{ github.repository_owner }}/notes - TEST_TAG: ${{ github.repository_owner }}/notes:test + IMAGE_NAME: ${{ github.repository}} + TEST_TAG: ${{ github.repository}}:test permissions: contents: read @@ -53,7 +53,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Install Playwright Browsers - run: npx playwright install --with-deps + run: pnpm exec playwright install --with-deps - name: Run the TypeScript build run: pnpm run server:build @@ -62,7 +62,7 @@ jobs: uses: docker/build-push-action@v6 with: context: apps/server - file: ${{ matrix.dockerfile }} + file: apps/server/${{ matrix.dockerfile }} load: true tags: ${{ env.TEST_TAG }} cache-from: type=gha @@ -70,7 +70,7 @@ jobs: - name: Validate container run output run: | - CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host -e TRILIUM_PORT=8082 --volume ./integration-tests/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }}) + CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host -e TRILIUM_PORT=8082 --volume ./apps/server/spec/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }}) echo "Container ID: $CONTAINER_ID" - name: Wait for the healthchecks to pass @@ -82,7 +82,15 @@ jobs: require-healthy: true - name: Run Playwright tests - run: TRILIUM_DOCKER=1 npx playwright test + run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e + + - name: Upload Playwright trace + if: failure() + uses: actions/upload-artifact@v4 + with: + name: Playwright trace (${{ matrix.dockerfile }}) + path: test-output/playwright/output + - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: @@ -111,6 +119,9 @@ jobs: - dockerfile: Dockerfile platform: linux/arm/v7 image: ubuntu-24.04-arm + - dockerfile: Dockerfile + platform: linux/arm/v8 + image: ubuntu-24.04-arm runs-on: ${{ matrix.image }} needs: - test_docker @@ -129,7 +140,6 @@ jobs: - name: Set TEST_TAG to lowercase run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV - - name: Checkout repository uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -142,6 +152,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Run the TypeScript build + run: pnpm run server:build + - name: Update build info run: pnpm run chore:update-build-info @@ -184,7 +197,7 @@ jobs: uses: docker/build-push-action@v6 with: context: apps/server - file: ${{ matrix.dockerfile }} + file: apps/server/${{ matrix.dockerfile }} platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 248e32538..990c8dfb9 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -11,7 +11,8 @@ on: pull_request: paths: - .github/actions/build-electron/* - - forge.config.cjs + - .github/workflows/nightly.yml + - forge.config.ts concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -37,7 +38,7 @@ jobs: shell: bash forge_platform: darwin - name: linux - image: ubuntu-latest + image: ubuntu-22.04 shell: bash forge_platform: linux - name: windows @@ -76,7 +77,7 @@ jobs: WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} - name: Publish release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.3.2 if: ${{ github.event_name != 'pull_request' }} with: make_latest: false @@ -91,7 +92,7 @@ jobs: uses: actions/upload-artifact@v4 if: ${{ github.event_name == 'pull_request' }} with: - name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }} + name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }} path: apps/desktop/upload nightly-server: @@ -102,7 +103,7 @@ jobs: arch: [x64, arm64] include: - arch: x64 - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 - arch: arm64 runs-on: ubuntu-24.04-arm runs-on: ${{ matrix.runs-on }} @@ -116,7 +117,7 @@ jobs: arch: ${{ matrix.arch }} - name: Publish release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.3.2 if: ${{ github.event_name != 'pull_request' }} with: make_latest: false diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 47d8e57e3..3749f1efd 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -3,7 +3,7 @@ name: playwright on: push: branches: - - master + - main pull_request: permissions: @@ -33,11 +33,11 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - run: npx playwright install --with-deps + - run: pnpm exec playwright install --with-deps - uses: nrwl/nx-set-shas@v4 # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud # - run: npx nx-cloud record -- echo Hello World # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected # When you enable task distribution, run the e2e-ci task instead of e2e - - run: npx nx affected -t e2e + - run: pnpm exec nx affected -t e2e --exclude desktop-e2e diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f571d634d..1d8dcd453 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: shell: bash forge_platform: darwin - name: linux - image: ubuntu-latest + image: ubuntu-22.04 shell: bash forge_platform: linux - name: windows @@ -73,7 +73,7 @@ jobs: arch: [x64, arm64] include: - arch: x64 - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 - arch: arm64 runs-on: ubuntu-24.04-arm runs-on: ${{ matrix.runs-on }} @@ -114,7 +114,7 @@ jobs: path: upload - name: Publish stable release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.3.2 with: draft: false body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md @@ -122,5 +122,5 @@ jobs: files: upload/*.* discussion_category_name: Announcements make_latest: ${{ !contains(github.ref, 'rc') }} - prerelease: ${{ !contains(github.ref, 'rc') }} + prerelease: ${{ contains(github.ref, 'rc') }} token: ${{ secrets.RELEASE_PAT }} diff --git a/.gitignore b/.gitignore index cf2fa89dd..d7694258d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ apps/*/out upload .rollup.cache -*.tsbuildinfo \ No newline at end of file +*.tsbuildinfo + +/result +.svelte-kit \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..fcc226927 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.17.0 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 13e5a892d..e64c42352 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -9,6 +9,8 @@ "redhat.vscode-yaml", "tobermory.es6-string-html", "vitest.explorer", - "yzhang.markdown-all-in-one" + "yzhang.markdown-all-in-one", + "svelte.svelte-vscode", + "bradlc.vscode-tailwindcss" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 60b3455da..5a2764983 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,5 +24,9 @@ }, "github-actions.workflows.pinned.workflows": [ ".github/workflows/nightly.yml" - ] + ], + "typescript.validate.enable": true, + "typescript.tsserver.experimental.enableProjectDiagnostics": true, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true } \ No newline at end of file diff --git a/README.md b/README.md index 496ce506b..11391c89e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,54 @@ -# TriliumNext Notes +# Trilium Notes -![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran) ![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total) +![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran?style=flat-square) +![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes?style=flat-square) +![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total?style=flat-square) +[![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop&style=flat-square)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md) -TriliumNext Notes is an open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. +Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview: Trilium Screenshot +## 🎁 Features + +* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes)) +* Rich WYSIWYG note editor including e.g. tables, images and [math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown [autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat) +* Support for editing [notes with source code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax highlighting +* Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting) +* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions) +* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts) +* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional) +* Direct [OpenID and TOTP integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md) for more secure login +* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server + * there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting) +* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet +* Strong [note encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with per-note granularity +* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type "canvas") +* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing notes and their relations +* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/) +* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with location pins and GPX tracks +* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases) +* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation +* Scales well in both usability and performance upwards of 100 000 notes +* Touch optimized [mobile frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for smartphones and tablets +* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support for user themes +* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown) +* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content +* Customizable UI (sidebar buttons, user-defined widgets, ...) +* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along with a [Grafana Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json) + +✨ Check out the following third-party resources/communities for more TriliumNext related goodies: + +- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more. +- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more. + ## ⚠️ Why TriliumNext? -[The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620) +[The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620). ### Migrating from Trilium? @@ -20,53 +56,49 @@ There are no special migration steps to migrate from a zadam/Trilium instance to Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented. +## 📖 Documentation + +We're currently in the progress of moving the documentation to in-app (hit the `F1` key within Trilium). As a result, there may be some missing parts until we've completed the migration. If you'd prefer to navigate through the documentation within GitHub, you can navigate the [User Guide](./docs/User%20Guide/User%20Guide/) documentation. + +Below are some quick links for your convenience to navigate the documentation: +- [Server installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md) + - [Docker installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md) +- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md) +- [Concepts and Features - Note](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md) +- [Patterns of personal knowledge base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge) + +Until we finish reorganizing the documentation, you may also want to [browse the old documentation](https://triliumnext.github.io/Docs). + ## 💬 Discuss with us Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have! -- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions) +- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.) - The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join) -- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For Asynchronous discussions) -- [Wiki](https://triliumnext.github.io/Docs/) (For common how-to questions and user guides) - -## 🎁 Features - -* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes)) -* Rich WYSIWYG note editing including e.g. tables, images and [math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown [autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat) -* Support for editing [notes with source code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax highlighting -* Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting) -* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions) -* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts) -* Direct OpenID and TOTP integration for more secure login -* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server - * there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting) -* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet -* Strong [note encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with per-note granularity -* Sketching diagrams with built-in Excalidraw (note type "canvas") -* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing notes and their relations -* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases) -* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation -* Scales well in both usability and performance upwards of 100 000 notes -* Touch optimized [mobile frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for smartphones and tablets -* [Night theme](https://triliumnext.github.io/Docs/Wiki/themes) -* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown) -* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content - -✨ Check out the following third-party resources/communities for more TriliumNext related goodies: - -- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more. -- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more. +- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.) +- [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.) ## 🏗 Installation -### Desktop +### Windows / MacOS -To use TriliumNext on your desktop machine (Linux, MacOS, and Windows) you have a few options: +Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable. -* Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the ```trilium``` executable. -* Access TriliumNext via the web interface of a server installation (see below) - * Currently only the latest versions of Chrome & Firefox are supported (and tested). -* TriliumNext is also provided as a Flatpak, but not yet published on FlatHub. +### Linux + +If your distribution is listed in the table below, use your distribution's package. + +[![Packaging status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions) + +You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable. + +TriliumNext is also provided as a Flatpak, but not yet published on FlatHub. + +### Browser (any OS) + +If you use a server installation (see below), you can directly access the web interface (which is almost identical to the desktop app). + +Currently only the latest versions of Chrome & Firefox are supported (and tested). ### Mobile @@ -80,33 +112,48 @@ See issue https://github.com/TriliumNext/Notes/issues/72 for more information on To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). -## 📝 Documentation - -[See wiki for complete list of documentation pages.](https://triliumnext.github.io/Docs) - -You can also read [Patterns of personal knowledge base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge) to get some inspiration on how you might use TriliumNext. ## 💻 Contribute ### Code +Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080): ```shell git clone https://github.com/TriliumNext/Notes.git cd Notes -npm install -npm run server:start +pnpm install +pnpm run server:start +``` + +### Documentation + +Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation: +```shell +git clone https://github.com/TriliumNext/Notes.git +cd Notes +pnpm install +pnpm nx run edit-docs:edit-docs +``` + +### Building the Executable +Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows: +```shell +git clone https://github.com/TriliumNext/Notes.git +cd Notes +pnpm install +pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32 ``` For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md). -### Documentation +### Developer Documentation -See the [documentation guide](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Documentation.md) for details. +Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above. ## 👏 Shoutouts * [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team -* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. TriliumNext Notes would not be the same without it. +* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it. * [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages * [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map) @@ -119,4 +166,6 @@ Support for the TriliumNext organization will be possible in the near future. Fo ## 🔑 License +Copyright 2017-2025 zadam, Elian Doran, and other contributors + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/_regroup/bin/build-docker.sh b/_regroup/bin/build-docker.sh deleted file mode 100644 index d95c289d4..000000000 --- a/_regroup/bin/build-docker.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e # Fail on any command error - -VERSION=`jq -r ".version" package.json` -SERIES=${VERSION:0:4}-latest - -sudo docker build -t triliumnext/notes:$VERSION --network host -t triliumnext/notes:$SERIES . - -if [[ $VERSION != *"beta"* ]]; then - sudo docker tag triliumnext/notes:$VERSION triliumnext/notes:latest -fi diff --git a/_regroup/bin/release-flatpack.sh b/_regroup/bin/release-flatpack.sh index f28ff7adb..31e42881b 100644 --- a/_regroup/bin/release-flatpack.sh +++ b/_regroup/bin/release-flatpack.sh @@ -24,7 +24,7 @@ if ! git diff-index --quiet HEAD --; then exit 1 fi -BASE_BRANCH=master +BASE_BRANCH=main if [[ "$VERSION" == *"beta"* ]]; then BASE_BRANCH=beta diff --git a/_regroup/bin/release.sh b/_regroup/bin/release.sh index db3c62e4b..fe9a65a36 100644 --- a/_regroup/bin/release.sh +++ b/_regroup/bin/release.sh @@ -47,11 +47,3 @@ echo "Tagging commit with $TAG" git tag $TAG git push origin $TAG - -echo "Updating master" - -git fetch -git checkout master -git reset --hard origin/master -git merge origin/develop -git push \ No newline at end of file diff --git a/_regroup/bin/translation.sh b/_regroup/bin/translation.sh index 4375303b9..15d211ea7 100644 --- a/_regroup/bin/translation.sh +++ b/_regroup/bin/translation.sh @@ -25,15 +25,16 @@ stats() { # Print the number of existing strings on the JSON files for each locale s=$(number_of_keys "${paths[0]}/en/server.json") c=$(number_of_keys "${paths[1]}/en/translation.json") - echo "| locale |server strings |client strings |" - echo "|--------|---------------|---------------|" - echo "| en | ${s} | ${c} |" + echo "| locale | server strings | client strings |" + echo "|--------|----------------|----------------|" + echo "| en | ${s} | ${c} |" + echo "|--------|----------------|----------------|" for locale in "${locales[@]}"; do s=$(number_of_keys "${paths[0]}/${locale}/server.json") c=$(number_of_keys "${paths[1]}/${locale}/translation.json") n1=$(((8 - ${#locale}) / 2)) n2=$((n1 == 1 ? n1 + 1 : n1)) - echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |" + echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |" done } @@ -78,7 +79,10 @@ file_path="$( cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit pwd -P )" -paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/") +paths=( + "${file_path}/../../apps/server/src/assets/translations/" + "${file_path}/../../apps/client/src/translations/" +) locales=(cn de es fr pt_br ro tw) if [ $# -eq 1 ]; then diff --git a/_regroup/ckeditor5-build-trilium/ckeditor-content.css b/_regroup/ckeditor5-build-trilium/ckeditor-content.css deleted file mode 100644 index 94d440047..000000000 --- a/_regroup/ckeditor5-build-trilium/ckeditor-content.css +++ /dev/null @@ -1,548 +0,0 @@ -/* - * CKEditor 5 (v41.0.0) content styles. - * Generated on Fri, 26 Jan 2024 10:23:49 GMT. - * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html - */ - -:root { - --ck-color-image-caption-background: hsl(0, 0%, 97%); - --ck-color-image-caption-text: hsl(0, 0%, 20%); - --ck-color-mention-background: hsla(341, 100%, 30%, 0.1); - --ck-color-mention-text: hsl(341, 100%, 30%); - --ck-color-selector-caption-background: hsl(0, 0%, 97%); - --ck-color-selector-caption-text: hsl(0, 0%, 20%); - --ck-highlight-marker-blue: hsl(201, 97%, 72%); - --ck-highlight-marker-green: hsl(120, 93%, 68%); - --ck-highlight-marker-pink: hsl(345, 96%, 73%); - --ck-highlight-marker-yellow: hsl(60, 97%, 73%); - --ck-highlight-pen-green: hsl(112, 100%, 27%); - --ck-highlight-pen-red: hsl(0, 85%, 49%); - --ck-image-style-spacing: 1.5em; - --ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2); - --ck-todo-list-checkmark-size: 16px; -} - -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table .ck-table-resized { - table-layout: fixed; -} -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table table { - overflow: hidden; -} -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table td, -.ck-content .table th { - overflow-wrap: break-word; - position: relative; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table { - margin: 0.9em auto; - display: table; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - height: 100%; - border: 1px double hsl(0, 0%, 70%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table td, -.ck-content .table table th { - min-width: 2em; - padding: .4em; - border: 1px solid hsl(0, 0%, 75%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table th { - font-weight: bold; - background: hsla(0, 0%, 0%, 5%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content[dir="rtl"] .table th { - text-align: right; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content[dir="ltr"] .table th { - text-align: left; -} -/* @ckeditor/ckeditor5-table/theme/tablecaption.css */ -.ck-content .table > figcaption { - display: table-caption; - caption-side: top; - word-break: break-word; - text-align: center; - color: var(--ck-color-selector-caption-text); - background-color: var(--ck-color-selector-caption-background); - padding: .6em; - font-size: .75em; - outline-offset: -1px; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break { - position: relative; - clear: both; - padding: 5px 0; - display: flex; - align-items: center; - justify-content: center; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break::after { - content: ''; - position: absolute; - border-bottom: 2px dashed hsl(0, 0%, 77%); - width: 100%; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break__label { - position: relative; - z-index: 1; - padding: .3em .6em; - display: block; - text-transform: uppercase; - border: 1px solid hsl(0, 0%, 77%); - border-radius: 2px; - font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif; - font-size: 0.75em; - font-weight: bold; - color: hsl(0, 0%, 20%); - background: hsl(0, 0%, 100%); - box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15); - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */ -.ck-content .media { - clear: both; - margin: 0.9em 0; - display: block; - min-width: 15em; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list { - list-style: none; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list li { - position: relative; - margin-bottom: 5px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list li .todo-list { - margin-top: 5px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input { - -webkit-appearance: none; - display: inline-block; - position: relative; - width: var(--ck-todo-list-checkmark-size); - height: var(--ck-todo-list-checkmark-size); - vertical-align: middle; - border: 0; - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content[dir=rtl] .todo-list .todo-list__label > input { - left: 0; - margin-right: 0; - right: -25px; - margin-left: -15px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input::before { - display: block; - position: absolute; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - border: 1px solid hsl(0, 0%, 20%); - border-radius: 2px; - transition: 250ms ease-in-out box-shadow; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input::after { - display: block; - position: absolute; - box-sizing: content-box; - pointer-events: none; - content: ''; - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); - border-style: solid; - border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; - transform: rotate(45deg); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input[checked]::before { - background: hsl(126, 64%, 41%); - border-color: hsl(126, 64%, 41%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input[checked]::after { - border-color: hsl(0, 0%, 100%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label .todo-list__label__description { - vertical-align: middle; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { - position: absolute; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > input, -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { - cursor: pointer; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before { - box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { - -webkit-appearance: none; - display: inline-block; - position: relative; - width: var(--ck-todo-list-checkmark-size); - height: var(--ck-todo-list-checkmark-size); - vertical-align: middle; - border: 0; - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input { - left: 0; - margin-right: 0; - right: -25px; - margin-left: -15px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before { - display: block; - position: absolute; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - border: 1px solid hsl(0, 0%, 20%); - border-radius: 2px; - transition: 250ms ease-in-out box-shadow; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after { - display: block; - position: absolute; - box-sizing: content-box; - pointer-events: none; - content: ''; - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); - border-style: solid; - border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; - transform: rotate(45deg); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before { - background: hsl(126, 64%, 41%); - border-color: hsl(126, 64%, 41%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after { - border-color: hsl(0, 0%, 100%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { - position: absolute; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol { - list-style-type: decimal; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol { - list-style-type: lower-latin; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol { - list-style-type: lower-roman; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol ol { - list-style-type: upper-latin; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol ol ol { - list-style-type: upper-roman; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul { - list-style-type: disc; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul { - list-style-type: circle; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul ul { - list-style-type: square; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul ul ul { - list-style-type: square; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image { - display: table; - clear: both; - text-align: center; - margin: 0.9em auto; - min-width: 50px; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image img { - display: block; - margin: 0 auto; - max-width: 100%; - min-width: 100%; - height: auto; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline { - /* - * Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).; - * Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root. - * This strange behavior does not happen with inline-flex. - */ - display: inline-flex; - max-width: 100%; - align-items: flex-start; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline picture { - display: flex; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline picture, -.ck-content .image-inline img { - flex-grow: 1; - flex-shrink: 1; - max-width: 100%; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content img.image_resized { - height: auto; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized { - max-width: 100%; - display: block; - box-sizing: border-box; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized img { - width: 100%; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized > figcaption { - display: block; -} -/* @ckeditor/ckeditor5-image/theme/imagecaption.css */ -.ck-content .image > figcaption { - display: table-caption; - caption-side: bottom; - word-break: break-word; - color: var(--ck-color-image-caption-text); - background-color: var(--ck-color-image-caption-background); - padding: .6em; - font-size: .75em; - outline-offset: -1px; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-left, -.ck-content .image-style-block-align-right { - max-width: calc(100% - var(--ck-image-style-spacing)); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-left, -.ck-content .image-style-align-right { - clear: none; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-side { - float: right; - margin-left: var(--ck-image-style-spacing); - max-width: 50%; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-left { - float: left; - margin-right: var(--ck-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-center { - margin-left: auto; - margin-right: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-right { - float: right; - margin-left: var(--ck-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-right { - margin-right: 0; - margin-left: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-left { - margin-left: 0; - margin-right: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content p + .image-style-align-left, -.ck-content p + .image-style-align-right, -.ck-content p + .image-style-side { - margin-top: 0; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-left, -.ck-content .image-inline.image-style-align-right { - margin-top: var(--ck-inline-image-style-spacing); - margin-bottom: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-left { - margin-right: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-right { - margin-left: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-yellow { - background-color: var(--ck-highlight-marker-yellow); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-green { - background-color: var(--ck-highlight-marker-green); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-pink { - background-color: var(--ck-highlight-marker-pink); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-blue { - background-color: var(--ck-highlight-marker-blue); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .pen-red { - color: var(--ck-highlight-pen-red); - background-color: transparent; -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .pen-green { - color: var(--ck-highlight-pen-green); - background-color: transparent; -} -/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ -.ck-content blockquote { - overflow: hidden; - padding-right: 1.5em; - padding-left: 1.5em; - margin-left: 0; - margin-right: 0; - font-style: italic; - border-left: solid 5px hsl(0, 0%, 80%); -} -/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ -.ck-content[dir="rtl"] blockquote { - border-left: 0; - border-right: solid 5px hsl(0, 0%, 80%); -} -/* @ckeditor/ckeditor5-basic-styles/theme/code.css */ -.ck-content code { - background-color: hsla(0, 0%, 78%, 0.3); - padding: .15em; - border-radius: 2px; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-tiny { - font-size: .7em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-small { - font-size: .85em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-big { - font-size: 1.4em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-huge { - font-size: 1.8em; -} -/* @ckeditor/ckeditor5-mention/theme/mention.css */ -.ck-content .mention { - background: var(--ck-color-mention-background); - color: var(--ck-color-mention-text); -} -/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */ -.ck-content hr { - margin: 15px 0; - height: 4px; - background: hsl(0, 0%, 87%); - border: 0; -} -/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */ -.ck-content pre { - padding: 1em; - text-align: left; - direction: ltr; - tab-size: 4; - white-space: pre-wrap; - font-style: normal; - min-width: 200px; - border: 0px; - border-radius: 6px; - box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2); -} -.ck-content pre:not(.hljs) { - color: hsl(0, 0%, 20.8%); - background: hsla(0, 0%, 78%, 0.3); -} -/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */ -.ck-content pre code { - background: unset; - padding: 0; - border-radius: 0; -} -@media print { - /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ - .ck-content .page-break { - padding: 0; - } - /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ - .ck-content .page-break::after { - display: none; - } -} diff --git a/_regroup/demo/style.css b/_regroup/demo/style.css deleted file mode 100644 index 0ebbae93d..000000000 --- a/_regroup/demo/style.css +++ /dev/null @@ -1,593 +0,0 @@ -/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */ - -.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */ - display: none; -} - -.page-break { - page-break-after: always; -} - -.printed-content .page-break:after, -.printed-content .page-break > * { - display: none !important; -} - -.ck-content li p { - margin: 0 !important; -} - -.admonition { - --accent-color: var(--card-border-color); - border: 1px solid var(--accent-color); - box-shadow: var(--card-box-shadow); - background: var(--card-background-color); - border-radius: 0.5em; - padding: 1em; - margin: 1.25em 0; - position: relative; - overflow: hidden; -} - -.admonition p:last-child { - margin-bottom: 0; -} - -.admonition p, h2 { - margin-top: 0; -} - -.admonition.note { --accent-color: #69c7ff; } -.admonition.tip { --accent-color: #40c025; } -.admonition.important { --accent-color: #9839f7; } -.admonition.caution { --accent-color: #ff2e2e; } -.admonition.warning { --accent-color: #e2aa03; } - -/* - * CKEditor 5 (v41.0.0) content styles. - * Generated on Fri, 26 Jan 2024 10:23:49 GMT. - * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html - */ - -:root { - --ck-color-image-caption-background: hsl(0, 0%, 97%); - --ck-color-image-caption-text: hsl(0, 0%, 20%); - --ck-color-mention-background: hsla(341, 100%, 30%, 0.1); - --ck-color-mention-text: hsl(341, 100%, 30%); - --ck-color-selector-caption-background: hsl(0, 0%, 97%); - --ck-color-selector-caption-text: hsl(0, 0%, 20%); - --ck-highlight-marker-blue: hsl(201, 97%, 72%); - --ck-highlight-marker-green: hsl(120, 93%, 68%); - --ck-highlight-marker-pink: hsl(345, 96%, 73%); - --ck-highlight-marker-yellow: hsl(60, 97%, 73%); - --ck-highlight-pen-green: hsl(112, 100%, 27%); - --ck-highlight-pen-red: hsl(0, 85%, 49%); - --ck-image-style-spacing: 1.5em; - --ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2); - --ck-todo-list-checkmark-size: 16px; -} - -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table .ck-table-resized { - table-layout: fixed; -} -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table table { - overflow: hidden; -} -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table td, -.ck-content .table th { - overflow-wrap: break-word; - position: relative; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table { - margin: 0.9em auto; - display: table; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - height: 100%; - border: 1px double hsl(0, 0%, 70%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table td, -.ck-content .table table th { - min-width: 2em; - padding: .4em; - border: 1px solid hsl(0, 0%, 75%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table th { - font-weight: bold; - background: hsla(0, 0%, 0%, 5%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content[dir="rtl"] .table th { - text-align: right; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content[dir="ltr"] .table th { - text-align: left; -} -/* @ckeditor/ckeditor5-table/theme/tablecaption.css */ -.ck-content .table > figcaption { - display: table-caption; - caption-side: top; - word-break: break-word; - text-align: center; - color: var(--ck-color-selector-caption-text); - background-color: var(--ck-color-selector-caption-background); - padding: .6em; - font-size: .75em; - outline-offset: -1px; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break { - position: relative; - clear: both; - padding: 5px 0; - display: flex; - align-items: center; - justify-content: center; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break::after { - content: ''; - position: absolute; - border-bottom: 2px dashed hsl(0, 0%, 77%); - width: 100%; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break__label { - position: relative; - z-index: 1; - padding: .3em .6em; - display: block; - text-transform: uppercase; - border: 1px solid hsl(0, 0%, 77%); - border-radius: 2px; - font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif; - font-size: 0.75em; - font-weight: bold; - color: hsl(0, 0%, 20%); - background: hsl(0, 0%, 100%); - box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15); - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */ -.ck-content .media { - clear: both; - margin: 0.9em 0; - display: block; - min-width: 15em; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list { - list-style: none; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list li { - position: relative; - margin-bottom: 5px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list li .todo-list { - margin-top: 5px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input { - -webkit-appearance: none; - display: inline-block; - position: relative; - width: var(--ck-todo-list-checkmark-size); - height: var(--ck-todo-list-checkmark-size); - vertical-align: middle; - border: 0; - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content[dir=rtl] .todo-list .todo-list__label > input { - left: 0; - margin-right: 0; - right: -25px; - margin-left: -15px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input::before { - display: block; - position: absolute; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - border: 1px solid hsl(0, 0%, 20%); - border-radius: 2px; - transition: 250ms ease-in-out box-shadow; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input::after { - display: block; - position: absolute; - box-sizing: content-box; - pointer-events: none; - content: ''; - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); - border-style: solid; - border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; - transform: rotate(45deg); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input[checked]::before { - background: hsl(126, 64%, 41%); - border-color: hsl(126, 64%, 41%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input[checked]::after { - border-color: hsl(0, 0%, 100%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label .todo-list__label__description { - vertical-align: middle; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { - position: absolute; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > input, -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { - cursor: pointer; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before { - box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { - -webkit-appearance: none; - display: inline-block; - position: relative; - width: var(--ck-todo-list-checkmark-size); - height: var(--ck-todo-list-checkmark-size); - vertical-align: middle; - border: 0; - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input { - left: 0; - margin-right: 0; - right: -25px; - margin-left: -15px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before { - display: block; - position: absolute; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - border: 1px solid hsl(0, 0%, 20%); - border-radius: 2px; - transition: 250ms ease-in-out box-shadow; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after { - display: block; - position: absolute; - box-sizing: content-box; - pointer-events: none; - content: ''; - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); - border-style: solid; - border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; - transform: rotate(45deg); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before { - background: hsl(126, 64%, 41%); - border-color: hsl(126, 64%, 41%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after { - border-color: hsl(0, 0%, 100%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { - position: absolute; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol { - list-style-type: decimal; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol { - list-style-type: lower-latin; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol { - list-style-type: lower-roman; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol ol { - list-style-type: upper-latin; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol ol ol { - list-style-type: upper-roman; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul { - list-style-type: disc; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul { - list-style-type: circle; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul ul { - list-style-type: square; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul ul ul { - list-style-type: square; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image { - display: table; - clear: both; - text-align: center; - margin: 0.9em auto; - min-width: 50px; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image img { - display: block; - margin: 0 auto; - max-width: 100%; - min-width: 100%; - height: auto; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline { - /* - * Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).; - * Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root. - * This strange behavior does not happen with inline-flex. - */ - display: inline-flex; - max-width: 100%; - align-items: flex-start; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline picture { - display: flex; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline picture, -.ck-content .image-inline img { - flex-grow: 1; - flex-shrink: 1; - max-width: 100%; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content img.image_resized { - height: auto; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized { - max-width: 100%; - display: block; - box-sizing: border-box; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized img { - width: 100%; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized > figcaption { - display: block; -} -/* @ckeditor/ckeditor5-image/theme/imagecaption.css */ -.ck-content .image > figcaption { - display: table-caption; - caption-side: bottom; - word-break: break-word; - color: var(--ck-color-image-caption-text); - background-color: var(--ck-color-image-caption-background); - padding: .6em; - font-size: .75em; - outline-offset: -1px; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-left, -.ck-content .image-style-block-align-right { - max-width: calc(100% - var(--ck-image-style-spacing)); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-left, -.ck-content .image-style-align-right { - clear: none; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-side { - float: right; - margin-left: var(--ck-image-style-spacing); - max-width: 50%; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-left { - float: left; - margin-right: var(--ck-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-center { - margin-left: auto; - margin-right: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-right { - float: right; - margin-left: var(--ck-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-right { - margin-right: 0; - margin-left: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-left { - margin-left: 0; - margin-right: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content p + .image-style-align-left, -.ck-content p + .image-style-align-right, -.ck-content p + .image-style-side { - margin-top: 0; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-left, -.ck-content .image-inline.image-style-align-right { - margin-top: var(--ck-inline-image-style-spacing); - margin-bottom: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-left { - margin-right: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-right { - margin-left: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-yellow { - background-color: var(--ck-highlight-marker-yellow); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-green { - background-color: var(--ck-highlight-marker-green); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-pink { - background-color: var(--ck-highlight-marker-pink); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-blue { - background-color: var(--ck-highlight-marker-blue); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .pen-red { - color: var(--ck-highlight-pen-red); - background-color: transparent; -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .pen-green { - color: var(--ck-highlight-pen-green); - background-color: transparent; -} -/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ -.ck-content blockquote { - overflow: hidden; - padding-right: 1.5em; - padding-left: 1.5em; - margin-left: 0; - margin-right: 0; - font-style: italic; - border-left: solid 5px hsl(0, 0%, 80%); -} -/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ -.ck-content[dir="rtl"] blockquote { - border-left: 0; - border-right: solid 5px hsl(0, 0%, 80%); -} -/* @ckeditor/ckeditor5-basic-styles/theme/code.css */ -.ck-content code { - background-color: hsla(0, 0%, 78%, 0.3); - padding: .15em; - border-radius: 2px; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-tiny { - font-size: .7em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-small { - font-size: .85em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-big { - font-size: 1.4em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-huge { - font-size: 1.8em; -} -/* @ckeditor/ckeditor5-mention/theme/mention.css */ -.ck-content .mention { - background: var(--ck-color-mention-background); - color: var(--ck-color-mention-text); -} -/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */ -.ck-content hr { - margin: 15px 0; - height: 4px; - background: hsl(0, 0%, 87%); - border: 0; -} -/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */ -.ck-content pre { - padding: 1em; - text-align: left; - direction: ltr; - tab-size: 4; - white-space: pre-wrap; - font-style: normal; - min-width: 200px; - border: 0px; - border-radius: 6px; - box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2); -} -.ck-content pre:not(.hljs) { - color: hsl(0, 0%, 20.8%); - background: hsla(0, 0%, 78%, 0.3); -} -/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */ -.ck-content pre code { - background: unset; - padding: 0; - border-radius: 0; -} -@media print { - /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ - .ck-content .page-break { - padding: 0; - } - /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ - .ck-content .page-break::after { - display: none; - } -} diff --git a/_regroup/eslint.config.js b/_regroup/eslint.config.js index 2f2b2c036..7c906beb2 100644 --- a/_regroup/eslint.config.js +++ b/_regroup/eslint.config.js @@ -44,7 +44,6 @@ export default tseslint.config( "dist/*", "docs/*", "demo/*", - "libraries/*", "src/public/app-dist/*", "src/public/app/doc_notes/*" ] diff --git a/_regroup/eslint.format.config.js b/_regroup/eslint.format.config.js index 23fbb6caf..9dbfd78b2 100644 --- a/_regroup/eslint.format.config.js +++ b/_regroup/eslint.format.config.js @@ -38,7 +38,6 @@ export default [ "dist/*", "docs/*", "demo/*", - "libraries/*", // TriliumNextTODO: check if we want to format packages here as well - for now skipping it "packages/*", "src/public/app-dist/*", diff --git a/_regroup/jsdoc-conf.json b/_regroup/jsdoc-conf.json deleted file mode 100644 index b61bbacb6..000000000 --- a/_regroup/jsdoc-conf.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "templates": { - "default": { - "includeDate": false - } - } -} diff --git a/_regroup/package.json b/_regroup/package.json index d3680bc02..b9e974e1b 100644 --- a/_regroup/package.json +++ b/_regroup/package.json @@ -35,13 +35,13 @@ "chore:generate-openapi": "tsx bin/generate-openapi.js" }, "devDependencies": { - "@playwright/test": "1.52.0", - "@stylistic/eslint-plugin": "4.2.0", - "@types/express": "5.0.1", - "@types/node": "22.15.17", + "@playwright/test": "1.53.1", + "@stylistic/eslint-plugin": "5.0.0", + "@types/express": "5.0.3", + "@types/node": "22.15.33", "@types/yargs": "17.0.33", - "@vitest/coverage-v8": "3.1.3", - "eslint": "9.26.0", + "@vitest/coverage-v8": "3.2.4", + "eslint": "9.29.0", "eslint-plugin-simple-import-sort": "12.1.1", "esm": "3.2.25", "jsdoc": "4.0.4", @@ -49,7 +49,7 @@ "rcedit": "4.0.1", "rimraf": "6.0.1", "tslib": "2.8.1", - "typedoc": "0.28.4", + "typedoc": "0.28.5", "typedoc-plugin-missing-exports": "4.0.0" }, "optionalDependencies": { diff --git a/_regroup/test-etapi/_login.http b/_regroup/test-etapi/_login.http deleted file mode 100644 index 9976e7cd4..000000000 --- a/_regroup/test-etapi/_login.http +++ /dev/null @@ -1,12 +0,0 @@ -POST {{triliumHost}}/etapi/auth/login -Content-Type: application/json - -{ - "password": "1234" -} - -> {% - client.assert(response.status === 201); - - client.global.set("authToken", response.body.authToken); -%} diff --git a/_regroup/test-etapi/app-info.http b/_regroup/test-etapi/app-info.http deleted file mode 100644 index a851005c2..000000000 --- a/_regroup/test-etapi/app-info.http +++ /dev/null @@ -1,7 +0,0 @@ -GET {{triliumHost}}/etapi/app-info -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.clipperProtocolVersion === "1.0"); -%} diff --git a/_regroup/test-etapi/basic-auth.http b/_regroup/test-etapi/basic-auth.http deleted file mode 100644 index cf79c357e..000000000 --- a/_regroup/test-etapi/basic-auth.http +++ /dev/null @@ -1,21 +0,0 @@ -GET {{triliumHost}}/etapi/app-info -Authorization: Basic etapi {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.clipperProtocolVersion === "1.0"); -%} - -### - -GET {{triliumHost}}/etapi/app-info -Authorization: Basic etapi wrong - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/app-info -Authorization: Basic wrong {{authToken}} - -> {% client.assert(response.status === 401); %} diff --git a/_regroup/test-etapi/create-backup.http b/_regroup/test-etapi/create-backup.http deleted file mode 100644 index 59ffbebc4..000000000 --- a/_regroup/test-etapi/create-backup.http +++ /dev/null @@ -1,4 +0,0 @@ -PUT {{triliumHost}}/etapi/backup/etapi_test -Authorization: {{authToken}} - -> {% client.assert(response.status === 201); %} diff --git a/_regroup/test-etapi/create-entities.http b/_regroup/test-etapi/create-entities.http deleted file mode 100644 index 98dae28b1..000000000 --- a/_regroup/test-etapi/create-entities.http +++ /dev/null @@ -1,158 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "forcedId{{$randomInt}}", - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!", - "dateCreated": "2023-08-21 23:38:51.123+0200", - "utcDateCreated": "2023-08-21 23:38:51.123Z" -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.note.noteId.startsWith("forcedId")); - client.assert(response.body.note.title == "Hello"); - client.assert(response.body.note.dateCreated == "2023-08-21 23:38:51.123+0200"); - client.assert(response.body.note.utcDateCreated == "2023-08-21 23:38:51.123Z"); - client.assert(response.body.branch.parentNoteId == "root"); - - client.log(`Created note ` + response.body.note.noteId + ` and branch ` + response.body.branch.branchId); - - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.parentNoteId == "_hidden"); - - client.global.set("clonedBranchId", response.body.branchId); - - client.log(`Created cloned branch ` + response.body.branchId); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.noteId == client.global.get("createdNoteId")); - client.assert(response.body.title == "Hello"); - // order is not defined and may fail in the future - client.assert(response.body.parentBranchIds[0] == client.global.get("clonedBranchId")) - client.assert(response.body.parentBranchIds[1] == client.global.get("createdBranchId")); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body == "Hi there!"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.branchId == client.global.get("createdBranchId")); - client.assert(response.body.parentNoteId == "root"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.branchId == client.global.get("clonedBranchId")); - client.assert(response.body.parentNoteId == "_hidden"); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Content-Type: application/json -Authorization: {{authToken}} - -{ - "attributeId": "forcedAttributeId{{$randomInt}}", - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.attributeId.startsWith("forcedAttributeId")); - - client.global.set("createdAttributeId", response.body.attributeId); -%} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.attributeId == client.global.get("createdAttributeId")); -%} - -### - -POST {{triliumHost}}/etapi/attachments -Content-Type: application/json -Authorization: {{authToken}} - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "plain/text", - "title": "my attachment", - "content": "my text" -} - -> {% - client.assert(response.status === 201); - - client.global.set("createdAttachmentId", response.body.attachmentId); -%} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.attachmentId == client.global.get("createdAttachmentId")); - client.assert(response.body.role == "file"); - client.assert(response.body.mime == "plain/text"); - client.assert(response.body.title == "my attachment"); -%} diff --git a/_regroup/test-etapi/delete-attachment.http b/_regroup/test-etapi/delete-attachment.http deleted file mode 100644 index d12e8de43..000000000 --- a/_regroup/test-etapi/delete-attachment.http +++ /dev/null @@ -1,52 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -DELETE {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTACHMENT_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/delete-attribute.http b/_regroup/test-etapi/delete-attribute.http deleted file mode 100644 index d61b75ba2..000000000 --- a/_regroup/test-etapi/delete-attribute.http +++ /dev/null @@ -1,52 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTRIBUTE_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/delete-cloned-branch.http b/_regroup/test-etapi/delete-cloned-branch.http deleted file mode 100644 index a87a6fa4d..000000000 --- a/_regroup/test-etapi/delete-cloned-branch.http +++ /dev/null @@ -1,87 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% client.global.set("clonedBranchId", response.body.branchId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} diff --git a/_regroup/test-etapi/delete-note-with-all-branches.http b/_regroup/test-etapi/delete-note-with-all-branches.http deleted file mode 100644 index 5a50bc4a9..000000000 --- a/_regroup/test-etapi/delete-note-with-all-branches.http +++ /dev/null @@ -1,126 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% client.global.set("clonedBranchId", response.body.branchId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code == "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "NOTE_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTRIBUTE_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/export-note-subtree.http b/_regroup/test-etapi/export-note-subtree.http deleted file mode 100644 index 28d90a362..000000000 --- a/_regroup/test-etapi/export-note-subtree.http +++ /dev/null @@ -1,37 +0,0 @@ -GET {{triliumHost}}/etapi/notes/root/export -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.headers.valueOf("Content-Type") == "application/zip"); -%} - -### - -GET {{triliumHost}}/etapi/notes/root/export?format=html -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.headers.valueOf("Content-Type") == "application/zip"); -%} - -### - -GET {{triliumHost}}/etapi/notes/root/export?format=markdown -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.headers.valueOf("Content-Type") == "application/zip"); -%} - -### - -GET {{triliumHost}}/etapi/notes/root/export?format=wrong -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "UNRECOGNIZED_EXPORT_FORMAT"); -%} diff --git a/_regroup/test-etapi/get-date-notes.http b/_regroup/test-etapi/get-date-notes.http deleted file mode 100644 index 19f0b4fc9..000000000 --- a/_regroup/test-etapi/get-date-notes.http +++ /dev/null @@ -1,72 +0,0 @@ -GET {{triliumHost}}/etapi/inbox/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "DATE_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "DATE_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "MONTH_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/years/2022 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/years/202 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "YEAR_INVALID"); -%} diff --git a/_regroup/test-etapi/get-inherited-attribute-cloned.http b/_regroup/test-etapi/get-inherited-attribute-cloned.http deleted file mode 100644 index eaf8d91b1..000000000 --- a/_regroup/test-etapi/get-inherited-attribute-cloned.http +++ /dev/null @@ -1,116 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello parent", - "type": "text", - "content": "Hi there!" -} - -> {% -client.assert(response.status === 201); -client.global.set("parentNoteId", response.body.note.noteId); -client.global.set("parentBranchId", response.body.branch.branchId); -%} - -### Create inheritable parent attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{parentNoteId}}", - "type": "label", - "name": "mylabel", - "value": "", - "isInheritable": true, - "position": 10 -} - -> {% -client.assert(response.status === 201); -client.global.set("parentAttributeId", response.body.attributeId); -%} - -### Create child note under root - -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello child", - "type": "text", - "content": "Hi there!" -} - -> {% -client.assert(response.status === 201); -client.global.set("childNoteId", response.body.note.noteId); -client.global.set("childBranchId", response.body.branch.branchId); -%} - -### Create child attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": false, - "position": 10 -} - -> {% -client.assert(response.status === 201); -client.global.set("childAttributeId", response.body.attributeId); -%} - -### Clone child to parent - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "parentNoteId": "{{parentNoteId}}" -} - -> {% -client.assert(response.status === 201); -client.assert(response.body.parentNoteId == client.global.get("parentNoteId")); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{childNoteId}} -Authorization: {{authToken}} - -> {% - -function hasAttribute(list, attributeId) { - for (let i = 0; i < list.length; i++) { - if (list[i]["attributeId"] === attributeId) { - return true; - } - } - return false; -} - -client.log(JSON.stringify(response.body.attributes)); - -client.assert(response.status === 200); -client.assert(response.body.noteId == client.global.get("childNoteId")); -client.assert(response.body.attributes.length == 2); -client.assert(hasAttribute(response.body.attributes, client.global.get("parentAttributeId"))); -client.assert(hasAttribute(response.body.attributes, client.global.get("childAttributeId"))); -%} diff --git a/_regroup/test-etapi/get-inherited-attribute.http b/_regroup/test-etapi/get-inherited-attribute.http deleted file mode 100644 index 26e9af854..000000000 --- a/_regroup/test-etapi/get-inherited-attribute.http +++ /dev/null @@ -1,61 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "GetInheritedAttributes Test Note", - "type": "text", - "content": "Hi there!" -} - -> {% - client.assert(response.status === 201); - client.global.set("parentNoteId", response.body.note.noteId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{parentNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "{{parentNoteId}}", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% -client.global.set("createdNoteId", response.body.note.noteId); -client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.noteId == client.global.get("createdNoteId")); -client.assert(response.body.attributes.length == 1); -client.assert(response.body.attributes[0].attributeId == client.global.get("createdAttributeId")); -%} diff --git a/_regroup/test-etapi/get-note-content.http b/_regroup/test-etapi/get-note-content.http deleted file mode 100644 index 50c677dd8..000000000 --- a/_regroup/test-etapi/get-note-content.http +++ /dev/null @@ -1,25 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body === "Hi there!"); -%} diff --git a/_regroup/test-etapi/http-client.env.json b/_regroup/test-etapi/http-client.env.json deleted file mode 100644 index 8ede0719c..000000000 --- a/_regroup/test-etapi/http-client.env.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dev": { - "triliumHost": "http://localhost:37740" - } -} diff --git a/_regroup/test-etapi/import-zip.http b/_regroup/test-etapi/import-zip.http deleted file mode 100644 index e831a050a..000000000 --- a/_regroup/test-etapi/import-zip.http +++ /dev/null @@ -1,12 +0,0 @@ -POST {{triliumHost}}/etapi/notes/root/import -Authorization: {{authToken}} -Content-Type: application/octet-stream -Content-Transfer-Encoding: binary - -< ../db/demo.zip - -> {% - client.assert(response.status === 201); - client.assert(response.body.note.title == "Trilium Demo"); - client.assert(response.body.branch.parentNoteId == "root"); -%} diff --git a/_regroup/test-etapi/logout.http b/_regroup/test-etapi/logout.http deleted file mode 100644 index 9bd7355e0..000000000 --- a/_regroup/test-etapi/logout.http +++ /dev/null @@ -1,34 +0,0 @@ -POST {{triliumHost}}/etapi/auth/login -Content-Type: application/json - -{ - "password": "1234" -} - -> {% - client.assert(response.status === 201); - - client.global.set("testAuthToken", response.body.authToken); -%} - -### - -GET {{triliumHost}}/etapi/notes/root -Authorization: {{testAuthToken}} - -> {% client.assert(response.status === 200); %} - -### - -POST {{triliumHost}}/etapi/auth/logout -Authorization: {{testAuthToken}} -Content-Type: application/json - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/notes/root -Authorization: {{testAuthToken}} - -> {% client.assert(response.status === 401); %} diff --git a/_regroup/test-etapi/no-token.http b/_regroup/test-etapi/no-token.http deleted file mode 100644 index d8198ed2b..000000000 --- a/_regroup/test-etapi/no-token.http +++ /dev/null @@ -1,109 +0,0 @@ -GET {{triliumHost}}/etapi/notes?search=aaa - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/notes/root - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/notes/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/notes/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/branches/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/branches/root - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/branches/root - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/inbox/2022-02-22 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-02-22 -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-02-22 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-02 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/years/2022 - -> {% client.assert(response.status === 401); %} - -### - -POST {{triliumHost}}/etapi/create-note - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/app-info - -> {% client.assert(response.status === 401); %} - -### Fake URL will get a 404 even without token - -GET {{triliumHost}}/etapi/zzzzzz - -> {% client.assert(response.status === 404); %} diff --git a/_regroup/test-etapi/other.http b/_regroup/test-etapi/other.http deleted file mode 100644 index c3f92fc94..000000000 --- a/_regroup/test-etapi/other.http +++ /dev/null @@ -1,4 +0,0 @@ -POST {{triliumHost}}/etapi/refresh-note-ordering/root -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} \ No newline at end of file diff --git a/_regroup/test-etapi/patch-attachment.http b/_regroup/test-etapi/patch-attachment.http deleted file mode 100644 index 44ffe696f..000000000 --- a/_regroup/test-etapi/patch-attachment.http +++ /dev/null @@ -1,79 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "title": "CHANGED", - "position": 999 -} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% - client.assert(response.body.title === "CHANGED"); - client.assert(response.body.position === 999); -%} - -### - -PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "root" -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "title": null -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} diff --git a/_regroup/test-etapi/patch-attribute.http b/_regroup/test-etapi/patch-attribute.http deleted file mode 100644 index 625c19446..000000000 --- a/_regroup/test-etapi/patch-attribute.http +++ /dev/null @@ -1,80 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "value": "CHANGED" -} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% -client.assert(response.body.value === "CHANGED"); -%} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "root" -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "value": null -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} \ No newline at end of file diff --git a/_regroup/test-etapi/patch-branch.http b/_regroup/test-etapi/patch-branch.http deleted file mode 100644 index 48116120c..000000000 --- a/_regroup/test-etapi/patch-branch.http +++ /dev/null @@ -1,66 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "type": "text", - "title": "Hello", - "content": "" -} - -> {% client.global.set("createdBranchId", response.body.branch.branchId); %} - -### - -PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "prefix": "pref", - "notePosition": 666, - "isExpanded": true -} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.prefix === 'pref'); -client.assert(response.body.notePosition === 666); -client.assert(response.body.isExpanded === true); -%} - -### - -PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root" -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "prefix": 123 -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} \ No newline at end of file diff --git a/_regroup/test-etapi/patch-note.http b/_regroup/test-etapi/patch-note.http deleted file mode 100644 index 24b9251d2..000000000 --- a/_regroup/test-etapi/patch-note.http +++ /dev/null @@ -1,83 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "code", - "mime": "application/json", - "content": "{}" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.title === 'Hello'); -client.assert(response.body.type === 'code'); -client.assert(response.body.mime === 'application/json'); -%} - -### - -PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "title": "Wassup", - "type": "html", - "mime": "text/html", - "dateCreated": "2023-08-21 23:38:51.123+0200", - "utcDateCreated": "2023-08-21 23:38:51.123Z" -} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.title === 'Wassup'); -client.assert(response.body.type === 'html'); -client.assert(response.body.mime === 'text/html'); -client.assert(response.body.dateCreated == "2023-08-21 23:38:51.123+0200"); -client.assert(response.body.utcDateCreated == "2023-08-21 23:38:51.123Z"); -%} - -### - -PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "isProtected": true -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "title": true -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} diff --git a/_regroup/test-etapi/post-revision.http b/_regroup/test-etapi/post-revision.http deleted file mode 100644 index 139397855..000000000 --- a/_regroup/test-etapi/post-revision.http +++ /dev/null @@ -1,23 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "code", - "mime": "text/plain", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/notes/{{createdNoteId}}/revision -Authorization: {{authToken}} -Content-Type: text/plain - -Changed content - -> {% client.assert(response.status === 204); %} diff --git a/_regroup/test-etapi/put-attachment-content-binary.http b/_regroup/test-etapi/put-attachment-content-binary.http deleted file mode 100644 index 6e6d6dad3..000000000 --- a/_regroup/test-etapi/put-attachment-content-binary.http +++ /dev/null @@ -1,39 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -PUT {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content -Authorization: {{authToken}} -Content-Type: application/octet-stream -Content-Transfer-Encoding: binary - -< ../images/icon-color.png - -> {% client.assert(response.status === 204); %} diff --git a/_regroup/test-etapi/put-attachment-content.http b/_regroup/test-etapi/put-attachment-content.http deleted file mode 100644 index 57e96a4b9..000000000 --- a/_regroup/test-etapi/put-attachment-content.http +++ /dev/null @@ -1,45 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -PUT {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content -Authorization: {{authToken}} -Content-Type: text/plain - -Changed content - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content -Authorization: {{authToken}} - -> {% client.assert(response.body === "Changed content"); %} diff --git a/_regroup/test-etapi/put-note-content-binary.http b/_regroup/test-etapi/put-note-content-binary.http deleted file mode 100644 index 545b3c111..000000000 --- a/_regroup/test-etapi/put-note-content-binary.http +++ /dev/null @@ -1,25 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "image", - "mime": "image/png", - "content": "" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -PUT {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} -Content-Type: application/octet-stream -Content-Transfer-Encoding: binary - -< ../images/icon-color.png - -> {% client.assert(response.status === 204); %} - diff --git a/_regroup/test-etapi/put-note-content.http b/_regroup/test-etapi/put-note-content.http deleted file mode 100644 index 670195ac2..000000000 --- a/_regroup/test-etapi/put-note-content.http +++ /dev/null @@ -1,30 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "code", - "mime": "text/plain", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -PUT {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} -Content-Type: text/plain - -Changed content - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% client.assert(response.body === "Changed content"); %} diff --git a/_regroup/test-etapi/search.http b/_regroup/test-etapi/search.http deleted file mode 100644 index 4655f22e0..000000000 --- a/_regroup/test-etapi/search.http +++ /dev/null @@ -1,39 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "title", - "type": "text", - "content": "{{$uuid}}" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% client.global.set("content", response.body); %} - -### - -GET {{triliumHost}}/etapi/notes?search={{content}}&debug=true -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.results.length === 1); -%} - -### Same but with fast search which doesn't look in the content so 0 notes should be found - -GET {{triliumHost}}/etapi/notes?search={{content}}&fastSearch=true -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.results.length === 0); -%} diff --git a/_regroup/trilium.iml b/_regroup/trilium.iml deleted file mode 100644 index bfa02661b..000000000 --- a/_regroup/trilium.iml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/client/.env b/apps/client/.env new file mode 100644 index 000000000..001b8becd --- /dev/null +++ b/apps/client/.env @@ -0,0 +1,4 @@ +# The development license key for premium CKEditor features. +# Note: This key must only be used for the Trilium Notes project. +# Expires on: 2025-09-13 +VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w \ No newline at end of file diff --git a/apps/client/.env.production b/apps/client/.env.production new file mode 100644 index 000000000..efd1fd517 --- /dev/null +++ b/apps/client/.env.production @@ -0,0 +1 @@ +VITE_CKEDITOR_ENABLE_INSPECTOR=false diff --git a/apps/client/package.json b/apps/client/package.json index 953324b37..b1515f0c6 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,16 +1,16 @@ { "name": "@triliumnext/client", - "version": "0.94.0", + "version": "0.95.0", "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", "private": true, "license": "AGPL-3.0-only", "author": { - "name": "TriliumNext Notes Team", + "name": "Trilium Notes Team", "email": "contact@eliandoran.me", "url": "https://github.com/TriliumNext/Notes" }, "dependencies": { - "@eslint/js": "9.26.0", + "@eslint/js": "9.29.0", "@excalidraw/excalidraw": "0.18.0", "@fullcalendar/core": "6.1.17", "@fullcalendar/daygrid": "6.1.17", @@ -18,35 +18,40 @@ "@fullcalendar/list": "6.1.17", "@fullcalendar/multimonth": "6.1.17", "@fullcalendar/timegrid": "6.1.17", - "@mermaid-js/layout-elk": "0.1.7", + "@mermaid-js/layout-elk": "0.1.8", "@mind-elixir/node-menu": "1.0.5", "@popperjs/core": "2.11.8", "@triliumnext/ckeditor5": "workspace:*", + "@triliumnext/codemirror": "workspace:*", "@triliumnext/commons": "workspace:*", - "bootstrap": "5.3.6", + "@triliumnext/highlightjs": "workspace:*", + "@triliumnext/share-theme": "workspace:*", + "autocomplete.js": "0.38.1", + "bootstrap": "5.3.7", + "boxicons": "2.1.4", "dayjs": "1.11.13", "dayjs-plugin-utc": "0.1.2", "debounce": "2.2.0", "draggabilly": "3.0.0", - "eslint-linter-browserify": "9.26.0", - "force-graph": "1.49.5", - "globals": "16.1.0", - "i18next": "25.1.2", + "force-graph": "1.49.6", + "globals": "16.2.0", + "i18next": "25.2.1", "i18next-http-backend": "3.0.2", "jquery": "3.7.1", "jquery-hotkeys": "0.2.2", "jquery.fancytree": "2.38.5", "jsplumb": "2.15.6", + "katex": "0.16.22", "knockout": "3.5.1", "leaflet": "1.9.4", "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", - "marked": "15.0.11", - "mermaid": "11.6.0", - "mind-elixir": "4.5.2", + "marked": "15.0.12", + "mermaid": "11.7.0", + "mind-elixir": "4.6.1", + "normalize.css": "8.0.1", "panzoom": "9.4.3", - "react": "19.1.0", - "react-dom": "19.1.0", + "preact": "10.26.9", "split.js": "1.6.5", "svg-pan-zoom": "3.6.2", "vanilla-js-wheel-zoom": "9.0.4" @@ -55,15 +60,25 @@ "@ckeditor/ckeditor5-inspector": "4.1.0", "@types/bootstrap": "5.2.10", "@types/jquery": "3.5.32", - "@types/leaflet": "1.9.17", + "@types/leaflet": "1.9.19", "@types/leaflet-gpx": "1.3.7", - "@types/react": "19.1.3", - "@types/react-dom": "19.1.3", + "@types/mark.js": "8.11.12", "copy-webpack-plugin": "13.0.0", - "happy-dom": "17.4.6", - "script-loader": "0.7.2" + "happy-dom": "18.0.1", + "script-loader": "0.7.2", + "vite-plugin-static-copy": "3.1.0" }, "nx": { - "name": "client" + "name": "client", + "targets": { + "serve": { + "dependsOn": [ + "^build" + ] + }, + "circular-deps": { + "command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular" + } + } } } \ No newline at end of file diff --git a/apps/client/src-example/app/app.element.css b/apps/client/src-example/app/app.element.css deleted file mode 100644 index 27d098404..000000000 --- a/apps/client/src-example/app/app.element.css +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Remove template code below - */ - html { - -webkit-text-size-adjust: 100%; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, - 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; - line-height: 1.5; - tab-size: 4; - scroll-behavior: smooth; - } - body { - font-family: inherit; - line-height: inherit; - margin: 0; - } - h1, - h2, - p, - pre { - margin: 0; - } - *, - ::before, - ::after { - box-sizing: border-box; - border-width: 0; - border-style: solid; - border-color: currentColor; - } - h1, - h2 { - font-size: inherit; - font-weight: inherit; - } - a { - color: inherit; - text-decoration: inherit; - } - pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - 'Liberation Mono', 'Courier New', monospace; - } - svg { - display: block; - vertical-align: middle; - } - - svg { - shape-rendering: auto; - text-rendering: optimizeLegibility; - } - pre { - background-color: rgba(55, 65, 81, 1); - border-radius: 0.25rem; - color: rgba(229, 231, 235, 1); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - 'Liberation Mono', 'Courier New', monospace; - overflow: scroll; - padding: 0.5rem 0.75rem; - } - - .shadow { - box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); - } - .rounded { - border-radius: 1.5rem; - } - - .wrapper { - width: 100%; - } - .container { - margin-left: auto; - margin-right: auto; - max-width: 768px; - padding-bottom: 3rem; - padding-left: 1rem; - padding-right: 1rem; - color: rgba(55, 65, 81, 1); - width: 100%; - } - #welcome { - margin-top: 2.5rem; - } - #welcome h1 { - font-size: 3rem; - font-weight: 500; - letter-spacing: -0.025em; - line-height: 1; - } - #welcome span { - display: block; - font-size: 1.875rem; - font-weight: 300; - line-height: 2.25rem; - margin-bottom: 0.5rem; - } - #hero { - align-items: center; - background-color: hsla(214, 62%, 21%, 1); - border: none; - box-sizing: border-box; - color: rgba(55, 65, 81, 1); - display: grid; - grid-template-columns: 1fr; - margin-top: 3.5rem; - } - #hero .text-container { - color: rgba(255, 255, 255, 1); - padding: 3rem 2rem; - } - #hero .text-container h2 { - font-size: 1.5rem; - line-height: 2rem; - position: relative; - } - #hero .text-container h2 svg { - color: hsla(162, 47%, 50%, 1); - height: 2rem; - left: -0.25rem; - position: absolute; - top: 0; - width: 2rem; - } - #hero .text-container h2 span { - margin-left: 2.5rem; - } - #hero .text-container a { - background-color: rgba(255, 255, 255, 1); - border-radius: 0.75rem; - color: rgba(55, 65, 81, 1); - display: inline-block; - margin-top: 1.5rem; - padding: 1rem 2rem; - text-decoration: inherit; - } - #hero .logo-container { - display: none; - justify-content: center; - padding-left: 2rem; - padding-right: 2rem; - } - #hero .logo-container svg { - color: rgba(255, 255, 255, 1); - width: 66.666667%; - } - - #middle-content { - align-items: flex-start; - display: grid; - gap: 4rem; - grid-template-columns: 1fr; - margin-top: 3.5rem; - } - - #learning-materials { - padding: 2.5rem 2rem; - } - #learning-materials h2 { - font-weight: 500; - font-size: 1.25rem; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - .list-item-link { - align-items: center; - border-radius: 0.75rem; - display: flex; - margin-top: 1rem; - padding: 1rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 100%; - } - .list-item-link svg:first-child { - margin-right: 1rem; - height: 1.5rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 1.5rem; - } - .list-item-link > span { - flex-grow: 1; - font-weight: 400; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - .list-item-link > span > span { - color: rgba(107, 114, 128, 1); - display: block; - flex-grow: 1; - font-size: 0.75rem; - font-weight: 300; - line-height: 1rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - .list-item-link svg:last-child { - height: 1rem; - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 1rem; - } - .list-item-link:hover { - color: rgba(255, 255, 255, 1); - background-color: hsla(162, 47%, 50%, 1); - } - .list-item-link:hover > span { - } - .list-item-link:hover > span > span { - color: rgba(243, 244, 246, 1); - } - .list-item-link:hover svg:last-child { - transform: translateX(0.25rem); - } - - #other-links { - } - .button-pill { - padding: 1.5rem 2rem; - transition-duration: 300ms; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - align-items: center; - display: flex; - } - .button-pill svg { - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - flex-shrink: 0; - width: 3rem; - } - .button-pill > span { - letter-spacing: -0.025em; - font-weight: 400; - font-size: 1.125rem; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - .button-pill span span { - display: block; - font-size: 0.875rem; - font-weight: 300; - line-height: 1.25rem; - } - .button-pill:hover svg, - .button-pill:hover { - color: rgba(255, 255, 255, 1) !important; - } - #nx-console:hover { - background-color: rgba(0, 122, 204, 1); - } - #nx-console svg { - color: rgba(0, 122, 204, 1); - } - #nx-console-jetbrains { - margin-top: 2rem; - } - #nx-console-jetbrains:hover { - background-color: rgba(255, 49, 140, 1); - } - #nx-console-jetbrains svg { - color: rgba(255, 49, 140, 1); - } - #nx-repo:hover { - background-color: rgba(24, 23, 23, 1); - } - #nx-repo svg { - color: rgba(24, 23, 23, 1); - } - - #nx-cloud { - margin-bottom: 2rem; - margin-top: 2rem; - padding: 2.5rem 2rem; - } - #nx-cloud > div { - align-items: center; - display: flex; - } - #nx-cloud > div svg { - border-radius: 0.375rem; - flex-shrink: 0; - width: 3rem; - } - #nx-cloud > div h2 { - font-size: 1.125rem; - font-weight: 400; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - #nx-cloud > div h2 span { - display: block; - font-size: 0.875rem; - font-weight: 300; - line-height: 1.25rem; - } - #nx-cloud p { - font-size: 1rem; - line-height: 1.5rem; - margin-top: 1rem; - } - #nx-cloud pre { - margin-top: 1rem; - } - #nx-cloud a { - color: rgba(107, 114, 128, 1); - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin-top: 1.5rem; - text-align: right; - } - #nx-cloud a:hover { - text-decoration: underline; - } - - #commands { - padding: 2.5rem 2rem; - - margin-top: 3.5rem; - } - #commands h2 { - font-size: 1.25rem; - font-weight: 400; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - #commands p { - font-size: 1rem; - font-weight: 300; - line-height: 1.5rem; - margin-top: 1rem; - padding-left: 1rem; - padding-right: 1rem; - } - details { - align-items: center; - display: flex; - margin-top: 1rem; - padding-left: 1rem; - padding-right: 1rem; - width: 100%; - } - details pre > span { - color: rgba(181, 181, 181, 1); - } - summary { - border-radius: 0.5rem; - display: flex; - font-weight: 400; - padding: 0.5rem; - cursor: pointer; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - summary:hover { - background-color: rgba(243, 244, 246, 1); - } - summary svg { - height: 1.5rem; - margin-right: 1rem; - width: 1.5rem; - } - - #love { - color: rgba(107, 114, 128, 1); - font-size: 0.875rem; - line-height: 1.25rem; - margin-top: 3.5rem; - opacity: 0.6; - text-align: center; - } - #love svg { - color: rgba(252, 165, 165, 1); - width: 1.25rem; - height: 1.25rem; - display: inline; - margin-top: -0.25rem; - } - - @media screen and (min-width: 768px) { - #hero { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - #hero .logo-container { - display: flex; - } - #middle-content { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } diff --git a/apps/client/src-example/app/app.element.spec.ts b/apps/client/src-example/app/app.element.spec.ts deleted file mode 100644 index 2c12da184..000000000 --- a/apps/client/src-example/app/app.element.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AppElement } from './app.element'; - -describe('AppElement', () => { - let app: AppElement; - - beforeEach(() => { - app = new AppElement(); - }); - - it('should create successfully', () => { - expect(app).toBeTruthy(); - }); - - it('should have a greeting', () => { - app.connectedCallback(); - - expect(app.querySelector('h1').innerHTML).toContain( - 'Welcome @triliumnext/client' - ); - }); -}); diff --git a/apps/client/src-example/app/app.element.ts b/apps/client/src-example/app/app.element.ts deleted file mode 100644 index fab80aa49..000000000 --- a/apps/client/src-example/app/app.element.ts +++ /dev/null @@ -1,409 +0,0 @@ -import './app.element.css'; - -export class AppElement extends HTMLElement { - public static observedAttributes = [ - - ]; - - connectedCallback() { - const title = '@triliumnext/client'; - this.innerHTML = ` -
-
- -
-

- Hello there, - Welcome ${title} 👋 -

-
- - -
-
-

- - - - You're up and running -

- What's next? -
-
- - - -
-
- - - - - -
-

Next steps

-

Here are some things you can do with Nx:

-
- - - - - Add UI library - -
# Generate UI lib
-nx g @nx/angular:lib ui
-
-# Add a component
-nx g @nx/angular:component ui/src/lib/button
-
-
- - - - - View interactive project graph - -
nx graph
-
-
- - - - - Run affected commands - -
# see what's been affected by changes
-nx affected:graph
-
-# run tests for current changes
-nx affected:test
-
-# run e2e tests for current changes
-nx affected:e2e
-
-
- -

- Carefully crafted with - - - -

-
-
- `; - } -} -customElements.define('triliumnext-root', AppElement); diff --git a/apps/client/src-example/assets/.gitkeep b/apps/client/src-example/assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/client/src-example/favicon.ico b/apps/client/src-example/favicon.ico deleted file mode 100644 index 317ebcb23..000000000 Binary files a/apps/client/src-example/favicon.ico and /dev/null differ diff --git a/apps/client/src-example/index.html b/apps/client/src-example/index.html deleted file mode 100644 index e206d4837..000000000 --- a/apps/client/src-example/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Client - - - - - - - - - diff --git a/apps/client/src-example/main.ts b/apps/client/src-example/main.ts deleted file mode 100644 index fdb879ded..000000000 --- a/apps/client/src-example/main.ts +++ /dev/null @@ -1 +0,0 @@ -import './app/app.element'; diff --git a/apps/client/src-example/styles.css b/apps/client/src-example/styles.css deleted file mode 100644 index 90d4ee007..000000000 --- a/apps/client/src-example/styles.css +++ /dev/null @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 1855876d3..443014572 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -1,5 +1,4 @@ import froca from "../services/froca.js"; -import bundleService from "../services/bundle.js"; import RootCommandExecutor from "./root_command_executor.js"; import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; import options from "../services/options.js"; @@ -27,6 +26,8 @@ import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.j import type { NativeImage, TouchBar } from "electron"; import TouchBarComponent from "./touch_bar.js"; import type { CKTextEditor } from "@triliumnext/ckeditor5"; +import type CodeMirror from "@triliumnext/codemirror"; +import { StartupChecks } from "./startup_checks.js"; interface Layout { getRootWidget: (appContext: AppContext) => RootWidget; @@ -127,6 +128,7 @@ export type CommandMappings = { openAboutDialog: CommandData; hideFloatingButtons: {}; hideLeftPane: CommandData; + showCpuArchWarning: CommandData; showLeftPane: CommandData; hoistNote: CommandData & { noteId: string }; leaveProtectedSession: CommandData; @@ -191,7 +193,7 @@ export type CommandMappings = { ExecuteCommandData & { callback?: GetTextEditorCallback; }; - executeWithCodeEditor: CommandData & ExecuteCommandData; + executeWithCodeEditor: CommandData & ExecuteCommandData; /** * Called upon when attempting to retrieve the content element of a {@link NoteContext}. * Generally should not be invoked manually, as it is used by {@link NoteContext.getContentElement}. @@ -278,11 +280,15 @@ export type CommandMappings = { buildIcon(name: string): NativeImage; }; refreshTouchBar: CommandData; + reloadTextEditor: CommandData; }; type EventMappings = { initialRenderComplete: {}; frocaReloaded: {}; + setLeftPaneVisibility: { + leftPaneVisible: boolean | null; + } protectedSessionStarted: {}; notesReloaded: { noteIds: string[]; @@ -463,13 +469,21 @@ export class AppContext extends Component { this.tabManager.loadTabs(); + const bundleService = (await import("../services/bundle.js")).default; setTimeout(() => bundleService.executeStartupBundles(), 2000); } initComponents() { this.tabManager = new TabManager(); - this.components = [this.tabManager, new RootCommandExecutor(), new Entrypoints(), new MainTreeExecutors(), new ShortcutComponent()]; + this.components = [ + this.tabManager, + new RootCommandExecutor(), + new Entrypoints(), + new MainTreeExecutors(), + new ShortcutComponent(), + new StartupChecks() + ]; if (utils.isMobile()) { this.components.push(new MobileScreenSwitcherExecutor()); diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index 81eae41e1..3a8a54310 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -11,6 +11,8 @@ import type { ViewScope } from "../services/link.js"; import type FNote from "../entities/fnote.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type { CKTextEditor } from "@triliumnext/ckeditor5"; +import type CodeMirror from "@triliumnext/codemirror"; +import { closeActiveDialog } from "../services/dialog.js"; export interface SetNoteOpts { triggerSwitchEvent?: unknown; @@ -82,7 +84,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> await this.triggerEvent("beforeNoteSwitch", { noteContext: this }); - utils.closeActiveDialog(); + closeActiveDialog(); this.notePath = resolvedNotePath; this.viewScope = opts.viewScope; @@ -158,6 +160,9 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> } saveToRecentNotes(resolvedNotePath: string) { + if (options.is("databaseReadonly")) { + return; + } setTimeout(async () => { // we include the note in the recent list only if the user stayed on the note at least 5 seconds if (resolvedNotePath && resolvedNotePath === this.notePath) { @@ -253,6 +258,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> return false; } + if (options.is("databaseReadonly")) { + return true; + } + if (this.note.isLabelTruthy("readOnly")) { return true; } @@ -261,14 +270,32 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> return true; } - const blob = await this.note.getBlob(); - if (!blob) { - return false; + // Store the initial decision about read-only status in the viewScope + // This will be "remembered" until the viewScope is refreshed + if (!this.viewScope) { + this.resetViewScope(); } - const sizeLimit = this.note.type === "text" ? options.getInt("autoReadonlySizeText") : options.getInt("autoReadonlySizeCode"); + const viewScope = this.viewScope!; - return sizeLimit && blob.contentLength > sizeLimit && !this.note.isLabelTruthy("autoReadOnlyDisabled"); + if (viewScope.isReadOnly === undefined) { + const blob = await this.note.getBlob(); + if (!blob) { + viewScope.isReadOnly = false; + return false; + } + + const sizeLimit = this.note.type === "text" + ? options.getInt("autoReadonlySizeText") + : options.getInt("autoReadonlySizeCode"); + + viewScope.isReadOnly = Boolean(sizeLimit && + blob.contentLength > sizeLimit && + !this.note.isLabelTruthy("autoReadOnlyDisabled")); + } + + // Return the cached decision, which won't change until viewScope is reset + return viewScope.isReadOnly || false; } async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { @@ -312,7 +339,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> async getCodeEditor() { return this.timeout( - new Promise((resolve) => + new Promise((resolve) => appContext.triggerCommand("executeWithCodeEditor", { resolve, ntxId: this.ntxId diff --git a/apps/client/src/components/root_command_executor.ts b/apps/client/src/components/root_command_executor.ts index 1e16fae81..8e7df9494 100644 --- a/apps/client/src/components/root_command_executor.ts +++ b/apps/client/src/components/root_command_executor.ts @@ -78,15 +78,15 @@ export default class RootCommandExecutor extends Component { } hideLeftPaneCommand() { - options.save(`leftPaneVisible`, "false"); + appContext.triggerEvent("setLeftPaneVisibility", { leftPaneVisible: false }); } showLeftPaneCommand() { - options.save(`leftPaneVisible`, "true"); + appContext.triggerEvent("setLeftPaneVisibility", { leftPaneVisible: true }); } toggleLeftPaneCommand() { - options.toggle("leftPaneVisible"); + appContext.triggerEvent("setLeftPaneVisibility", { leftPaneVisible: null }); } async showBackendLogCommand() { diff --git a/apps/client/src/components/startup_checks.ts b/apps/client/src/components/startup_checks.ts new file mode 100644 index 000000000..b320d7e13 --- /dev/null +++ b/apps/client/src/components/startup_checks.ts @@ -0,0 +1,26 @@ +import server from "../services/server"; +import Component from "./component"; + +// TODO: Deduplicate. +interface CpuArchResponse { + isCpuArchMismatch: boolean; +} + +export class StartupChecks extends Component { + + constructor() { + super(); + this.checkCpuArchMismatch(); + } + + async checkCpuArchMismatch() { + try { + const response = await server.get("system-checks") as CpuArchResponse; + if (response.isCpuArchMismatch) { + this.triggerCommand("showCpuArchWarning", {}); + } + } catch (error) { + console.warn("Could not check CPU arch status:", error); + } + } +} diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index 937bd8982..0416071c6 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -44,6 +44,9 @@ export default class TabManager extends Component { if (!appContext.isMainWindow) { return; } + if (options.is("databaseReadonly")) { + return; + } const openNoteContexts = this.noteContexts .map((nc) => nc.getPojoState()) @@ -277,10 +280,18 @@ export default class TabManager extends Component { return noteContext; } - async openInNewTab(targetNoteId: string, hoistedNoteId: string | null = null) { + async openInNewTab(targetNoteId: string, hoistedNoteId: string | null = null, activate: boolean = false) { const noteContext = await this.openEmptyTab(null, hoistedNoteId || this.getActiveContext()?.hoistedNoteId); await noteContext.setNote(targetNoteId); + + if (activate && noteContext.notePath) { + this.activateNoteContext(noteContext.ntxId, false); + await this.triggerEvent("noteSwitchedAndActivated", { + noteContext, + notePath: noteContext.notePath + }); + } } async openInSameTab(targetNoteId: string, hoistedNoteId: string | null = null) { @@ -677,7 +688,7 @@ export default class TabManager extends Component { const titleFragments = [ // it helps to navigate in history if note title is included in the title await activeNoteContext.getNavigationTitle(), - "TriliumNext Notes" + "Trilium Notes" ].filter(Boolean); document.title = titleFragments.join(" - "); diff --git a/apps/client/src/components/touch_bar.ts b/apps/client/src/components/touch_bar.ts index 4afe65151..7bf10d7f1 100644 --- a/apps/client/src/components/touch_bar.ts +++ b/apps/client/src/components/touch_bar.ts @@ -54,7 +54,7 @@ export default class TouchBarComponent extends Component { #refreshTouchBar() { const { TouchBar } = this.remote; const parentComponent = this.lastFocusedComponent; - let touchBar = null; + let touchBar: Electron.CrossProcessExports.TouchBar | null = null; if (this.$activeModal?.length) { touchBar = this.#buildModalTouchBar(); diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts index 51c69aa70..65e2e285a 100644 --- a/apps/client/src/desktop.ts +++ b/apps/client/src/desktop.ts @@ -8,9 +8,13 @@ import electronContextMenu from "./menus/electron_context_menu.js"; import glob from "./services/glob.js"; import { t } from "./services/i18n.js"; import options from "./services/options.js"; +import server from "./services/server.js"; import type ElectronRemote from "@electron/remote"; import type Electron from "electron"; import "./stylesheets/bootstrap.scss"; +import "boxicons/css/boxicons.min.css"; +import "jquery-hotkeys"; +import "autocomplete.js/index_jquery.js"; await appContext.earlyInit(); diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index e968dcae9..fd35c09b8 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -1,7 +1,6 @@ import server from "../services/server.js"; import noteAttributeCache from "../services/note_attribute_cache.js"; import ws from "../services/ws.js"; -import froca from "../services/froca.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; import type { Froca } from "../services/froca-interface.js"; @@ -410,8 +409,8 @@ class FNote { const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ notePath: path, isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), - isArchived: path.some((noteId) => froca.notes[noteId].isArchived), - isSearch: path.some((noteId) => froca.notes[noteId].type === "search"), + isArchived: path.some((noteId) => this.froca.notes[noteId].isArchived), + isSearch: path.some((noteId) => this.froca.notes[noteId].type === "search"), isHidden: path.includes("_hidden") })); @@ -789,7 +788,7 @@ class FNote { */ async getRelationTargets(name: string) { const relations = this.getRelations(name); - const targets = []; + const targets: (FNote | null)[] = []; for (const relation of relations) { targets.push(await this.froca.getNote(relation.value)); @@ -982,7 +981,7 @@ class FNote { continue; } - const parentNote = froca.notes[parentNoteId]; + const parentNote = this.froca.notes[parentNoteId]; if (!parentNote || parentNote.type === "search") { continue; diff --git a/apps/client/src/layouts/layout_commons.ts b/apps/client/src/layouts/layout_commons.ts index d9559cde2..e53839839 100644 --- a/apps/client/src/layouts/layout_commons.ts +++ b/apps/client/src/layouts/layout_commons.ts @@ -21,6 +21,7 @@ import ConfirmDialog from "../widgets/dialogs/confirm.js"; import RevisionsDialog from "../widgets/dialogs/revisions.js"; import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js"; import InfoDialog from "../widgets/dialogs/info.js"; +import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; export function applyModals(rootContainer: RootContainer) { rootContainer @@ -45,4 +46,5 @@ export function applyModals(rootContainer: RootContainer) { .child(new InfoDialog()) .child(new ConfirmDialog()) .child(new PromptDialog()) + .child(new IncorrectCpuArchDialog()) } diff --git a/apps/client/src/libraries/codemirror/eslint.js b/apps/client/src/libraries/codemirror/eslint.js deleted file mode 100644 index 403cb4e54..000000000 --- a/apps/client/src/libraries/codemirror/eslint.js +++ /dev/null @@ -1,74 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - async function validatorHtml(text, options) { - const result = /]*>([\s\S]+)<\/script>/ig.exec(text); - - if (result !== null) { - // preceding code is copied over but any (non-newline) character is replaced with space - // this will preserve line numbers etc. - const prefix = text.substr(0, result.index).replace(/./g, " "); - - const js = prefix + result[1]; - - return await validatorJavaScript(js, options); - } - - return []; - } - - async function validatorJavaScript(text, options) { - if (glob.isMobile() - || glob.getActiveContextNote() == null - || glob.getActiveContextNote().mime === 'application/json') { - // eslint doesn't seem to validate pure JSON well - return []; - } - - if (text.length > 20000) { - console.log("Skipping linting because of large size: ", text.length); - - return []; - } - - const errors = await glob.linter(text, glob.getActiveContextNote().mime); - - console.log(errors); - - const result = []; - if (errors) { - parseErrors(errors, result); - } - - return result; - } - - CodeMirror.registerHelper("lint", "javascript", validatorJavaScript); - CodeMirror.registerHelper("lint", "html", validatorHtml); - - function parseErrors(errors, output) { - for (const error of errors) { - const startLine = error.line - 1; - const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine; - const startCol = error.column - 1; - const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1; - - output.push({ - message: error.message, - severity: error.severity === 1 ? "warning" : "error", - from: CodeMirror.Pos(startLine, startCol), - to: CodeMirror.Pos(endLine, endCol) - }); - } - } -}); diff --git a/apps/client/src/libraries/codemirror/hcl.js b/apps/client/src/libraries/codemirror/hcl.js deleted file mode 100644 index 04c0ea749..000000000 --- a/apps/client/src/libraries/codemirror/hcl.js +++ /dev/null @@ -1,204 +0,0 @@ -// Source: https://github.com/codemirror/codemirror5/pull/7080/files - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/5/LICENSE - -(function (mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function (CodeMirror) { - "use strict"; - - CodeMirror.defineMode("hcl", function (config) { - var indentUnit = config.indentUnit; - - var keywords = { - "resource": true, - "variable": true, - "output": true, - "module": true, - "provider": true, - "data": true, - "locals": true, - "terraform": true, - "if": true, - "else": true, - "for": true, - "foreach": true, - "in": true, - "true": true, - "false": true, - "null": true, - }; - - var atoms = { - "true": true, - "false": true, - "null": true, - }; - - var isOperatorChar = /[+\-*&^%:=<>!|\/]/; - - var curPunc; - - function tokenBase(stream, state) { - var ch = stream.next(); - if (ch == '"' || ch == "'" || ch == "`") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } - if (/[\d\.]/.test(ch)) { - if (ch == ".") { - stream.match(/^[0-9_]+([eE][\-+]?[0-9_]+)?/); - } else { - stream.match(/^[0-9_]*\.?[0-9_]*([eE][\-+]?[0-9_]+)?/); - } - return "number"; - } - if (/[\[\]{}\(\),;\:\.]/.test(ch)) { - curPunc = ch; - return null; - } - if (ch == "/") { - if (stream.eat("*")) { - state.tokenize = tokenComment; - return tokenComment(stream, state); - } - if (stream.eat("/")) { - stream.skipToEnd(); - return "comment"; - } - } - if (isOperatorChar.test(ch)) { - stream.eatWhile(isOperatorChar); - return "operator"; - } - stream.eatWhile(/[\w\$_\xa1-\uffff]/); - var cur = stream.current(); - if (keywords.propertyIsEnumerable(cur)) { - return "keyword"; - } - if (atoms.propertyIsEnumerable(cur)) return "atom"; - return "variable"; - } - - function tokenString(quote) { - return function (stream, state) { - var escaped = false, - next, - end = false; - while ((next = stream.next()) != null) { - if (next == quote && !escaped) { - end = true; - break; - } - escaped = !escaped && quote != "`" && next == "\\"; - } - if (end || !(escaped || quote == "`")) - state.tokenize = tokenBase; - return "string"; - }; - } - - function tokenComment(stream, state) { - var maybeEnd = false, - ch; - while (ch = stream.next()) { - if (ch == "/" && maybeEnd) { - state.tokenize = tokenBase; - break; - } - maybeEnd = (ch == "*"); - } - return "comment"; - } - - function Context(indented, column, type, align, prev) { - this.indented = indented; - this.column = column; - this.type = type; - this.align = align; - this.prev = prev; - } - function pushContext(state, col, type) { - return state.context = new Context(state.indented, col, type, null, state.context); - } - function popContext(state) { - if (!state.context.prev) return; - var t = state.context.type; - if (t == ")" || t == "]" || t == "}") - state.indented = state.context.indented; - return state.context = state.context.prev; - } - - // Interface - - return { - startState: function (basecolumn) { - return { - tokenize: null, - context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), - indented: 0, - startOfLine: true - }; - }, - - token: function (stream, state) { - var ctx = state.context; - if (stream.sol()) { - if (ctx.align == null) ctx.align = false; - state.indented = stream.indentation(); - state.startOfLine = true; - } - if (stream.eatSpace()) return null; - curPunc = null; - var style = (state.tokenize || tokenBase)(stream, state); - if (style == "comment") return style; - if (ctx.align == null) ctx.align = true; - - if (curPunc == "{") pushContext(state, stream.column(), "}"); - else if (curPunc == "[") pushContext(state, stream.column(), "]"); - else if (curPunc == "(") pushContext(state, stream.column(), ")"); - else if (curPunc == "}" && ctx.type == "}") popContext(state); - else if (curPunc == ctx.type) popContext(state); - state.startOfLine = false; - return style; - }, - - indent: function (state, textAfter) { - if (state.tokenize != tokenBase && state.tokenize != null) return CodeMirror.Pass; - var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); - if (firstChar == "#" || firstChar == ";") return 0; - if (stream.sol()) { - if (ctx.type == "case" && /^(?:case|default)\b/.test(textAfter)) { - state.context.type = "}"; - return ctx.indented; - } - var closing = firstChar == ctx.type; - if (ctx.align) return ctx.column + (closing ? 0 : 1); - else return ctx.indented + (closing ? 0 : indentUnit); - } - }, - - electricChars: "{}):", - closeBrackets: "()[]{}''\"\"``", - fold: "brace", - blockCommentStart: "/*", - blockCommentEnd: "*/", - lineComment: "//" - }; - }); - - CodeMirror.defineMIME("text/x-hcl", "hcl"); - CodeMirror.modeInfo.push({ - ext: [ "hcl " ], - mime: "text/x-hcl", - mode: "hcl", - name: "Terraform (HCL)" - }); - -}); diff --git a/apps/client/src/libraries/highlightjs/terraform.js b/apps/client/src/libraries/highlightjs/terraform.js deleted file mode 100644 index 514e727cf..000000000 --- a/apps/client/src/libraries/highlightjs/terraform.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * highlight.js terraform syntax highlighting definition - * - * @see https://github.com/highlightjs/highlight.js - * - * :TODO: - * - * @package: highlightjs-terraform - * @author: Nikos Tsirmirakis - * @since: 2019-03-20 - * - * Description: Terraform (HCL) language definition - * Category: scripting - */ - -var module = module ? module : {}; // shim for browser use - -function hljsDefineTerraform(hljs) { - var NUMBERS = { - className: 'number', - begin: '\\b\\d+(\\.\\d+)?', - relevance: 0 - }; - var STRINGS = { - className: 'string', - begin: '"', - end: '"', - contains: [{ - className: 'variable', - begin: '\\${', - end: '\\}', - relevance: 9, - contains: [{ - className: 'string', - begin: '"', - end: '"' - }, { - className: 'meta', - begin: '[A-Za-z_0-9]*' + '\\(', - end: '\\)', - contains: [ - NUMBERS, { - className: 'string', - begin: '"', - end: '"', - contains: [{ - className: 'variable', - begin: '\\${', - end: '\\}', - contains: [{ - className: 'string', - begin: '"', - end: '"', - contains: [{ - className: 'variable', - begin: '\\${', - end: '\\}' - }] - }, { - className: 'meta', - begin: '[A-Za-z_0-9]*' + '\\(', - end: '\\)' - }] - }] - }, - 'self'] - }] - }] - }; - -return { - aliases: ['tf', 'hcl'], - keywords: 'resource variable provider output locals module data terraform|10', - literal: 'false true null', - contains: [ - hljs.COMMENT('\\#', '$'), - NUMBERS, - STRINGS - ] -} -} - -hljs.registerLanguage('terraform', hljsDefineTerraform); \ No newline at end of file diff --git a/apps/client/src/menus/context_menu.ts b/apps/client/src/menus/context_menu.ts index 7d0bc0a2f..72519233a 100644 --- a/apps/client/src/menus/context_menu.ts +++ b/apps/client/src/menus/context_menu.ts @@ -192,13 +192,17 @@ class ContextMenu { // it's important to stop the propagation especially for sub-menus, otherwise the event // might be handled again by top-level menu return false; - }) - .on("mouseup", (e) =>{ + }); + + $item.on("mouseup", (e) => { + // Prevent submenu from failing to expand on mobile + if (!this.isMobile || !("items" in item && item.items)) { e.stopPropagation(); // Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below. this.hide(); return false; - }); + } + }); if ("enabled" in item && item.enabled !== undefined && !item.enabled) { $item.addClass("disabled"); diff --git a/apps/client/src/mobile.ts b/apps/client/src/mobile.ts index 5d88ec1c4..805ffe276 100644 --- a/apps/client/src/mobile.ts +++ b/apps/client/src/mobile.ts @@ -2,6 +2,8 @@ import appContext from "./components/app_context.js"; import noteAutocompleteService from "./services/note_autocomplete.js"; import glob from "./services/glob.js"; import "./stylesheets/bootstrap.scss"; +import "boxicons/css/boxicons.min.css"; +import "autocomplete.js/index_jquery.js"; glob.setupGlobs(); diff --git a/apps/client/src/runtime.ts b/apps/client/src/runtime.ts new file mode 100644 index 000000000..50c385778 --- /dev/null +++ b/apps/client/src/runtime.ts @@ -0,0 +1,5 @@ +import $ from "jquery"; +(window as any).$ = $; +(window as any).jQuery = $; + +$("body").show(); diff --git a/apps/client/src/server_types.ts b/apps/client/src/server_types.ts index df6e1a7fc..34de9ecc4 100644 --- a/apps/client/src/server_types.ts +++ b/apps/client/src/server_types.ts @@ -8,7 +8,7 @@ interface Entity { export interface EntityChange { id?: number | null; noteId?: string; - entityName: EntityRowNames; + entityName: EntityType; entityId: string; entity?: Entity; positions?: Record; @@ -22,3 +22,5 @@ export interface EntityChange { changeId?: string | null; instanceId?: string | null; } + +export type EntityType = "notes" | "branches" | "attributes" | "note_reordering" | "revisions" | "options" | "attachments" | "blobs" | "etapi_tokens"; diff --git a/apps/client/src/services/bundle.ts b/apps/client/src/services/bundle.ts index e6eea7ef1..e7b88a343 100644 --- a/apps/client/src/services/bundle.ts +++ b/apps/client/src/services/bundle.ts @@ -1,6 +1,6 @@ import ScriptContext from "./script_context.js"; import server from "./server.js"; -import toastService from "./toast.js"; +import toastService, { showError } from "./toast.js"; import froca from "./froca.js"; import utils from "./utils.js"; import { t } from "./i18n.js"; @@ -37,7 +37,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont } catch (e: any) { const note = await froca.getNote(bundle.noteId); - toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`); + const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`; + showError(message); + logError(message); } } diff --git a/apps/client/src/services/clipboard.ts b/apps/client/src/services/clipboard.ts index feffee065..9ca5f9d09 100644 --- a/apps/client/src/services/clipboard.ts +++ b/apps/client/src/services/clipboard.ts @@ -4,6 +4,7 @@ import froca from "./froca.js"; import linkService from "./link.js"; import utils from "./utils.js"; import { t } from "./i18n.js"; +import { throwError } from "./ws.js"; let clipboardBranchIds: string[] = []; let clipboardMode: string | null = null; @@ -36,7 +37,7 @@ async function pasteAfter(afterBranchId: string) { // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places } else { - toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); + throwError(`Unrecognized clipboard mode=${clipboardMode}`); } } @@ -68,7 +69,7 @@ async function pasteInto(parentBranchId: string) { // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places } else { - toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); + throwError(`Unrecognized clipboard mode=${clipboardMode}`); } } @@ -79,7 +80,7 @@ async function copy(branchIds: string[]) { if (utils.isElectron()) { // https://github.com/zadam/trilium/issues/2401 const { clipboard } = require("electron"); - const links = []; + const links: string[] = []; for (const branch of froca.getBranches(clipboardBranchIds)) { const $link = await linkService.createLink(`${branch.parentNoteId}/${branch.noteId}`, { referenceLink: true }); diff --git a/apps/client/src/services/clipboard_ext.ts b/apps/client/src/services/clipboard_ext.ts new file mode 100644 index 000000000..9ab98af68 --- /dev/null +++ b/apps/client/src/services/clipboard_ext.ts @@ -0,0 +1,37 @@ +export function copyText(text: string) { + if (!text) { + return; + } + try { + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + return true; + } else { + // Fallback method: https://stackoverflow.com/a/72239825 + const textArea = document.createElement("textarea"); + textArea.value = text; + try { + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + return document.execCommand('copy'); + } finally { + document.body.removeChild(textArea); + } + } + } catch (e) { + console.warn(e); + return false; + } +} + +export async function copyTextWithToast(text: string) { + const t = (await import("./i18n.js")).t; + const toast = (await import("./toast.js")).default; + + if (copyText(text)) { + toast.showMessage(t("clipboard.copy_success")); + } else { + toast.showError(t("clipboard.copy_failed")); + } +} diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index 0bf7bca28..08ed561ff 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -1,7 +1,6 @@ import renderService from "./render.js"; import protectedSessionService from "./protected_session.js"; import protectedSessionHolder from "./protected_session_holder.js"; -import libraryLoader from "./library_loader.js"; import openService from "./open.js"; import froca from "./froca.js"; import utils from "./utils.js"; @@ -10,12 +9,13 @@ import treeService from "./tree.js"; import FNote from "../entities/fnote.js"; import FAttachment from "../entities/fattachment.js"; import imageContextMenuService from "../menus/image_context_menu.js"; -import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js"; +import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; -import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js"; import renderDoc from "./doc_renderer.js"; -import { t } from "i18next"; +import { t } from "../services/i18n.js"; import WheelZoom from 'vanilla-js-wheel-zoom'; +import { renderMathInElement } from "./math.js"; +import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; let idCounter = 1; @@ -94,8 +94,6 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery').html(blob.content)); if ($renderedContent.find("span.math-tex").length > 0) { - await libraryLoader.requireLibrary(libraryLoader.KATEX); - renderMathInElement($renderedContent[0], { trust: true }); } @@ -108,7 +106,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery, closeActDialog = true) { + if (closeActDialog) { + closeActiveDialog(); + glob.activeDialog = $dialog; + } + + saveFocusedElement(); + Modal.getOrCreateInstance($dialog[0]).show(); + + $dialog.on("hidden.bs.modal", () => { + const $autocompleteEl = $(".aa-input"); + if ("autocomplete" in $autocompleteEl) { + $autocompleteEl.autocomplete("close"); + } + + if (!glob.activeDialog || glob.activeDialog === $dialog) { + focusSavedElement(); + } + }); + + const keyboardActionsService = (await import("./keyboard_actions.js")).default; + keyboardActionsService.updateDisplayedShortcuts($dialog); + + return $dialog; +} + +export function closeActiveDialog() { + if (glob.activeDialog) { + Modal.getOrCreateInstance(glob.activeDialog[0]).hide(); + glob.activeDialog = null; + } +} async function info(message: string) { return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); diff --git a/apps/client/src/services/doc_renderer.ts b/apps/client/src/services/doc_renderer.ts index c8dac0ac0..1eca3d7c6 100644 --- a/apps/client/src/services/doc_renderer.ts +++ b/apps/client/src/services/doc_renderer.ts @@ -1,6 +1,6 @@ import type FNote from "../entities/fnote.js"; import { getCurrentLanguage } from "./i18n.js"; -import { applySyntaxHighlight } from "./syntax_highlight.js"; +import { formatCodeBlocks } from "./syntax_highlight.js"; export default function renderDoc(note: FNote) { return new Promise>((resolve) => { @@ -41,12 +41,13 @@ function processContent(url: string, $content: JQuery) { $img.attr("src", dir + "/" + $img.attr("src")); }); - applySyntaxHighlight($content); + formatCodeBlocks($content); } function getUrl(docNameValue: string, language: string) { // Cannot have spaces in the URL due to how JQuery.load works. docNameValue = docNameValue.replaceAll(" ", "%20"); - return `${window.glob.appPath}/doc_notes/${language}/${docNameValue}.html`; + const basePath = window.glob.isDev ? new URL(window.glob.assetPath).pathname : window.glob.assetPath; + return `${basePath}/doc_notes/${language}/${docNameValue}.html`; } diff --git a/apps/client/src/services/focus.ts b/apps/client/src/services/focus.ts new file mode 100644 index 000000000..066c74558 --- /dev/null +++ b/apps/client/src/services/focus.ts @@ -0,0 +1,29 @@ +let $lastFocusedElement: JQuery | null; + +// perhaps there should be saved focused element per tab? +export function saveFocusedElement() { + $lastFocusedElement = $(":focus"); +} + +export function focusSavedElement() { + if (!$lastFocusedElement) { + return; + } + + if ($lastFocusedElement.hasClass("ck")) { + // must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607 + // the bug manifests itself in resetting the cursor position to the first character - jumping above + + const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance"); + + if (editor) { + editor.editing.view.focus(); + } else { + console.log("Could not find CKEditor instance to focus last element"); + } + } else { + $lastFocusedElement.focus(); + } + + $lastFocusedElement = null; +} diff --git a/apps/client/src/services/froca.ts b/apps/client/src/services/froca.ts index 131cec06f..6bbc3a50d 100644 --- a/apps/client/src/services/froca.ts +++ b/apps/client/src/services/froca.ts @@ -245,6 +245,10 @@ class FrocaImpl implements Froca { } async getNotes(noteIds: string[] | JQuery, silentNotFoundError = false): Promise { + if (noteIds.length === 0) { + return []; + } + noteIds = Array.from(new Set(noteIds)); // make unique const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]); diff --git a/apps/client/src/services/froca_updater.ts b/apps/client/src/services/froca_updater.ts index 37f9d2814..b528047ed 100644 --- a/apps/client/src/services/froca_updater.ts +++ b/apps/client/src/services/froca_updater.ts @@ -36,7 +36,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) { } else if (ec.entityName === "attachments") { processAttachment(loadResults, ec); } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { - // NOOP + // NOOP - these entities are handled at the backend level and don't require frontend processing } else { throw new Error(`Unknown entityName '${ec.entityName}'`); } @@ -50,7 +50,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) { // To this we count: standard parent-child relationships and template/inherit relations (attribute inheritance follows them). // Here we watch for changes which might violate this principle - e.g., an introduction of a new "inherit" relation might // mean we need to load the target of the relation (and then perhaps transitively the whole note path of this target). - const missingNoteIds = []; + const missingNoteIds: string[] = []; for (const { entityName, entity } of entityChanges) { if (!entity) { diff --git a/apps/client/src/services/glob.ts b/apps/client/src/services/glob.ts index 3dafc03fd..6e261b4c8 100644 --- a/apps/client/src/services/glob.ts +++ b/apps/client/src/services/glob.ts @@ -1,11 +1,9 @@ import utils from "./utils.js"; import appContext from "../components/app_context.js"; import server from "./server.js"; -import libraryLoader from "./library_loader.js"; import ws from "./ws.js"; import froca from "./froca.js"; import linkService from "./link.js"; -import { lint } from "./eslint.js"; function setupGlobs() { window.glob.isDesktop = utils.isDesktop; @@ -18,8 +16,6 @@ function setupGlobs() { // required for ESLint plugin and CKEditor window.glob.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); - window.glob.requireLibrary = libraryLoader.requireLibrary; - window.glob.linter = lint; window.glob.appContext = appContext; // for debugging window.glob.froca = froca; window.glob.treeCache = froca; // compatibility for CKEditor builds for a while @@ -30,12 +26,18 @@ function setupGlobs() { window.onerror = function (msg, url, lineNo, columnNo, error) { const string = String(msg).toLowerCase(); + let errorObjectString = ""; + try { + errorObjectString = JSON.stringify(error); + } catch (e: any) { + errorObjectString = e.toString(); + } let message = "Uncaught error: "; if (string.includes("script error")) { message += "No details available"; } else { - message += [`Message: ${msg}`, `URL: ${url}`, `Line: ${lineNo}`, `Column: ${columnNo}`, `Error object: ${JSON.stringify(error)}`, `Stack: ${error && error.stack}`].join(", "); + message += [`Message: ${msg}`, `URL: ${url}`, `Line: ${lineNo}`, `Column: ${columnNo}`, `Error object: ${errorObjectString}`, `Stack: ${error && error.stack}`].join(", "); } ws.logError(message); @@ -66,7 +68,7 @@ function setupGlobs() { }); for (const appCssNoteId of glob.appCssNoteIds || []) { - libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`, false); + requireCss(`api/notes/download/${appCssNoteId}`, false); } utils.initHelpButtons($(window)); @@ -78,6 +80,18 @@ function setupGlobs() { }); } +async function requireCss(url: string, prependAssetPath = true) { + const cssLinks = Array.from(document.querySelectorAll("link")).map((el) => el.href); + + if (!cssLinks.some((l) => l.endsWith(url))) { + if (prependAssetPath) { + url = `${window.glob.assetPath}/${url}`; + } + + $("head").append($('').attr("href", url)); + } +} + export default { setupGlobs }; diff --git a/apps/client/src/services/i18n.spec.ts b/apps/client/src/services/i18n.spec.ts index 9143097be..e64605949 100644 --- a/apps/client/src/services/i18n.spec.ts +++ b/apps/client/src/services/i18n.spec.ts @@ -1,6 +1,7 @@ import { LOCALES } from "@triliumnext/commons"; import { readFileSync } from "fs"; import { join } from "path"; +import { describe, expect, it } from "vitest"; describe("i18n", () => { it("translations are valid JSON", () => { diff --git a/apps/client/src/services/image.ts b/apps/client/src/services/image.ts index 3cf1424d5..f13a9a3c7 100644 --- a/apps/client/src/services/image.ts +++ b/apps/client/src/services/image.ts @@ -1,5 +1,5 @@ import { t } from "./i18n.js"; -import toastService from "./toast.js"; +import toastService, { showError } from "./toast.js"; function copyImageReferenceToClipboard($imageWrapper: JQuery) { try { @@ -11,7 +11,9 @@ function copyImageReferenceToClipboard($imageWrapper: JQuery) { if (success) { toastService.showMessage(t("image.copied-to-clipboard")); } else { - toastService.showAndLogError(t("image.cannot-copy")); + const message = t("image.cannot-copy"); + showError(message); + logError(message); } } finally { window.getSelection()?.removeAllRanges(); diff --git a/apps/client/src/services/keyboard_actions.ts b/apps/client/src/services/keyboard_actions.ts index dfa888620..3cb0ffd33 100644 --- a/apps/client/src/services/keyboard_actions.ts +++ b/apps/client/src/services/keyboard_actions.ts @@ -115,6 +115,7 @@ function updateDisplayedShortcuts($container: JQuery) { export default { updateDisplayedShortcuts, setupActionsForElement, + getAction, getActions, getActionsForScope }; diff --git a/apps/client/src/services/library_loader.ts b/apps/client/src/services/library_loader.ts deleted file mode 100644 index 90a19cd72..000000000 --- a/apps/client/src/services/library_loader.ts +++ /dev/null @@ -1,158 +0,0 @@ -import mimeTypesService from "./mime_types.js"; -import optionsService from "./options.js"; -import { getStylesheetUrl } from "./syntax_highlight.js"; - -export interface Library { - js?: string[] | (() => string[]); - css?: string[]; -} - -const CODE_MIRROR: Library = { - js: () => { - const scriptsToLoad = [ - "node_modules/codemirror/lib/codemirror.js", - "node_modules/codemirror/addon/display/placeholder.js", - "node_modules/codemirror/addon/edit/matchbrackets.js", - "node_modules/codemirror/addon/edit/matchtags.js", - "node_modules/codemirror/addon/fold/xml-fold.js", - "node_modules/codemirror/addon/lint/lint.js", - "node_modules/codemirror/addon/mode/loadmode.js", - "node_modules/codemirror/addon/mode/multiplex.js", - "node_modules/codemirror/addon/mode/overlay.js", - "node_modules/codemirror/addon/mode/simple.js", - "node_modules/codemirror/addon/search/match-highlighter.js", - "node_modules/codemirror/mode/meta.js", - "node_modules/codemirror/keymap/vim.js", - "libraries/codemirror/eslint.js" - ]; - - const mimeTypes = mimeTypesService.getMimeTypes(); - for (const mimeType of mimeTypes) { - if (mimeType.enabled && mimeType.codeMirrorSource) { - scriptsToLoad.push(mimeType.codeMirrorSource); - } - } - - return scriptsToLoad; - }, - css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"] -}; - -const KATEX: Library = { - js: ["node_modules/katex/dist/katex.min.js", "node_modules/katex/dist/contrib/mhchem.min.js", "node_modules/katex/dist/contrib/auto-render.min.js"], - css: ["node_modules/katex/dist/katex.min.css"] -}; - -const HIGHLIGHT_JS: Library = { - js: () => { - const mimeTypes = mimeTypesService.getMimeTypes(); - const scriptsToLoad = new Set(); - scriptsToLoad.add("node_modules/@highlightjs/cdn-assets/highlight.min.js"); - for (const mimeType of mimeTypes) { - const id = mimeType.highlightJs; - if (!mimeType.enabled || !id) { - continue; - } - - if (mimeType.highlightJsSource === "libraries") { - scriptsToLoad.add(`libraries/highlightjs/${id}.js`); - } else { - // Built-in module. - scriptsToLoad.add(`node_modules/@highlightjs/cdn-assets/languages/${id}.min.js`); - } - } - - const currentTheme = String(optionsService.get("codeBlockTheme")); - loadHighlightingTheme(currentTheme); - - return Array.from(scriptsToLoad); - } -}; - -async function requireLibrary(library: Library) { - if (library.css) { - library.css.map((cssUrl) => requireCss(cssUrl)); - } - - if (library.js) { - for (const scriptUrl of await unwrapValue(library.js)) { - await requireScript(scriptUrl); - } - } -} - -async function unwrapValue(value: T | (() => T) | Promise) { - if (value && typeof value === "object" && "then" in value) { - return (await (value as Promise<() => T>))(); - } - - if (typeof value === "function") { - return (value as () => T)(); - } - - return value; -} - -// we save the promises in case of the same script being required concurrently multiple times -const loadedScriptPromises: Record = {}; - -async function requireScript(url: string) { - url = `${window.glob.assetPath}/${url}`; - - if (!loadedScriptPromises[url]) { - loadedScriptPromises[url] = $.ajax({ - url: url, - dataType: "script", - cache: true - }); - } - - await loadedScriptPromises[url]; -} - -async function requireCss(url: string, prependAssetPath = true) { - const cssLinks = Array.from(document.querySelectorAll("link")).map((el) => el.href); - - if (!cssLinks.some((l) => l.endsWith(url))) { - if (prependAssetPath) { - url = `${window.glob.assetPath}/${url}`; - } - - $("head").append($('').attr("href", url)); - } -} - -let highlightingThemeEl: JQuery | null = null; -function loadHighlightingTheme(theme: string) { - if (!theme) { - return; - } - - if (theme === "none") { - // Deactivate the theme. - if (highlightingThemeEl) { - highlightingThemeEl.remove(); - highlightingThemeEl = null; - } - return; - } - - if (!highlightingThemeEl) { - highlightingThemeEl = $(``); - $("head").append(highlightingThemeEl); - } - - const url = getStylesheetUrl(theme); - if (url) { - highlightingThemeEl.attr("href", url); - } -} - -export default { - requireCss, - requireLibrary, - loadHighlightingTheme, - CODE_MIRROR, - KATEX, - HIGHLIGHT_JS -}; diff --git a/apps/client/src/services/link.spec.ts b/apps/client/src/services/link.spec.ts index 60812ccf9..eb52c3efc 100644 --- a/apps/client/src/services/link.spec.ts +++ b/apps/client/src/services/link.spec.ts @@ -16,4 +16,29 @@ describe("Link", () => { const output = parseNavigationStateFromUrl(`#root/WWaBNf3SSA1b/mQ2tIzLVFKHL`); expect(output).toMatchObject({ notePath: "root/WWaBNf3SSA1b/mQ2tIzLVFKHL", noteId: "mQ2tIzLVFKHL" }); }); + + it("parses notePath with spaces", () => { + const output = parseNavigationStateFromUrl(` #root/WWaBNf3SSA1b/mQ2tIzLVFKHL`); + expect(output).toMatchObject({ notePath: "root/WWaBNf3SSA1b/mQ2tIzLVFKHL", noteId: "mQ2tIzLVFKHL" }); + }); + + it("parses notePath with extraWindow", () => { + const output = parseNavigationStateFromUrl(`127.0.0.1:8080/?extraWindow=1#root/QZGqKB7wVZF8?ntxId=0XPvXG`); + expect(output).toMatchObject({ notePath: "root/QZGqKB7wVZF8", noteId: "QZGqKB7wVZF8" }); + }); + + it("ignores external URL with internal hash anchor", () => { + const output = parseNavigationStateFromUrl(`https://en.wikipedia.org/wiki/Bearded_Collie#Health`); + expect(output).toMatchObject({}); + }); + + it("ignores malformed but hash-containing external URL", () => { + const output = parseNavigationStateFromUrl("https://abc.com/#drop?searchString=firefox"); + expect(output).toStrictEqual({}); + }); + + it("ignores non-hash internal path", () => { + const output = parseNavigationStateFromUrl("/root/abc123"); + expect(output).toStrictEqual({}); + }); }); diff --git a/apps/client/src/services/link.ts b/apps/client/src/services/link.ts index a0d464741..116ca8a5b 100644 --- a/apps/client/src/services/link.ts +++ b/apps/client/src/services/link.ts @@ -48,6 +48,13 @@ export interface ViewScope { viewMode?: ViewMode; attachmentId?: string; readOnlyTemporarilyDisabled?: boolean; + /** + * If true, it indicates that the note in the view should be opened in read-only mode (for supported note types such as text or code). + * + * The reason why we store this information here is that a note can become read-only as the user types content in it, and we wouldn't want + * to immediately enter read-only mode. + */ + isReadOnly?: boolean; highlightsListPreviousVisible?: boolean; highlightsListTemporarilyHidden?: boolean; tocTemporarilyHidden?: boolean; @@ -58,6 +65,7 @@ export interface ViewScope { * toc will appear and then close immediately, because getToc(html) function will consume time */ tocPreviousVisible?: boolean; + tocCollapsedHeadings?: Set; } interface CreateLinkOptions { @@ -203,20 +211,26 @@ export function parseNavigationStateFromUrl(url: string | undefined) { return {}; } + url = url.trim(); const hashIdx = url.indexOf("#"); if (hashIdx === -1) { return {}; } + // Exclude external links that contain # + if (hashIdx !== 0 && !url.includes("/#root") && !url.includes("/#?searchString") && !url.includes("/?extraWindow")) { + return {}; + } + const hash = url.substr(hashIdx + 1); // strip also the initial '#' let [notePath, paramString] = hash.split("?"); const viewScope: ViewScope = { viewMode: "default" }; - let ntxId = null; - let hoistedNoteId = null; - let searchString = null; + let ntxId: string | null = null; + let hoistedNoteId: string | null = null; + let searchString: string | null = null; if (paramString) { for (const pair of paramString.split("&")) { @@ -271,8 +285,10 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent evt.preventDefault(); evt.stopPropagation(); - if (hrefLink?.startsWith("#fn") && $link) { - return handleFootnote(hrefLink, $link); + if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) { + if (handleAnchor(hrefLink, $link)) { + return true; + } } const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); @@ -334,18 +350,19 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent } /** - * Scrolls to either the footnote (if clicking on a reference such as `[1]`), or to the reference of a footnote (if clicking on the footnote `^` arrow). + * Scrolls to either the footnote (if clicking on a reference such as `[1]`), or to the reference of a footnote (if clicking on the footnote `^` arrow), + * or CKEditor bookmarks. * * @param hrefLink the URL of the link that was clicked (it should be in the form of `#fn` or `#fnref`). * @param $link the element of the link that was clicked. - * @returns whether the event should be consumed or not. + * @returns `true` if the link was handled (i.e., the element was found and scrolled to), `false` otherwise. */ -function handleFootnote(hrefLink: string, $link: JQuery) { +function handleAnchor(hrefLink: string, $link: JQuery) { const el = $link.closest(".ck-content").find(hrefLink)[0]; if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); } - return true; + return !!el; } function linkContextMenu(e: PointerEvent) { diff --git a/apps/client/src/services/load_results.ts b/apps/client/src/services/load_results.ts index 11f9a1a11..5f9db2db7 100644 --- a/apps/client/src/services/load_results.ts +++ b/apps/client/src/services/load_results.ts @@ -44,10 +44,7 @@ interface OptionRow {} interface NoteReorderingRow {} -interface ContentNoteIdToComponentIdRow { - noteId: string; - componentId: string; -} + type EntityRowMappings = { notes: NoteRow; diff --git a/apps/client/src/services/math.ts b/apps/client/src/services/math.ts new file mode 100644 index 000000000..2a5fc45b2 --- /dev/null +++ b/apps/client/src/services/math.ts @@ -0,0 +1,5 @@ +import katex from "katex"; +import "katex/contrib/mhchem"; +import "katex/dist/katex.min.css"; +export { default as renderMathInElement } from "katex/contrib/auto-render"; +export default katex; diff --git a/apps/client/src/services/mime_type_definitions.ts b/apps/client/src/services/mime_type_definitions.ts deleted file mode 100644 index 26e7011b0..000000000 --- a/apps/client/src/services/mime_type_definitions.ts +++ /dev/null @@ -1,221 +0,0 @@ -// TODO: deduplicate with /src/services/import/mime_type_definitions.ts - -/** - * A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics. - */ -export const MIME_TYPE_AUTO = "text-x-trilium-auto"; - -export interface MimeTypeDefinition { - default?: boolean; - title: string; - mime: string; - /** The name of the language/mime type as defined by highlight.js (or one of the aliases), in order to be used for syntax highlighting such as inside code blocks. */ - highlightJs?: string; - /** If specified, will load the corresponding highlight.js file from the `libraries/highlightjs/${id}.js` instead of `node_modules/@highlightjs/cdn-assets/languages/${id}.min.js`. */ - highlightJsSource?: "libraries"; - /** If specified, will load the corresponding highlight file from the given path instead of `node_modules`. */ - codeMirrorSource?: string; -} - -/** - * For highlight.js-supported languages, see https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md. - */ - -export const MIME_TYPES_DICT: readonly MimeTypeDefinition[] = Object.freeze([ - { title: "Plain text", mime: "text/plain", highlightJs: "plaintext", default: true }, - - // Keep sorted alphabetically. - { title: "APL", mime: "text/apl" }, - { title: "ASN.1", mime: "text/x-ttcn-asn" }, - { title: "ASP.NET", mime: "application/x-aspx" }, - { title: "Asterisk", mime: "text/x-asterisk" }, - { title: "Batch file (DOS)", mime: "application/x-bat", highlightJs: "dos", codeMirrorSource: "libraries/codemirror/batch.js" }, - { title: "Brainfuck", mime: "text/x-brainfuck", highlightJs: "brainfuck" }, - { title: "C", mime: "text/x-csrc", highlightJs: "c", default: true }, - { title: "C#", mime: "text/x-csharp", highlightJs: "csharp", default: true }, - { title: "C++", mime: "text/x-c++src", highlightJs: "cpp", default: true }, - { title: "Clojure", mime: "text/x-clojure", highlightJs: "clojure" }, - { title: "ClojureScript", mime: "text/x-clojurescript" }, - { title: "Closure Stylesheets (GSS)", mime: "text/x-gss" }, - { title: "CMake", mime: "text/x-cmake", highlightJs: "cmake" }, - { title: "Cobol", mime: "text/x-cobol" }, - { title: "CoffeeScript", mime: "text/coffeescript", highlightJs: "coffeescript" }, - { title: "Common Lisp", mime: "text/x-common-lisp", highlightJs: "lisp" }, - { title: "CQL", mime: "text/x-cassandra" }, - { title: "Crystal", mime: "text/x-crystal", highlightJs: "crystal" }, - { title: "CSS", mime: "text/css", highlightJs: "css", default: true }, - { title: "Cypher", mime: "application/x-cypher-query" }, - { title: "Cython", mime: "text/x-cython" }, - { title: "D", mime: "text/x-d", highlightJs: "d" }, - { title: "Dart", mime: "application/dart", highlightJs: "dart" }, - { title: "diff", mime: "text/x-diff", highlightJs: "diff" }, - { title: "Django", mime: "text/x-django", highlightJs: "django" }, - { title: "Dockerfile", mime: "text/x-dockerfile", highlightJs: "dockerfile" }, - { title: "DTD", mime: "application/xml-dtd" }, - { title: "Dylan", mime: "text/x-dylan" }, - { title: "EBNF", mime: "text/x-ebnf", highlightJs: "ebnf" }, - { title: "ECL", mime: "text/x-ecl" }, - { title: "edn", mime: "application/edn" }, - { title: "Eiffel", mime: "text/x-eiffel" }, - { title: "Elm", mime: "text/x-elm", highlightJs: "elm" }, - { title: "Embedded Javascript", mime: "application/x-ejs" }, - { title: "Embedded Ruby", mime: "application/x-erb", highlightJs: "erb" }, - { title: "Erlang", mime: "text/x-erlang", highlightJs: "erlang" }, - { title: "Esper", mime: "text/x-esper" }, - { title: "F#", mime: "text/x-fsharp", highlightJs: "fsharp" }, - { title: "Factor", mime: "text/x-factor" }, - { title: "FCL", mime: "text/x-fcl" }, - { title: "Forth", mime: "text/x-forth" }, - { title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" }, - { title: "Gas", mime: "text/x-gas" }, - { title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" }, - { title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" }, - { title: "Go", mime: "text/x-go", highlightJs: "go", default: true }, - { title: "Groovy", mime: "text/x-groovy", highlightJs: "groovy", default: true }, - { title: "HAML", mime: "text/x-haml", highlightJs: "haml" }, - { title: "Haskell (Literate)", mime: "text/x-literate-haskell" }, - { title: "Haskell", mime: "text/x-haskell", highlightJs: "haskell", default: true }, - { title: "Haxe", mime: "text/x-haxe", highlightJs: "haxe" }, - { title: "HTML", mime: "text/html", highlightJs: "xml", default: true }, - { title: "HTTP", mime: "message/http", highlightJs: "http", default: true }, - { title: "HXML", mime: "text/x-hxml" }, - { title: "IDL", mime: "text/x-idl" }, - { title: "Java Server Pages", mime: "application/x-jsp", highlightJs: "java" }, - { title: "Java", mime: "text/x-java", highlightJs: "java", default: true }, - { title: "Jinja2", mime: "text/jinja2" }, - { title: "JS backend", mime: "application/javascript;env=backend", highlightJs: "javascript", default: true }, - { title: "JS frontend", mime: "application/javascript;env=frontend", highlightJs: "javascript", default: true }, - { title: "JSON-LD", mime: "application/ld+json", highlightJs: "json" }, - { title: "JSON", mime: "application/json", highlightJs: "json", default: true }, - { title: "JSX", mime: "text/jsx", highlightJs: "javascript" }, - { title: "Julia", mime: "text/x-julia", highlightJs: "julia" }, - { title: "Kotlin", mime: "text/x-kotlin", highlightJs: "kotlin", default: true }, - { title: "LaTeX", mime: "text/x-latex", highlightJs: "latex" }, - { title: "LESS", mime: "text/x-less", highlightJs: "less" }, - { title: "LiveScript", mime: "text/x-livescript", highlightJs: "livescript" }, - { title: "Lua", mime: "text/x-lua", highlightJs: "lua" }, - { title: "MariaDB SQL", mime: "text/x-mariadb", highlightJs: "sql" }, - { title: "Markdown", mime: "text/x-markdown", highlightJs: "markdown", default: true }, - { title: "Mathematica", mime: "text/x-mathematica", highlightJs: "mathematica" }, - { title: "mbox", mime: "application/mbox" }, - { title: "MIPS Assembler", mime: "text/x-asm-mips", highlightJs: "mipsasm" }, - { title: "mIRC", mime: "text/mirc" }, - { title: "Modelica", mime: "text/x-modelica" }, - { title: "MS SQL", mime: "text/x-mssql", highlightJs: "sql" }, - { title: "mscgen", mime: "text/x-mscgen" }, - { title: "msgenny", mime: "text/x-msgenny" }, - { title: "MUMPS", mime: "text/x-mumps" }, - { title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" }, - { title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" }, - { title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" }, - { title: "NTriples", mime: "application/n-triples" }, - { title: "Objective-C", mime: "text/x-objectivec", highlightJs: "objectivec" }, - { title: "OCaml", mime: "text/x-ocaml", highlightJs: "ocaml" }, - { title: "Octave", mime: "text/x-octave" }, - { title: "Oz", mime: "text/x-oz" }, - { title: "Pascal", mime: "text/x-pascal", highlightJs: "delphi" }, - { title: "PEG.js", mime: "null" }, - { title: "Perl", mime: "text/x-perl", default: true }, - { title: "PGP", mime: "application/pgp" }, - { title: "PHP", mime: "text/x-php", default: true, highlightJs: "php" }, - { title: "Pig", mime: "text/x-pig" }, - { title: "PLSQL", mime: "text/x-plsql", highlightJs: "sql" }, - { title: "PostgreSQL", mime: "text/x-pgsql", highlightJs: "pgsql" }, - { title: "PowerShell", mime: "application/x-powershell", highlightJs: "powershell" }, - { title: "Properties files", mime: "text/x-properties", highlightJs: "properties" }, - { title: "ProtoBuf", mime: "text/x-protobuf", highlightJs: "protobuf" }, - { title: "Pug", mime: "text/x-pug" }, - { title: "Puppet", mime: "text/x-puppet", highlightJs: "puppet" }, - { title: "Python", mime: "text/x-python", highlightJs: "python", default: true }, - { title: "Q", mime: "text/x-q", highlightJs: "q" }, - { title: "R", mime: "text/x-rsrc", highlightJs: "r" }, - { title: "reStructuredText", mime: "text/x-rst" }, - { title: "RPM Changes", mime: "text/x-rpm-changes" }, - { title: "RPM Spec", mime: "text/x-rpm-spec" }, - { title: "Ruby", mime: "text/x-ruby", highlightJs: "ruby", default: true }, - { title: "Rust", mime: "text/x-rustsrc", highlightJs: "rust" }, - { title: "SAS", mime: "text/x-sas", highlightJs: "sas" }, - { title: "Sass", mime: "text/x-sass", highlightJs: "scss" }, - { title: "Scala", mime: "text/x-scala" }, - { title: "Scheme", mime: "text/x-scheme" }, - { title: "SCSS", mime: "text/x-scss", highlightJs: "scss" }, - { title: "Shell (bash)", mime: "text/x-sh", highlightJs: "bash", default: true }, - { title: "Sieve", mime: "application/sieve" }, - { title: "Slim", mime: "text/x-slim" }, - { title: "Smalltalk", mime: "text/x-stsrc", highlightJs: "smalltalk" }, - { title: "Smarty", mime: "text/x-smarty" }, - { title: "SML", mime: "text/x-sml", highlightJs: "sml" }, - { title: "Solr", mime: "text/x-solr" }, - { title: "Soy", mime: "text/x-soy" }, - { title: "SPARQL", mime: "application/sparql-query" }, - { title: "Spreadsheet", mime: "text/x-spreadsheet" }, - { title: "SQL", mime: "text/x-sql", highlightJs: "sql", default: true }, - { title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium", highlightJs: "sql", default: true }, - { title: "SQLite", mime: "text/x-sqlite", highlightJs: "sql" }, - { title: "Squirrel", mime: "text/x-squirrel" }, - { title: "sTeX", mime: "text/x-stex" }, - { title: "Stylus", mime: "text/x-styl", highlightJs: "stylus" }, - { title: "Swift", mime: "text/x-swift", default: true }, - { title: "SystemVerilog", mime: "text/x-systemverilog" }, - { title: "Tcl", mime: "text/x-tcl", highlightJs: "tcl" }, - { title: "Terraform (HCL)", mime: "text/x-hcl", highlightJs: "terraform", highlightJsSource: "libraries", codeMirrorSource: "libraries/codemirror/hcl.js" }, - { title: "Textile", mime: "text/x-textile" }, - { title: "TiddlyWiki ", mime: "text/x-tiddlywiki" }, - { title: "Tiki wiki", mime: "text/tiki" }, - { title: "TOML", mime: "text/x-toml", highlightJs: "ini" }, - { title: "Tornado", mime: "text/x-tornado" }, - { title: "troff", mime: "text/troff" }, - { title: "TTCN_CFG", mime: "text/x-ttcn-cfg" }, - { title: "TTCN", mime: "text/x-ttcn" }, - { title: "Turtle", mime: "text/turtle" }, - { title: "Twig", mime: "text/x-twig", highlightJs: "twig" }, - { title: "TypeScript-JSX", mime: "text/typescript-jsx", highlightJs: "typescript" }, - { title: "TypeScript", mime: "application/typescript", highlightJs: "typescript" }, - { title: "VB.NET", mime: "text/x-vb", highlightJs: "vbnet" }, - { title: "VBScript", mime: "text/vbscript", highlightJs: "vbscript" }, - { title: "Velocity", mime: "text/velocity" }, - { title: "Verilog", mime: "text/x-verilog", highlightJs: "verilog" }, - { title: "VHDL", mime: "text/x-vhdl", highlightJs: "vhdl" }, - { title: "Vue.js Component", mime: "text/x-vue" }, - { title: "Web IDL", mime: "text/x-webidl" }, - { title: "XML", mime: "text/xml", highlightJs: "xml", default: true }, - { title: "XQuery", mime: "application/xquery", highlightJs: "xquery" }, - { title: "xu", mime: "text/x-xu" }, - { title: "Yacas", mime: "text/x-yacas" }, - { title: "YAML", mime: "text/x-yaml", highlightJs: "yaml", default: true }, - { title: "Z80", mime: "text/x-z80" } -]); - -/** - * Given a MIME type in the usual format (e.g. `text/csrc`), it returns a MIME type that can be passed down to the CKEditor - * code plugin. - * - * @param mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`). - * @returns the normalized MIME type (e.g. `text-c-src`). - */ -export function normalizeMimeTypeForCKEditor(mimeType: string) { - return mimeType.toLowerCase().replace(/[\W_]+/g, "-"); -} - -let byHighlightJsNameMappings: Record | null = null; - -/** - * Given a Highlight.js language tag (e.g. `css`), it returns a corresponding {@link MimeTypeDefinition} if found. - * - * If there are multiple {@link MimeTypeDefinition}s for the language tag, then only the first one is retrieved. For example for `javascript`, the "JS frontend" mime type is returned. - * - * @param highlightJsName a language tag. - * @returns the corresponding {@link MimeTypeDefinition} if found, or `undefined` otherwise. - */ -export function getMimeTypeFromHighlightJs(highlightJsName: string) { - if (!byHighlightJsNameMappings) { - byHighlightJsNameMappings = {}; - for (const mimeType of MIME_TYPES_DICT) { - if (mimeType.highlightJs && !byHighlightJsNameMappings[mimeType.highlightJs]) { - byHighlightJsNameMappings[mimeType.highlightJs] = mimeType; - } - } - } - - return byHighlightJsNameMappings[highlightJsName]; -} diff --git a/apps/client/src/services/mime_types.ts b/apps/client/src/services/mime_types.ts index f0f318141..3b72c531c 100644 --- a/apps/client/src/services/mime_types.ts +++ b/apps/client/src/services/mime_types.ts @@ -1,13 +1,6 @@ -import { MIME_TYPE_AUTO, MIME_TYPES_DICT, normalizeMimeTypeForCKEditor, type MimeTypeDefinition } from "./mime_type_definitions.js"; +import { normalizeMimeTypeForCKEditor, type MimeType, MIME_TYPE_AUTO, MIME_TYPES_DICT } from "@triliumnext/commons"; import options from "./options.js"; -interface MimeType extends MimeTypeDefinition { - /** - * True if this mime type was enabled by the user in the "Available MIME types in the dropdown" option in the Code Notes settings. - */ - enabled: boolean; -} - let mimeTypes: MimeType[] | null = null; function loadMimeTypes() { @@ -45,8 +38,8 @@ export function getHighlightJsNameForMime(mimeType: string) { for (const mimeType of mimeTypes) { // The mime stored by CKEditor is text-x-csrc instead of text/x-csrc so we keep this format for faster lookup. const normalizedMime = normalizeMimeTypeForCKEditor(mimeType.mime); - if (mimeType.highlightJs) { - mimeToHighlightJsMapping[normalizedMime] = mimeType.highlightJs; + if (mimeType.mdLanguageCode) { + mimeToHighlightJsMapping[normalizedMime] = mimeType.mdLanguageCode; } } } diff --git a/apps/client/src/services/note_autocomplete.ts b/apps/client/src/services/note_autocomplete.ts index 4ffff8594..d6eb4df0e 100644 --- a/apps/client/src/services/note_autocomplete.ts +++ b/apps/client/src/services/note_autocomplete.ts @@ -289,13 +289,11 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { } if (suggestion.action === "create-note") { - const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType(); - + const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType(); if (!success) { return; } - - const { note } = await noteCreateService.createNote(suggestion.parentNoteId, { + const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, { title: suggestion.noteTitle, activate: false, type: noteType, diff --git a/apps/client/src/services/note_create.ts b/apps/client/src/services/note_create.ts index 5fa262553..6ce92bc0d 100644 --- a/apps/client/src/services/note_create.ts +++ b/apps/client/src/services/note_create.ts @@ -116,7 +116,7 @@ async function chooseNoteType() { } async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) { - const { success, noteType, templateNoteId } = await chooseNoteType(); + const { success, noteType, templateNoteId, notePath } = await chooseNoteType(); if (!success) { return; @@ -125,7 +125,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN options.type = noteType; options.templateNoteId = templateNoteId; - return await createNote(parentNotePath, options); + return await createNote(notePath || parentNotePath, options); } /* If the first element is heading, parse it out and use it as a new heading. */ diff --git a/apps/client/src/services/note_tooltip.ts b/apps/client/src/services/note_tooltip.ts index 19ac71801..4a2b88bb6 100644 --- a/apps/client/src/services/note_tooltip.ts +++ b/apps/client/src/services/note_tooltip.ts @@ -8,6 +8,10 @@ import appContext from "../components/app_context.js"; import type FNote from "../entities/fnote.js"; import { t } from "./i18n.js"; +// Track all elements that open tooltips +let openTooltipElements: JQuery[] = []; +let dismissTimer: ReturnType; + function setupGlobalTooltip() { $(document).on("mouseenter", "a", mouseEnterHandler); @@ -23,7 +27,12 @@ function setupGlobalTooltip() { } function dismissAllTooltips() { - $(".note-tooltip").remove(); + clearTimeout(dismissTimer); + openTooltipElements.forEach($el => { + $el.tooltip("dispose"); + $el.removeAttr("aria-describedby"); + }); + openTooltipElements = []; } function setupElementTooltip($el: JQuery) { @@ -64,8 +73,8 @@ async function mouseEnterHandler(this: HTMLElement) { } let renderPromise; - if (url?.startsWith("#fn")) { - renderPromise = renderFootnote($link, url); + if (url && url.startsWith("#") && !url.startsWith("#root/")) { + renderPromise = renderFootnoteOrAnchor($link, url); } else { renderPromise = renderTooltip(await froca.getNote(noteId)); } @@ -86,8 +95,8 @@ async function mouseEnterHandler(this: HTMLElement) { // we need to check if we're still hovering over the element // since the operation to get tooltip content was async, it is possible that // we now create tooltip which won't close because it won't receive mouseleave event - if ($(this).filter(":hover").length > 0) { - $(this).tooltip({ + if ($link.filter(":hover").length > 0) { + $link.tooltip({ container: "body", // https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988 // with bottom this flickering happens a bit less @@ -103,7 +112,9 @@ async function mouseEnterHandler(this: HTMLElement) { }); dismissAllTooltips(); - $(this).tooltip("show"); + $link.tooltip("show"); + + openTooltipElements.push($link); // Dismiss the tooltip immediately if a link was clicked inside the tooltip. $(`.${tooltipClass} a`).on("click", (e) => { @@ -115,15 +126,16 @@ async function mouseEnterHandler(this: HTMLElement) { // click on links within tooltip etc. without tooltip disappearing // - once the user moves the cursor away from both link and the tooltip, hide the tooltip const checkTooltip = () => { - if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) { + + if (!$link.filter(":hover").length && !$(`.${linkId}:hover`).length) { // cursor is neither over the link nor over the tooltip, user likely is not interested dismissAllTooltips(); } else { - setTimeout(checkTooltip, 1000); + dismissTimer = setTimeout(checkTooltip, 1000); } }; - setTimeout(checkTooltip, 1000); + dismissTimer = setTimeout(checkTooltip, 1000); } } @@ -166,17 +178,49 @@ async function renderTooltip(note: FNote | null) { return content; } -function renderFootnote($link: JQuery, url: string) { +function renderFootnoteOrAnchor($link: JQuery, url: string) { // A footnote text reference const footnoteRef = url.substring(3); - const $footnoteContent = $link - .closest(".ck-content") // find the parent CK content - .find("> .footnote-section") // find the footnote section - .find(`a[href="#fnref${footnoteRef}"]`) // find the footnote link - .closest(".footnote-item") // find the parent container of the footnote - .find(".footnote-content"); // find the actual text content of the footnote + let $targetContent: JQuery; - return $footnoteContent.html() || ""; + if (url.startsWith("#fn")) { + $targetContent = $link + .closest(".ck-content") // find the parent CK content + .find("> .footnote-section") // find the footnote section + .find(`a[href="#fnref${footnoteRef}"]`) // find the footnote link + .closest(".footnote-item") // find the parent container of the footnote + .find(".footnote-content"); // find the actual text content of the footnote + } else { + $targetContent = $link + .closest(".ck-content") + .find(url) + .closest("p"); + } + + if (!$targetContent.length) { + // If the target content is not found, return an empty string + return ""; + } + + const isEditable = $link.closest(".ck-content").hasClass("note-detail-editable-text-editor"); + if (isEditable) { + /* Remove widget buttons for tables, formulas, and images in editable notes. */ + $targetContent.find('.ck-widget__selection-handle').remove(); + $targetContent.find('.ck-widget__type-around').remove(); + $targetContent.find('.ck-widget__resizer').remove(); + + /* Handling in-line math formulas */ + $targetContent.find('.ck-math-tex.ck-math-tex-inline.ck-widget').each(function () { + const $katex = $(this).find('.katex').first(); + if ($katex.length) { + $(this).replaceWith($('').append($('').append($katex.clone()))); + } + }); + } + + let footnoteContent = $targetContent.html(); + footnoteContent = `
${footnoteContent}
` + return footnoteContent || ""; } export default { diff --git a/apps/client/src/services/note_types.ts b/apps/client/src/services/note_types.ts index 4de0bd8bf..e9905063c 100644 --- a/apps/client/src/services/note_types.ts +++ b/apps/client/src/services/note_types.ts @@ -4,6 +4,8 @@ import { t } from "./i18n.js"; import type { MenuItem } from "../menus/context_menu.js"; import type { TreeCommandNames } from "../menus/tree_context_menu.js"; +const SEPARATOR = { title: "----" }; + async function getNoteTypeItems(command?: TreeCommandNames) { const items: MenuItem[] = [ { title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" }, @@ -18,25 +20,59 @@ async function getNoteTypeItems(command?: TreeCommandNames) { { title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" }, { title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }, { title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" }, + ...await getBuiltInTemplates(command), + ...await getUserTemplates(command) ]; + return items; +} + +async function getUserTemplates(command?: TreeCommandNames) { const templateNoteIds = await server.get("search-templates"); const templateNotes = await froca.getNotes(templateNoteIds); - - if (templateNotes.length > 0) { - items.push({ title: "----" }); - - for (const templateNote of templateNotes) { - items.push({ - title: templateNote.title, - uiIcon: templateNote.getIcon(), - command: command, - type: templateNote.type, - templateNoteId: templateNote.noteId - }); - } + if (templateNotes.length === 0) { + return []; } + const items: MenuItem[] = [ + SEPARATOR + ]; + for (const templateNote of templateNotes) { + items.push({ + title: templateNote.title, + uiIcon: templateNote.getIcon(), + command: command, + type: templateNote.type, + templateNoteId: templateNote.noteId + }); + } + return items; +} + +async function getBuiltInTemplates(command?: TreeCommandNames) { + const templatesRoot = await froca.getNote("_templates"); + if (!templatesRoot) { + console.warn("Unable to find template root."); + return []; + } + + const childNotes = await templatesRoot.getChildNotes(); + if (childNotes.length === 0) { + return []; + } + + const items: MenuItem[] = [ + SEPARATOR + ]; + for (const templateNote of childNotes) { + items.push({ + title: templateNote.title, + uiIcon: templateNote.getIcon(), + command: command, + type: templateNote.type, + templateNoteId: templateNote.noteId + }); + } return items; } diff --git a/apps/client/src/services/options.ts b/apps/client/src/services/options.ts index c20c5bfe0..c1590721f 100644 --- a/apps/client/src/services/options.ts +++ b/apps/client/src/services/options.ts @@ -1,4 +1,5 @@ import server from "./server.js"; +import { isShare } from "./utils.js"; type OptionValue = number | string; @@ -7,7 +8,11 @@ class Options { private arr!: Record; constructor() { - this.initializedPromise = server.get>("options").then((data) => this.load(data)); + if (!isShare) { + this.initializedPromise = server.get>("options").then((data) => this.load(data)); + } else { + this.initializedPromise = Promise.resolve(); + } } load(arr: Record) { diff --git a/apps/client/src/services/promoted_attribute_definition_parser.ts b/apps/client/src/services/promoted_attribute_definition_parser.ts index e40c24bbc..f46040e2a 100644 --- a/apps/client/src/services/promoted_attribute_definition_parser.ts +++ b/apps/client/src/services/promoted_attribute_definition_parser.ts @@ -1,7 +1,7 @@ type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url"; type Multiplicity = "single" | "multi"; -interface DefinitionObject { +export interface DefinitionObject { isPromoted?: boolean; labelType?: LabelType; multiplicity?: Multiplicity; diff --git a/apps/client/src/services/resizer.ts b/apps/client/src/services/resizer.ts index 1cce0f993..e0dc40995 100644 --- a/apps/client/src/services/resizer.ts +++ b/apps/client/src/services/resizer.ts @@ -3,7 +3,11 @@ import Split from "split.js" export const DEFAULT_GUTTER_SIZE = 5; +let leftPaneWidth: number; +let reservedPx: number; +let layoutOrientation: string; let leftInstance: ReturnType | null; +let rightPaneWidth: number; let rightInstance: ReturnType | null; function setupLeftPaneResizer(leftPaneVisible: boolean) { @@ -14,27 +18,34 @@ function setupLeftPaneResizer(leftPaneVisible: boolean) { $("#left-pane").toggle(leftPaneVisible); + layoutOrientation = layoutOrientation ?? options.get("layoutOrientation"); + reservedPx = reservedPx ?? (layoutOrientation === "vertical" ? ($("#launcher-pane").outerWidth() || 0) : 0); + // Window resizing causes `window.innerWidth` to change, so `reservedWidth` needs to be recalculated each time. + const reservedWidth = reservedPx / window.innerWidth * 100; if (!leftPaneVisible) { - $("#rest-pane").css("width", "100%"); - + $("#rest-pane").css("width", layoutOrientation === "vertical" ? `${100 - reservedWidth}%` : "100%"); return; } - let leftPaneWidth = options.getInt("leftPaneWidth"); + leftPaneWidth = leftPaneWidth ?? (options.getInt("leftPaneWidth") ?? 0); if (!leftPaneWidth || leftPaneWidth < 5) { leftPaneWidth = 5; } + const restPaneWidth = 100 - leftPaneWidth - reservedWidth; if (leftPaneVisible) { // Delayed initialization ensures that all DOM elements are fully rendered and part of the layout, // preventing Split.js from retrieving incorrect dimensions due to #left-pane not being rendered yet, // which would cause the minSize setting to have no effect. requestAnimationFrame(() => { leftInstance = Split(["#left-pane", "#rest-pane"], { - sizes: [leftPaneWidth, 100 - leftPaneWidth], + sizes: [leftPaneWidth, restPaneWidth], gutterSize: DEFAULT_GUTTER_SIZE, minSize: [150, 300], - onDragEnd: (sizes) => options.save("leftPaneWidth", Math.round(sizes[0])) + onDragEnd: (sizes) => { + leftPaneWidth = Math.round(sizes[0]); + options.save("leftPaneWidth", Math.round(sizes[0])); + } }); }); } @@ -54,7 +65,7 @@ function setupRightPaneResizer() { return; } - let rightPaneWidth = options.getInt("rightPaneWidth"); + rightPaneWidth = rightPaneWidth ?? (options.getInt("rightPaneWidth") ?? 0); if (!rightPaneWidth || rightPaneWidth < 5) { rightPaneWidth = 5; } @@ -63,8 +74,11 @@ function setupRightPaneResizer() { rightInstance = Split(["#center-pane", "#right-pane"], { sizes: [100 - rightPaneWidth, rightPaneWidth], gutterSize: DEFAULT_GUTTER_SIZE, - minSize: [ 300, 180 ], - onDragEnd: (sizes) => options.save("rightPaneWidth", Math.round(sizes[1])) + minSize: [300, 180], + onDragEnd: (sizes) => { + rightPaneWidth = Math.round(sizes[1]); + options.save("rightPaneWidth", Math.round(sizes[1])); + } }); } } diff --git a/apps/client/src/services/script_context.ts b/apps/client/src/services/script_context.ts index 27a8a7d44..7c15db1ae 100644 --- a/apps/client/src/services/script_context.ts +++ b/apps/client/src/services/script_context.ts @@ -1,4 +1,4 @@ -import FrontendScriptApi, { type Entity } from "./frontend_script_api.js"; +import type { Entity } from "./frontend_script_api.js"; import utils from "./utils.js"; import froca from "./froca.js"; @@ -14,6 +14,8 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn throw new Error(`Could not find start note ${startNoteId}.`); } + const FrontendScriptApi = (await import("./frontend_script_api.js")).default; + return { modules: modules, notes: utils.toObject(allNotes, (note) => [note.noteId, note]), diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index e15e3ba88..cb557b19b 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -1,4 +1,4 @@ -import utils from "./utils.js"; +import utils, { isShare } from "./utils.js"; import ValidationError from "./validation_error.js"; type Headers = Record; @@ -28,6 +28,10 @@ export interface StandardResponse { } async function getHeaders(headers?: Headers) { + if (isShare) { + return {}; + } + const appContext = (await import("../components/app_context.js")).default; const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null; @@ -58,8 +62,11 @@ async function getWithSilentNotFound(url: string, componentId?: string) { return await call("GET", url, componentId, { silentNotFound: true }); } -async function get(url: string, componentId?: string) { - return await call("GET", url, componentId); +/** + * @param raw if `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc. + */ +async function get(url: string, componentId?: string, raw?: boolean) { + return await call("GET", url, componentId, { raw }); } async function post(url: string, data?: unknown, componentId?: string) { @@ -102,6 +109,8 @@ let maxKnownEntityChangeId = 0; interface CallOptions { data?: unknown; silentNotFound?: boolean; + // If `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc. + raw?: boolean; } async function call(method: string, url: string, componentId?: string, options: CallOptions = {}) { @@ -132,7 +141,7 @@ async function call(method: string, url: string, componentId?: string, option }); })) as any; } else { - resp = await ajax(url, method, data, headers, !!options.silentNotFound); + resp = await ajax(url, method, data, headers, !!options.silentNotFound, options.raw); } const maxEntityChangeIdStr = resp.headers["trilium-max-entity-change-id"]; @@ -144,7 +153,10 @@ async function call(method: string, url: string, componentId?: string, option return resp.body as T; } -function ajax(url: string, method: string, data: unknown, headers: Headers, silentNotFound: boolean): Promise { +/** + * @param raw if `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc. + */ +function ajax(url: string, method: string, data: unknown, headers: Headers, silentNotFound: boolean, raw?: boolean): Promise { return new Promise((res, rej) => { const options: JQueryAjaxSettings = { url: window.glob.baseApiUrl + url, @@ -186,6 +198,10 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile } }; + if (raw) { + options.dataType = "text"; + } + if (data) { try { options.data = JSON.stringify(data); @@ -260,7 +276,8 @@ async function reportError(method: string, url: string, statusCode: number, resp } else { const title = `${statusCode} ${method} ${url}`; toastService.showErrorTitleAndMessage(title, messageStr); - toastService.throwError(`${title} - ${message}`); + const { throwError } = await import("./ws.js"); + throwError(`${title} - ${message}`); } } diff --git a/apps/client/src/services/syntax_highlight.ts b/apps/client/src/services/syntax_highlight.ts index ae2fbde72..02089e09c 100644 --- a/apps/client/src/services/syntax_highlight.ts +++ b/apps/client/src/services/syntax_highlight.ts @@ -1,29 +1,21 @@ -import library_loader from "./library_loader.js"; +import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs"; import mime_types from "./mime_types.js"; import options from "./options.js"; +import { t } from "./i18n.js"; +import { copyText, copyTextWithToast } from "./clipboard_ext.js"; +import { isShare } from "./utils.js"; +import { MimeType } from "@triliumnext/commons"; -export function getStylesheetUrl(theme: string) { - if (!theme) { - return null; - } - - const defaultPrefix = "default:"; - if (theme.startsWith(defaultPrefix)) { - return `${window.glob.assetPath}/node_modules/@highlightjs/cdn-assets/styles/${theme.substr(defaultPrefix.length)}.min.css`; - } - - return null; -} +let highlightingLoaded = false; /** * Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks. + * Additionally, adds a "Copy to clipboard" button. * * @param $container the container under which to look for code blocks and to apply syntax highlighting to them. */ -export async function applySyntaxHighlight($container: JQuery) { - if (!isSyntaxHighlightEnabled()) { - return; - } +export async function formatCodeBlocks($container: JQuery) { + const syntaxHighlightingEnabled = isSyntaxHighlightEnabled(); const codeBlocks = $container.find("pre code"); for (const codeBlock of codeBlocks) { @@ -32,10 +24,28 @@ export async function applySyntaxHighlight($container: JQuery) { continue; } - applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); + applyCopyToClipboardButton($(codeBlock)); + + if (syntaxHighlightingEnabled) { + applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); + } } } +export function applyCopyToClipboardButton($codeBlock: JQuery) { + const $copyButton = $(" + + + + + +`; + +export default class IncorrectCpuArchDialog extends BasicWidget { + private modal!: Modal; + private $downloadButton!: JQuery; + + doRender() { + this.$widget = $(TPL); + this.modal = Modal.getOrCreateInstance(this.$widget[0]); + this.$downloadButton = this.$widget.find(".download-correct-version-button"); + + this.$downloadButton.on("click", () => { + // Open the releases page where users can download the correct version + if (utils.isElectron()) { + const { shell } = utils.dynamicRequire("electron"); + shell.openExternal("https://github.com/TriliumNext/Notes/releases/latest"); + } else { + window.open("https://github.com/TriliumNext/Notes/releases/latest", "_blank"); + } + }); + + // Auto-focus the download button when shown + this.$widget.on("shown.bs.modal", () => { + this.$downloadButton.trigger("focus"); + }); + } + + showCpuArchWarningEvent() { + this.modal.show(); + } +} diff --git a/apps/client/src/widgets/dialogs/info.ts b/apps/client/src/widgets/dialogs/info.ts index 508599912..34015dc7d 100644 --- a/apps/client/src/widgets/dialogs/info.ts +++ b/apps/client/src/widgets/dialogs/info.ts @@ -1,9 +1,9 @@ import type { EventData } from "../../components/app_context.js"; import { t } from "../../services/i18n.js"; -import utils from "../../services/utils.js"; import BasicWidget from "../basic_widget.js"; import { Modal } from "bootstrap"; import type { ConfirmDialogCallback } from "./confirm.js"; +import { openDialog } from "../../services/dialog.js"; const TPL = /*html*/`