Full Code of sissbruecker/linkding for AI

master 573b6f5411ea cached
405 files
1.7 MB
424.5k tokens
2534 symbols
1 requests
Download .txt
Showing preview only (1,898K chars total). Download the full file or copy to clipboard to get everything.
Repository: sissbruecker/linkding
Branch: master
Commit: 573b6f5411ea
Files: 405
Total size: 1.7 MB

Directory structure:
gitextract_7gdsh6yf/

├── .coveragerc
├── .devcontainer/
│   └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── build-test.yaml
│       ├── build.yaml
│       └── main.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── Makefile
├── README.md
├── SECURITY.md
├── assets/
│   ├── header.afdesign
│   ├── logo-inset.afdesign
│   ├── logo.afdesign
│   └── social-preview.afdesign
├── bookmarks/
│   ├── __init__.py
│   ├── admin.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   ├── routes.py
│   │   └── serializers.py
│   ├── apps.py
│   ├── context_processors.py
│   ├── feeds.py
│   ├── forms.py
│   ├── frontend/
│   │   ├── api.js
│   │   ├── components/
│   │   │   ├── bookmark-page.js
│   │   │   ├── clear-button.js
│   │   │   ├── confirm-dropdown.js
│   │   │   ├── details-modal.js
│   │   │   ├── dev-tool.js
│   │   │   ├── dropdown.js
│   │   │   ├── filter-drawer.js
│   │   │   ├── form.js
│   │   │   ├── modal.js
│   │   │   ├── search-autocomplete.js
│   │   │   ├── tag-autocomplete.js
│   │   │   └── upload-button.js
│   │   ├── index.js
│   │   ├── shortcuts.js
│   │   └── utils/
│   │       ├── element.js
│   │       ├── focus.js
│   │       ├── input.js
│   │       ├── position-controller.js
│   │       ├── search-history.js
│   │       └── tag-cache.js
│   ├── management/
│   │   └── commands/
│   │       ├── backup.py
│   │       ├── create_initial_superuser.py
│   │       ├── enable_wal.py
│   │       ├── ensure_superuser.py
│   │       ├── full_backup.py
│   │       ├── generate_secret_key.py
│   │       ├── import_netscape.py
│   │       └── migrate_tasks.py
│   ├── middlewares.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_auto_20190629_2303.py
│   │   ├── 0003_auto_20200913_0656.py
│   │   ├── 0004_auto_20200926_1028.py
│   │   ├── 0005_auto_20210103_1212.py
│   │   ├── 0006_bookmark_is_archived.py
│   │   ├── 0007_userprofile.py
│   │   ├── 0008_userprofile_bookmark_date_display.py
│   │   ├── 0009_bookmark_web_archive_snapshot_url.py
│   │   ├── 0010_userprofile_bookmark_link_target.py
│   │   ├── 0011_userprofile_web_archive_integration.py
│   │   ├── 0012_toast.py
│   │   ├── 0013_web_archive_optin_toast.py
│   │   ├── 0014_alter_bookmark_unread.py
│   │   ├── 0015_feedtoken.py
│   │   ├── 0016_bookmark_shared.py
│   │   ├── 0017_userprofile_enable_sharing.py
│   │   ├── 0018_bookmark_favicon_file.py
│   │   ├── 0019_userprofile_enable_favicons.py
│   │   ├── 0020_userprofile_tag_search.py
│   │   ├── 0021_userprofile_display_url.py
│   │   ├── 0022_bookmark_notes.py
│   │   ├── 0023_userprofile_permanent_notes.py
│   │   ├── 0024_userprofile_enable_public_sharing.py
│   │   ├── 0025_userprofile_search_preferences.py
│   │   ├── 0026_userprofile_custom_css.py
│   │   ├── 0027_userprofile_bookmark_description_display_and_more.py
│   │   ├── 0028_userprofile_display_archive_bookmark_action_and_more.py
│   │   ├── 0029_bookmark_list_actions_toast.py
│   │   ├── 0030_bookmarkasset.py
│   │   ├── 0031_userprofile_enable_automatic_html_snapshots.py
│   │   ├── 0032_html_snapshots_hint_toast.py
│   │   ├── 0033_userprofile_default_mark_unread.py
│   │   ├── 0034_bookmark_preview_image_file_and_more.py
│   │   ├── 0035_userprofile_tag_grouping.py
│   │   ├── 0036_userprofile_auto_tagging_rules.py
│   │   ├── 0037_globalsettings.py
│   │   ├── 0038_globalsettings_guest_profile_user.py
│   │   ├── 0039_globalsettings_enable_link_prefetch.py
│   │   ├── 0040_userprofile_items_per_page_and_more.py
│   │   ├── 0041_merge_metadata.py
│   │   ├── 0042_userprofile_custom_css_hash.py
│   │   ├── 0043_userprofile_collapse_side_panel.py
│   │   ├── 0044_bookmark_latest_snapshot.py
│   │   ├── 0045_userprofile_hide_bundles_bookmarkbundle.py
│   │   ├── 0046_add_url_normalized_field.py
│   │   ├── 0047_populate_url_normalized_field.py
│   │   ├── 0048_userprofile_default_mark_shared.py
│   │   ├── 0049_userprofile_legacy_search.py
│   │   ├── 0050_new_search_toast.py
│   │   ├── 0051_fix_normalized_url.py
│   │   ├── 0052_apitoken.py
│   │   ├── 0053_migrate_api_tokens.py
│   │   ├── 0054_bookmarkbundle_filter_shared_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── queries.py
│   ├── services/
│   │   ├── __init__.py
│   │   ├── assets.py
│   │   ├── auto_tagging.py
│   │   ├── bookmarks.py
│   │   ├── bundles.py
│   │   ├── exporter.py
│   │   ├── favicon_loader.py
│   │   ├── importer.py
│   │   ├── monolith.py
│   │   ├── parser.py
│   │   ├── preview_image_loader.py
│   │   ├── search_query_parser.py
│   │   ├── singlefile.py
│   │   ├── tags.py
│   │   ├── tasks.py
│   │   ├── wayback.py
│   │   └── website_loader.py
│   ├── settings/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── custom.py
│   │   ├── dev.py
│   │   └── prod.py
│   ├── signals.py
│   ├── static/
│   │   ├── live-reload.js
│   │   ├── robots.txt
│   │   └── vendor/
│   │       └── Readability.js
│   ├── styles/
│   │   ├── auth.css
│   │   ├── bookmark-details.css
│   │   ├── bookmark-form.css
│   │   ├── bookmark-page.css
│   │   ├── bundles.css
│   │   ├── components.css
│   │   ├── crud.css
│   │   ├── layout.css
│   │   ├── markdown.css
│   │   ├── reader-mode.css
│   │   ├── responsive.css
│   │   ├── settings.css
│   │   ├── tags.css
│   │   ├── theme/
│   │   │   ├── LICENSE
│   │   │   ├── _normalize.css
│   │   │   ├── animations.css
│   │   │   ├── asian.css
│   │   │   ├── autocomplete.css
│   │   │   ├── badges.css
│   │   │   ├── base.css
│   │   │   ├── buttons.css
│   │   │   ├── code.css
│   │   │   ├── dropdowns.css
│   │   │   ├── empty.css
│   │   │   ├── forms.css
│   │   │   ├── menus.css
│   │   │   ├── modals.css
│   │   │   ├── pagination.css
│   │   │   ├── tables.css
│   │   │   ├── tabs.css
│   │   │   ├── toasts.css
│   │   │   ├── typography.css
│   │   │   ├── utilities.css
│   │   │   └── variables.css
│   │   ├── theme-dark.css
│   │   └── theme-light.css
│   ├── tasks.py
│   ├── templates/
│   │   ├── admin/
│   │   │   └── background_tasks.html
│   │   ├── bookmarks/
│   │   │   ├── bookmark_list.html
│   │   │   ├── bookmark_page.html
│   │   │   ├── bulk_edit_bar.html
│   │   │   ├── bundle_section.html
│   │   │   ├── close.html
│   │   │   ├── details/
│   │   │   │   ├── asset_icon.html
│   │   │   │   ├── assets.html
│   │   │   │   ├── form.html
│   │   │   │   └── modal.html
│   │   │   ├── edit.html
│   │   │   ├── empty_bookmarks.html
│   │   │   ├── form.html
│   │   │   ├── new.html
│   │   │   ├── read.html
│   │   │   ├── search.html
│   │   │   ├── tag_cloud.html
│   │   │   ├── tag_section.html
│   │   │   └── user_section.html
│   │   ├── bundles/
│   │   │   ├── edit.html
│   │   │   ├── form.html
│   │   │   ├── index.html
│   │   │   ├── new.html
│   │   │   └── preview.html
│   │   ├── opensearch.xml
│   │   ├── registration/
│   │   │   ├── login.html
│   │   │   ├── password_change_done.html
│   │   │   └── password_change_form.html
│   │   ├── settings/
│   │   │   ├── bookmarklet.js
│   │   │   ├── bookmarklet_clientside.js
│   │   │   ├── create_api_token_modal.html
│   │   │   ├── general.html
│   │   │   └── integrations.html
│   │   ├── shared/
│   │   │   ├── dev_tool.html
│   │   │   ├── error_list.html
│   │   │   ├── head.html
│   │   │   ├── layout.html
│   │   │   ├── messages.html
│   │   │   ├── modal_header.html
│   │   │   ├── nav_menu.html
│   │   │   ├── pagination.html
│   │   │   └── top_frame.html
│   │   └── tags/
│   │       ├── edit.html
│   │       ├── form.html
│   │       ├── index.html
│   │       ├── merge.html
│   │       └── new.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   ├── bookmarks.py
│   │   ├── pagination.py
│   │   └── shared.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── helpers.py
│   │   ├── resources/
│   │   │   ├── simple_valid_import_file.html
│   │   │   └── simple_valid_import_file_with_one_invalid_bookmark.html
│   │   ├── test_app_options.py
│   │   ├── test_assets_service.py
│   │   ├── test_auth_api.py
│   │   ├── test_auth_proxy_support.py
│   │   ├── test_auto_tagging.py
│   │   ├── test_bookmark_action_view.py
│   │   ├── test_bookmark_archived_view.py
│   │   ├── test_bookmark_archived_view_performance.py
│   │   ├── test_bookmark_asset_view.py
│   │   ├── test_bookmark_assets.py
│   │   ├── test_bookmark_assets_api.py
│   │   ├── test_bookmark_details_modal.py
│   │   ├── test_bookmark_edit_view.py
│   │   ├── test_bookmark_index_view.py
│   │   ├── test_bookmark_index_view_performance.py
│   │   ├── test_bookmark_new_view.py
│   │   ├── test_bookmark_previews.py
│   │   ├── test_bookmark_search_form.py
│   │   ├── test_bookmark_search_model.py
│   │   ├── test_bookmark_search_tag.py
│   │   ├── test_bookmark_shared_view.py
│   │   ├── test_bookmark_shared_view_performance.py
│   │   ├── test_bookmark_validation.py
│   │   ├── test_bookmarks_api.py
│   │   ├── test_bookmarks_api_performance.py
│   │   ├── test_bookmarks_api_permissions.py
│   │   ├── test_bookmarks_list_template.py
│   │   ├── test_bookmarks_model.py
│   │   ├── test_bookmarks_service.py
│   │   ├── test_bookmarks_tasks.py
│   │   ├── test_bundles_api.py
│   │   ├── test_bundles_edit_view.py
│   │   ├── test_bundles_index_view.py
│   │   ├── test_bundles_new_view.py
│   │   ├── test_bundles_preview_view.py
│   │   ├── test_context_path.py
│   │   ├── test_create_initial_superuser_command.py
│   │   ├── test_custom_css_view.py
│   │   ├── test_exporter.py
│   │   ├── test_exporter_performance.py
│   │   ├── test_favicon_loader.py
│   │   ├── test_feeds.py
│   │   ├── test_feeds_performance.py
│   │   ├── test_health_view.py
│   │   ├── test_importer.py
│   │   ├── test_layout.py
│   │   ├── test_linkding_middleware.py
│   │   ├── test_login_view.py
│   │   ├── test_metadata_view.py
│   │   ├── test_monolith_service.py
│   │   ├── test_oidc_support.py
│   │   ├── test_opensearch_view.py
│   │   ├── test_pagination_tag.py
│   │   ├── test_parser.py
│   │   ├── test_password_change_view.py
│   │   ├── test_preview_image_loader.py
│   │   ├── test_queries.py
│   │   ├── test_root_view.py
│   │   ├── test_search_query_parser.py
│   │   ├── test_settings_export_view.py
│   │   ├── test_settings_general_view.py
│   │   ├── test_settings_import_view.py
│   │   ├── test_settings_integrations_view.py
│   │   ├── test_singlefile_service.py
│   │   ├── test_tag_cloud_template.py
│   │   ├── test_tags_edit_view.py
│   │   ├── test_tags_index_view.py
│   │   ├── test_tags_merge_view.py
│   │   ├── test_tags_model.py
│   │   ├── test_tags_new_view.py
│   │   ├── test_tags_service.py
│   │   ├── test_toasts_view.py
│   │   ├── test_user_profile_model.py
│   │   ├── test_user_select_tag.py
│   │   ├── test_utils.py
│   │   └── test_website_loader.py
│   ├── tests_e2e/
│   │   ├── __init__.py
│   │   ├── e2e_test_a11y_navigation_focus.py
│   │   ├── e2e_test_bookmark_details_modal.py
│   │   ├── e2e_test_bookmark_item.py
│   │   ├── e2e_test_bookmark_page_bulk_edit.py
│   │   ├── e2e_test_bookmark_page_partial_updates.py
│   │   ├── e2e_test_bundle_preview.py
│   │   ├── e2e_test_collapse_side_panel.py
│   │   ├── e2e_test_dropdown.py
│   │   ├── e2e_test_edit_bookmark_form.py
│   │   ├── e2e_test_filter_drawer.py
│   │   ├── e2e_test_global_shortcuts.py
│   │   ├── e2e_test_new_bookmark_form.py
│   │   ├── e2e_test_settings_general.py
│   │   ├── e2e_test_settings_integrations.py
│   │   ├── e2e_test_tag_management.py
│   │   └── helpers.py
│   ├── type_defs.py
│   ├── urls.py
│   ├── utils.py
│   ├── validators.py
│   ├── views/
│   │   ├── __init__.py
│   │   ├── access.py
│   │   ├── assets.py
│   │   ├── auth.py
│   │   ├── bookmarks.py
│   │   ├── bundles.py
│   │   ├── contexts.py
│   │   ├── custom_css.py
│   │   ├── health.py
│   │   ├── manifest.py
│   │   ├── opensearch.py
│   │   ├── reload.py
│   │   ├── root.py
│   │   ├── settings.py
│   │   ├── tags.py
│   │   ├── toasts.py
│   │   └── turbo.py
│   ├── widgets.py
│   └── wsgi.py
├── bootstrap.sh
├── docker/
│   ├── alpine.Dockerfile
│   └── default.Dockerfile
├── docker-compose.yml
├── docs/
│   ├── .gitignore
│   ├── README.md
│   ├── astro.config.mjs
│   ├── package.json
│   ├── src/
│   │   ├── assets/
│   │   │   ├── Add To Linkding.shortcut
│   │   │   └── linkding_shortcut.json
│   │   ├── components/
│   │   │   ├── Card.astro
│   │   │   └── icons.ts
│   │   ├── content/
│   │   │   ├── config.ts
│   │   │   └── docs/
│   │   │       ├── acknowledgements.md
│   │   │       ├── admin.md
│   │   │       ├── api.md
│   │   │       ├── archiving.md
│   │   │       ├── auto-tagging.md
│   │   │       ├── backups.md
│   │   │       ├── browser-extension.md
│   │   │       ├── community.md
│   │   │       ├── how-to.md
│   │   │       ├── index.mdx
│   │   │       ├── installation.md
│   │   │       ├── managed-hosting.md
│   │   │       ├── options.md
│   │   │       ├── search.md
│   │   │       ├── shortcuts.md
│   │   │       └── troubleshooting.md
│   │   ├── env.d.ts
│   │   └── styles/
│   │       └── custom.css
│   └── tsconfig.json
├── install-linkding.sh
├── manage.py
├── package.json
├── postcss.config.js
├── pyproject.toml
├── pytest.ini
├── rollup.config.mjs
├── scripts/
│   ├── build-docker.sh
│   ├── coverage.sh
│   ├── generate-changelog.py
│   ├── release.sh
│   ├── run-docker.sh
│   ├── run-postgres.sh
│   ├── setup-ublock.sh
│   ├── test-environments/
│   │   ├── authelia-oidc/
│   │   │   ├── authelia/
│   │   │   │   ├── configuration.yml
│   │   │   │   └── users_database.yml
│   │   │   ├── compose.yml
│   │   │   ├── setup.sh
│   │   │   └── traefik/
│   │   │       └── certificates.yml
│   │   └── postgres/
│   │       └── compose.yml
│   └── test-postgres.sh
├── supervisord-all.conf
├── supervisord-tasks.conf
├── uwsgi.ini
└── version.txt

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
source = bookmarks
omit = bookmarks/tests/*


================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
	"name": "Python 3",
	"image": "mcr.microsoft.com/devcontainers/python:3.13",
	"features": {
		"ghcr.io/devcontainers/features/node:1": {}
	},

	// Features to add to the dev container. More info: https://containers.dev/features.
	// "features": {},

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	"forwardPorts": [8000],

	// Use 'postCreateCommand' to run commands after the container is created.
	"postCreateCommand": "pip install uv && uv sync --group dev && npm install && mkdir -p data && uv run manage.py migrate",

	// Configure tool-specific properties.
	"customizations": {
		"vscode": {
			"extensions": [
				"ms-python.python"
			]
		}
	},

	"remoteUser": "vscode"
}


================================================
FILE: .dockerignore
================================================
# Ignore everything
*

# Include files required for build or at runtime
!/bookmarks

!/bootstrap.sh
!/LICENSE.txt
!/manage.py
!/package.json
!/package-lock.json
!/postcss.config.js
!/pyproject.toml
!/rollup.config.mjs
!/supervisord-tasks.conf
!/supervisord-all.conf
!/uv.lock
!/uwsgi.ini
!/version.txt

 # Remove dev settings
/bookmarks/settings/dev.py


================================================
FILE: .gitattributes
================================================
* text=auto
*.sh text eol=lf

================================================
FILE: .github/workflows/build-test.yaml
================================================
name: build-test

on: workflow_dispatch

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ github.token }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build latest
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/default.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: |
            ghcr.io/sissbruecker/linkding:test
          target: linkding
          push: true
          cache-from: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache
          cache-to: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache,mode=max

      - name: Build latest-alpine
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/alpine.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: |
            ghcr.io/sissbruecker/linkding:test-alpine
          target: linkding
          push: true
          cache-from: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache-alpine
          cache-to: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache-alpine,mode=max

      - name: Build latest-plus
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/default.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: |
            ghcr.io/sissbruecker/linkding:test-plus
          target: linkding-plus
          push: true
          cache-from: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache
          cache-to: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache,mode=max

      - name: Build latest-plus-alpine
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/alpine.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: |
            ghcr.io/sissbruecker/linkding:test-plus-alpine
          target: linkding-plus
          push: true
          cache-from: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache-alpine
          cache-to: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache-alpine,mode=max


================================================
FILE: .github/workflows/build.yaml
================================================
name: build

on: workflow_dispatch

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Read version from file
        id: get_version
        run: echo "VERSION=$(cat version.txt)" >> $GITHUB_ENV

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ github.token }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build latest
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/default.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: |
            sissbruecker/linkding:latest
            sissbruecker/linkding:${{ env.VERSION }}
            ghcr.io/sissbruecker/linkding:latest
            ghcr.io/sissbruecker/linkding:${{ env.VERSION }}
          target: linkding
          push: true
          cache-from: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache
          cache-to: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache,mode=max

      - name: Build latest-alpine
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/alpine.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: |
            sissbruecker/linkding:latest-alpine
            sissbruecker/linkding:${{ env.VERSION }}-alpine
            ghcr.io/sissbruecker/linkding:latest-alpine
            ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-alpine
          target: linkding
          push: true
          cache-from: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache-alpine
          cache-to: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache-alpine,mode=max

      - name: Build latest-plus
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/default.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: |
            sissbruecker/linkding:latest-plus
            sissbruecker/linkding:${{ env.VERSION }}-plus
            ghcr.io/sissbruecker/linkding:latest-plus
            ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-plus
          target: linkding-plus
          push: true
          cache-from: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache
          cache-to: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache,mode=max

      - name: Build latest-plus-alpine
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/alpine.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: |
            sissbruecker/linkding:latest-plus-alpine
            sissbruecker/linkding:${{ env.VERSION }}-plus-alpine
            ghcr.io/sissbruecker/linkding:latest-plus-alpine
            ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-plus-alpine
          target: linkding-plus
          push: true
          cache-from: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache-alpine
          cache-to: type=registry,ref=ghcr.io/sissbruecker/linkding:buildcache-alpine,mode=max

================================================
FILE: .github/workflows/main.yaml
================================================
name: linkding CI

on:
  pull_request:
  push:
    branches:
      - master

jobs:
  unit_tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - name: Install uv
        uses: astral-sh/setup-uv@v6
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - name: Install Node dependencies
        run: npm ci
      - name: Setup Python environment
        run: |
          uv sync
          mkdir data
      - name: Run tests
        run: uv run pytest -n auto
  e2e_tests:
    name: E2E Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - name: Install uv
        uses: astral-sh/setup-uv@v6
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - name: Install Node dependencies
        run: npm ci
      - name: Setup Python environment
        run: |
          uv sync
          uv run playwright install chromium
          mkdir data
      - name: Run build
        run: |
          npm run build
          uv run manage.py collectstatic
      - name: Run tests
        run: uv run pytest bookmarks/tests_e2e -n auto -o "python_files=e2e_test_*.py"
      - name: Upload screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: e2e-screenshots
          path: test-results/screenshots


================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# File-based project format
*.iws

# IntelliJ
.idea
*.iml
out/

### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
test-results/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next

### Custom
# Rollup compilation output
/bookmarks/static/bundle.js*
# CSS compilation output
/bookmarks/static/theme-*.css*
# Collected static files for deployment
/static
# Build output, etc.
/tmp
# Database file
/data
# ublock + chromium
/uBOLite.chromium.mv3
/chromium-profile
# direnv
/.direnv

# Test setups
/scripts/unsecure-test-setups/authelia-oidc/authelia/db.sqlite3
/scripts/unsecure-test-setups/authelia-oidc/traefik/certs


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## v1.45.0 (06/01/2026)

### What's Changed
* API token management by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1248
* Add option to disable login form by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1269
* Turn scheme-less URLs into HTTPS instead of HTTP links by @Maaxxs in https://github.com/sissbruecker/linkding/pull/1225
* Disable bulk execute button when no bookmarks selected by @emanuelebeffa in https://github.com/sissbruecker/linkding/pull/1241
* Add option to run supervisor as main process by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1270
* Allow setting date_added and date_modified for new bookmarks through REST API by @jmason in https://github.com/sissbruecker/linkding/pull/1063
* Download PDF instead of creating HTML snapshot if URL points at PDF by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1271
* Allow sandboxed scripts when viewing assets by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1252
* Allow viewing video assets by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1259
* Remove absolute URIs from settings page by @packrat386 in https://github.com/sissbruecker/linkding/pull/1261
* Move tag management forms into dialogs by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1253
* Move bulk edit checkboxes into bookmark list container by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1257
* Remove registration switch by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1268
* Add linkdinger to community projects by @lmmendes in https://github.com/sissbruecker/linkding/pull/1266

### New Contributors
* @packrat386 made their first contribution in https://github.com/sissbruecker/linkding/pull/1261
* @lmmendes made their first contribution in https://github.com/sissbruecker/linkding/pull/1266
* @Maaxxs made their first contribution in https://github.com/sissbruecker/linkding/pull/1225
* @emanuelebeffa made their first contribution in https://github.com/sissbruecker/linkding/pull/1241
* @jmason made their first contribution in https://github.com/sissbruecker/linkding/pull/1063

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.44.2...v1.45.0

---

## v1.44.2 (13/12/2025)

### What's Changed

> [!WARNING] 
> *This resolves a [security vulnerability](https://github.com/sissbruecker/linkding/security/advisories/GHSA-3pf9-5cjv-2w7q) in linkding. Everyone is encouraged to upgrade to the latest version as soon as possible.*

* Use sandbox CSP for viewing assets by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1245
* Fix devcontainer by @m3eno in https://github.com/sissbruecker/linkding/pull/1208
* Fix tag cloud highlighting first char when tags are not grouped by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1209
* Bump supervisor to 4.3.0 to fix warning by @simonhammes in https://github.com/sissbruecker/linkding/pull/1216
* Added Javascript client and library for Linkding REST API by @vbsampath in https://github.com/sissbruecker/linkding/pull/1195
* Add Komrade project to community resources by @dev-inside in https://github.com/sissbruecker/linkding/pull/1236

### New Contributors
* @m3eno made their first contribution in https://github.com/sissbruecker/linkding/pull/1208
* @vbsampath made their first contribution in https://github.com/sissbruecker/linkding/pull/1195
* @dev-inside made their first contribution in https://github.com/sissbruecker/linkding/pull/1236
* @simonhammes made their first contribution in https://github.com/sissbruecker/linkding/pull/1216

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.44.1...v1.44.2

---

## v1.44.1 (11/10/2025)

### What's Changed
* Fix normalized URL not being generated in bookmark import by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1202
* Fix missing tags causing errors in import with Postgres by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1203
* Check for dupes by exact URL if normalized URL is missing by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1204
* Attempt to fix botched normalized URL migration from 1.43.0 by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1205


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.44.0...v1.44.1

---

## v1.44.0 (05/10/2025)

### What's Changed
* Add new search engine that supports logical expressions (and, or, not) by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1198
* Fix pagination links to use relative URLs by @dunlor in https://github.com/sissbruecker/linkding/pull/1186
* Fix queued tasks link when context path is used by @dunlor in https://github.com/sissbruecker/linkding/pull/1187
* Fix bundle preview pagination resetting to first page by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1194

### New Contributors
* @dunlor made their first contribution in https://github.com/sissbruecker/linkding/pull/1186

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.43.0...v1.44.0

---

## v1.43.0 (28/09/2025)

### What's Changed
* Add basic tag management by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1175
* Normalize URLs when checking for duplicates by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1169
* Add option to mark bookmarks as shared by default by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1170
* Use modal dialog for confirming actions by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1168
* Fix error when filtering bookmark assets in the admin UI by @proog in https://github.com/sissbruecker/linkding/pull/1162
* Document API bundle filter by @proog in https://github.com/sissbruecker/linkding/pull/1161
* Add alfred-linkding-bookmarks to community.md by @FireFingers21 in https://github.com/sissbruecker/linkding/pull/1160
* Switch to uv by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1172
* Replace Svelte components with Lit elements by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1174
* Bump versions by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1173
* Bump astro from 5.12.8 to 5.13.2 in /docs by @dependabot[bot] in https://github.com/sissbruecker/linkding/pull/1166
* Bump vite from 6.3.5 to 6.3.6 in /docs by @dependabot[bot] in https://github.com/sissbruecker/linkding/pull/1184

### New Contributors
* @FireFingers21 made their first contribution in https://github.com/sissbruecker/linkding/pull/1160

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.42.0...v1.43.0

---

## v1.42.0 (16/08/2025)

### What's Changed
* Bulk create HTML snapshots by @Tql-ws1 in https://github.com/sissbruecker/linkding/pull/1132
* Create bundle from current search query by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1154
* Add alternative bookmarklet that uses browser metadata by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1159
* Add date and time to HTML export filename by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1101
* Automatically compress uploads with gzip by @hkclark in https://github.com/sissbruecker/linkding/pull/1087
* Show bookmark bundles in admin by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1110
* Allow filtering feeds by bundle by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1152
* Submit bookmark form with Ctrl/Cmd + Enter by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1158
* Improve bookmark form accessibility by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1116
* Fix custom CSS not being used in reader mode by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1102
* Use filename when downloading asset through UI by @proog in https://github.com/sissbruecker/linkding/pull/1146
* Update order when deleting bundle by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1114
* Wrap long titles in bookmark details modal by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1150
* Ignore tags with just whitespace by @pvl in https://github.com/sissbruecker/linkding/pull/1125
* Ignore tags that exceed length limit during import by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1153
* Add CloudBreak on Managed Hosting by @benjaminoakes in https://github.com/sissbruecker/linkding/pull/1079
* Add Pocket migration to to community page by @hkclark in https://github.com/sissbruecker/linkding/pull/1112
* Add linkding-media-archiver to community.md by @proog in https://github.com/sissbruecker/linkding/pull/1144
* Bump astro from 5.7.13 to 5.12.8 in /docs by @dependabot[bot] in https://github.com/sissbruecker/linkding/pull/1147

### New Contributors
* @hkclark made their first contribution in https://github.com/sissbruecker/linkding/pull/1087
* @benjaminoakes made their first contribution in https://github.com/sissbruecker/linkding/pull/1079
* @proog made their first contribution in https://github.com/sissbruecker/linkding/pull/1146
* @pvl made their first contribution in https://github.com/sissbruecker/linkding/pull/1125
* @Tql-ws1 made their first contribution in https://github.com/sissbruecker/linkding/pull/1132

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.41.0...v1.42.0

---

## v1.41.0 (19/06/2025)

### What's Changed
* Add bundles for organizing bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1097
* Add REST API for bookmark bundles by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1100
* Add date filters for REST API by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1080
* Fix side panel not being hidden on smaller viewports by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1089
* Fix assets not using correct icon by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1098
* Add LinkBuddy to community section by @peterto in https://github.com/sissbruecker/linkding/pull/1088
* Bump tar-fs in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/1084
* Bump django from 5.1.9 to 5.1.10 by @dependabot in https://github.com/sissbruecker/linkding/pull/1086
* Bump requests from 2.32.3 to 2.32.4 by @dependabot in https://github.com/sissbruecker/linkding/pull/1090
* Bump urllib3 from 2.2.3 to 2.5.0 by @dependabot in https://github.com/sissbruecker/linkding/pull/1096

### New Contributors
* @peterto made their first contribution in https://github.com/sissbruecker/linkding/pull/1088

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.40.0...v1.41.0

---

## v1.40.0 (17/05/2025)

### What's Changed
* Add bulk and single bookmark metadata refresh by @Teknicallity in https://github.com/sissbruecker/linkding/pull/999
* Prefer local snapshot over web archive link in bookmark list links by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1021
* Push Docker images to GHCR in addition to Docker Hub by @caycehouse in https://github.com/sissbruecker/linkding/pull/1024
* Allow auto tagging rules to match URL fragments by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1045
* Linkify plain URLs in notes by @sonicdoe in https://github.com/sissbruecker/linkding/pull/1051
* Add opensearch declaration by @jzorn in https://github.com/sissbruecker/linkding/pull/1058
* Allow pre-filling tags in new bookmark form by @dasrecht in https://github.com/sissbruecker/linkding/pull/1060
* Handle lowercase "true" in environment variables by @jose-elias-alvarez in https://github.com/sissbruecker/linkding/pull/1020
* Accessibility improvements in page structure by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1014
* Improve announcements after navigation by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1015
* Fix OIDC login link by @cite in https://github.com/sissbruecker/linkding/pull/1019
* Fix bookmark asset download endpoint by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1033
* Add docs for auto tagging by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1009
* Fix typo in index.mdx tagline by @cenviity in https://github.com/sissbruecker/linkding/pull/1052
* Add how-to for using linkding PWA in native Android share sheet by @kzshantonu in https://github.com/sissbruecker/linkding/pull/1055
* Adding linktiles to community projects by @haondt in https://github.com/sissbruecker/linkding/pull/1025
* Bump django from 5.1.5 to 5.1.7 by @dependabot in https://github.com/sissbruecker/linkding/pull/1007
* Bump django from 5.1.7 to 5.1.8 by @dependabot in https://github.com/sissbruecker/linkding/pull/1030
* Bump tar-fs in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/1028
* Bump prismjs from 1.29.0 to 1.30.0 in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/1034
* Bump @babel/helpers from 7.26.7 to 7.27.0 in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/1035
* Bump vite from 5.4.14 to 5.4.17 in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/1036
* Bump esbuild, @astrojs/starlight and astro in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/1037
* Bump django from 5.1.8 to 5.1.9 by @dependabot in https://github.com/sissbruecker/linkding/pull/1059

### New Contributors
* @cite made their first contribution in https://github.com/sissbruecker/linkding/pull/1019
* @jose-elias-alvarez made their first contribution in https://github.com/sissbruecker/linkding/pull/1020
* @Teknicallity made their first contribution in https://github.com/sissbruecker/linkding/pull/999
* @haondt made their first contribution in https://github.com/sissbruecker/linkding/pull/1025
* @caycehouse made their first contribution in https://github.com/sissbruecker/linkding/pull/1024
* @cenviity made their first contribution in https://github.com/sissbruecker/linkding/pull/1052
* @sonicdoe made their first contribution in https://github.com/sissbruecker/linkding/pull/1051
* @jzorn made their first contribution in https://github.com/sissbruecker/linkding/pull/1058
* @dasrecht made their first contribution in https://github.com/sissbruecker/linkding/pull/1060

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.39.1...v1.40.0

---

## v1.39.1 (06/03/2025)

> [!WARNING]
> Due to changes in the release process the `1.39.0` Docker image accidentally runs the application in debug mode. Please upgrade to `1.39.1` instead.

---

## v1.39.0 (06/03/2025)

### What's Changed
* Add REST endpoint for uploading snapshots from the Singlefile extension by @sissbruecker in https://github.com/sissbruecker/linkding/pull/996
* Add bookmark assets API by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1003
* Allow providing REST API authentication token with Bearer keyword by @sissbruecker in https://github.com/sissbruecker/linkding/pull/995
* Add Telegram bot to community section by @marb08 in https://github.com/sissbruecker/linkding/pull/1001
* Adding linklater to community projects by @nsartor in https://github.com/sissbruecker/linkding/pull/1002

### New Contributors
* @marb08 made their first contribution in https://github.com/sissbruecker/linkding/pull/1001
* @nsartor made their first contribution in https://github.com/sissbruecker/linkding/pull/1002

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.38.1...v1.39.0

---

## v1.38.1 (22/02/2025)

### What's Changed
* Remove preview image when bookmark is deleted by @sissbruecker in https://github.com/sissbruecker/linkding/pull/989
* Try limit uwsgi memory usage by configuring file descriptor limit by @sissbruecker in https://github.com/sissbruecker/linkding/pull/990
* Add note about OIDC and LD_SUPERUSER_NAME combination by @tebriel in https://github.com/sissbruecker/linkding/pull/992
* Return web archive fallback URL from REST API by @sissbruecker in https://github.com/sissbruecker/linkding/pull/993
* Fix auth proxy logout by @sissbruecker in https://github.com/sissbruecker/linkding/pull/994

### New Contributors
* @tebriel made their first contribution in https://github.com/sissbruecker/linkding/pull/992

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.38.0...v1.38.1

---

## v1.38.0 (09/02/2025)

### What's Changed
* Fix nav menu closing on mousedown in Safari by @sissbruecker in https://github.com/sissbruecker/linkding/pull/965
* Allow customizing username when creating user through OIDC by @kyuuk in https://github.com/sissbruecker/linkding/pull/971
* Improve accessibility of modal dialogs by @sissbruecker in https://github.com/sissbruecker/linkding/pull/974
* Add option to collapse side panel by @sissbruecker in https://github.com/sissbruecker/linkding/pull/975
* Convert tag modal into drawer by @sissbruecker in https://github.com/sissbruecker/linkding/pull/977
* Add RSS link to shared bookmarks page by @sissbruecker in https://github.com/sissbruecker/linkding/pull/984
* Add Additional iOS Shortcut to community section by @joshdick in https://github.com/sissbruecker/linkding/pull/968

### New Contributors
* @kyuuk made their first contribution in https://github.com/sissbruecker/linkding/pull/971

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.37.0...v1.38.0

---

## v1.37.0 (26/01/2025)

### What's Changed
* Add option to disable request logs by @dmarcoux in https://github.com/sissbruecker/linkding/pull/887
* Add default robots.txt to block crawlers by @sissbruecker in https://github.com/sissbruecker/linkding/pull/959
* Fix menu dropdown focus traps by @sissbruecker in https://github.com/sissbruecker/linkding/pull/944
* Provide accessible name to radio groups by @sissbruecker in https://github.com/sissbruecker/linkding/pull/945
* Add serchding to community projects, sort the list by alphabetical order by @ldwgchen in https://github.com/sissbruecker/linkding/pull/880
* Add cosmicding To Community Resources by @vkhitrin in https://github.com/sissbruecker/linkding/pull/892
* Add 3 new community projects by @sebw in https://github.com/sissbruecker/linkding/pull/949
* Add a rust client library to community.md by @zbrox in https://github.com/sissbruecker/linkding/pull/914
* Update community.md by @justusthane in https://github.com/sissbruecker/linkding/pull/897
* Bump astro from 4.15.8 to 4.16.3 in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/884
* Bump vite from 5.4.9 to 5.4.14 in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/953
* Bump django from 5.1.1 to 5.1.5 by @dependabot in https://github.com/sissbruecker/linkding/pull/947
* Bump nanoid from 3.3.7 to 3.3.8 by @dependabot in https://github.com/sissbruecker/linkding/pull/928
* Bump astro from 4.16.3 to 4.16.18 in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/929
* Bump nanoid from 3.3.7 to 3.3.8 in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/962

### New Contributors
* @ldwgchen made their first contribution in https://github.com/sissbruecker/linkding/pull/880
* @dmarcoux made their first contribution in https://github.com/sissbruecker/linkding/pull/887
* @vkhitrin made their first contribution in https://github.com/sissbruecker/linkding/pull/892
* @sebw made their first contribution in https://github.com/sissbruecker/linkding/pull/949
* @justusthane made their first contribution in https://github.com/sissbruecker/linkding/pull/897

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.36.0...v1.37.0

---

## v1.36.0 (02/10/2024)

### What's Changed
* Replace uBlock Origin with uBlock Origin Lite by @sissbruecker in https://github.com/sissbruecker/linkding/pull/866
* Add LAST_MODIFIED attribute when exporting by @ixzhao in https://github.com/sissbruecker/linkding/pull/860
* Return client error status code for invalid form submissions by @sissbruecker in https://github.com/sissbruecker/linkding/pull/849
* Fix header.svg text by @vladh in https://github.com/sissbruecker/linkding/pull/850
* Do not clear fields in POST requests (API behavior change) by @sissbruecker in https://github.com/sissbruecker/linkding/pull/852
* Prevent duplicates when editing by @sissbruecker in https://github.com/sissbruecker/linkding/pull/853
* Fix jumping details modal on back navigation by @sissbruecker in https://github.com/sissbruecker/linkding/pull/854
* Fix select dropdown menu background in dark theme by @sissbruecker in https://github.com/sissbruecker/linkding/pull/858
* Do not escape valid characters in custom CSS by @sissbruecker in https://github.com/sissbruecker/linkding/pull/863
* Simplify Docker build by @sissbruecker in https://github.com/sissbruecker/linkding/pull/865
* Improve error handling for auto tagging by @sissbruecker in https://github.com/sissbruecker/linkding/pull/855
* Bump rollup from 4.13.0 to 4.22.4 by @dependabot in https://github.com/sissbruecker/linkding/pull/851
* Bump rollup from 4.21.3 to 4.22.4 in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/856

### New Contributors
* @vladh made their first contribution in https://github.com/sissbruecker/linkding/pull/850
* @ixzhao made their first contribution in https://github.com/sissbruecker/linkding/pull/860

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.35.0...v1.36.0

---

## v1.35.0 (23/09/2024)

### What's Changed
* Add configuration options for pagination by @sissbruecker in https://github.com/sissbruecker/linkding/pull/835
* Show placeholder if there is no preview image by @sissbruecker in https://github.com/sissbruecker/linkding/pull/842
* Allow bookmarks to have empty title and description by @sissbruecker in https://github.com/sissbruecker/linkding/pull/843
* Add clear buttons in bookmark form by @sissbruecker in https://github.com/sissbruecker/linkding/pull/846
* Add basic fail2ban support by @sissbruecker in https://github.com/sissbruecker/linkding/pull/847
* Add documentation website by @sissbruecker in https://github.com/sissbruecker/linkding/pull/833
* Add go-linkding to community projects by @piero-vic in https://github.com/sissbruecker/linkding/pull/836
* Fix a broken link to options documentation by @zbrox in https://github.com/sissbruecker/linkding/pull/844
* Use HTTPS repository link for devcontainer by @voltagex in https://github.com/sissbruecker/linkding/pull/837
* Bump requests version to 3.23.3 by @voltagex in https://github.com/sissbruecker/linkding/pull/839
* Bump path-to-regexp and astro in /docs by @dependabot in https://github.com/sissbruecker/linkding/pull/840
* Bump dependencies by @sissbruecker in https://github.com/sissbruecker/linkding/pull/841

### New Contributors
* @piero-vic made their first contribution in https://github.com/sissbruecker/linkding/pull/836
* @voltagex made their first contribution in https://github.com/sissbruecker/linkding/pull/839
* @zbrox made their first contribution in https://github.com/sissbruecker/linkding/pull/844

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.34.0...v1.35.0

---

## v1.34.0 (16/09/2024)

### What's Changed
* Fix several issues around browser back navigation by @sissbruecker in https://github.com/sissbruecker/linkding/pull/825
* Speed up response times for certain actions by @sissbruecker in https://github.com/sissbruecker/linkding/pull/829
* Implement IPv6 capability by @itz-Jana in https://github.com/sissbruecker/linkding/pull/826

### New Contributors
* @itz-Jana made their first contribution in https://github.com/sissbruecker/linkding/pull/826

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.33.0...v1.34.0

---

## v1.33.0 (14/09/2024)

### What's Changed
* Theme improvements by @sissbruecker in https://github.com/sissbruecker/linkding/pull/822
* Speed up navigation by @sissbruecker in https://github.com/sissbruecker/linkding/pull/824
* Rename "SingeFileError" to "SingleFileError" by @curiousleo in https://github.com/sissbruecker/linkding/pull/823
* Bump svelte from 4.2.12 to 4.2.19 by @dependabot in https://github.com/sissbruecker/linkding/pull/806

### New Contributors
* @curiousleo made their first contribution in https://github.com/sissbruecker/linkding/pull/823

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.32.0...v1.33.0

---

## v1.32.0 (10/09/2024)

### What's Changed
* Allow configuring landing page for unauthenticated users by @sissbruecker in https://github.com/sissbruecker/linkding/pull/808
* Allow configuring guest user profile by @sissbruecker in https://github.com/sissbruecker/linkding/pull/809
* Return bookmark tags in RSS feeds by @sissbruecker in https://github.com/sissbruecker/linkding/pull/810
* Additional filter parameters for RSS feeds by @sissbruecker in https://github.com/sissbruecker/linkding/pull/811
* Allow pre-filling notes in new bookmark form by @sissbruecker in https://github.com/sissbruecker/linkding/pull/812
* Fix inconsistent tag order in bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/819
* Fix auto-tagging when URL includes port by @sissbruecker in https://github.com/sissbruecker/linkding/pull/820


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.31.1...v1.32.0

---

## v1.31.1 (30/08/2024)

### What's Changed
* Include favicons and thumbnails in REST API by @sissbruecker in https://github.com/sissbruecker/linkding/pull/763
* Add Pinkt to the Community section by @fibelatti in https://github.com/sissbruecker/linkding/pull/772
* removed version line from docker compose yaml by @volumedata21 in https://github.com/sissbruecker/linkding/pull/800
* Add resource linkding logo by @QYG2297248353 in https://github.com/sissbruecker/linkding/pull/788
* Allow use of standard docker `TZ` env var by @watsonbox in https://github.com/sissbruecker/linkding/pull/765
* Add OCI source annotation to link back to source repo by @Ramblurr in https://github.com/sissbruecker/linkding/pull/701
* Generate fallback URLs for web archive links by @sissbruecker in https://github.com/sissbruecker/linkding/pull/804
* Fix overflow in settings page by @sissbruecker in https://github.com/sissbruecker/linkding/pull/805
* Bump django from 5.0.3 to 5.0.8 by @dependabot in https://github.com/sissbruecker/linkding/pull/795
* Bump certifi from 2023.11.17 to 2024.7.4 by @dependabot in https://github.com/sissbruecker/linkding/pull/775
* Bump djangorestframework from 3.14.0 to 3.15.2 by @dependabot in https://github.com/sissbruecker/linkding/pull/769
* Bump urllib3 from 2.1.0 to 2.2.2 by @dependabot in https://github.com/sissbruecker/linkding/pull/762

### New Contributors
* @fibelatti made their first contribution in https://github.com/sissbruecker/linkding/pull/772
* @volumedata21 made their first contribution in https://github.com/sissbruecker/linkding/pull/800
* @QYG2297248353 made their first contribution in https://github.com/sissbruecker/linkding/pull/788
* @watsonbox made their first contribution in https://github.com/sissbruecker/linkding/pull/765
* @Ramblurr made their first contribution in https://github.com/sissbruecker/linkding/pull/701

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.31.0...v1.31.1

---

## v1.31.0 (16/06/2024)

### What's Changed
* Add support for bookmark thumbnails by @vslinko in https://github.com/sissbruecker/linkding/pull/721
* Automatically add tags to bookmarks based on URL pattern by @vslinko in https://github.com/sissbruecker/linkding/pull/736
* Load bookmark thumbnails after import by @vslinko in https://github.com/sissbruecker/linkding/pull/724
* Load missing thumbnails after enabling the feature by @sissbruecker in https://github.com/sissbruecker/linkding/pull/725
* Thumbnails lazy loading by @vslinko in https://github.com/sissbruecker/linkding/pull/734
* Add option for disabling tag grouping by @vslinko in https://github.com/sissbruecker/linkding/pull/735
* Preview auto tags in bookmark form by @sissbruecker in https://github.com/sissbruecker/linkding/pull/737
* Hide tooltip on mobile by @vslinko in https://github.com/sissbruecker/linkding/pull/733
* Bump requests from 2.31.0 to 2.32.0 by @dependabot in https://github.com/sissbruecker/linkding/pull/740

### New Contributors
* @vslinko made their first contribution in https://github.com/sissbruecker/linkding/pull/721

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.30.0...v1.31.0

---

## v1.30.0 (20/04/2024)

### What's Changed
* Add reader mode by @sissbruecker in https://github.com/sissbruecker/linkding/pull/703
* Allow uploading custom files for bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/713
* Add option for marking bookmarks as unread by default by @ab623 in https://github.com/sissbruecker/linkding/pull/706
* Make blocking cookie banners more reliable by @sissbruecker in https://github.com/sissbruecker/linkding/pull/699
* Close bookmark details with escape by @sissbruecker in https://github.com/sissbruecker/linkding/pull/702
* Show proper name for bookmark assets in admin by @ab623 in https://github.com/sissbruecker/linkding/pull/708
* Bump sqlparse from 0.4.4 to 0.5.0 by @dependabot in https://github.com/sissbruecker/linkding/pull/704

### New Contributors
* @ab623 made their first contribution in https://github.com/sissbruecker/linkding/pull/706

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.29.0...v1.30.0

---

## v1.29.0 (14/04/2024)

### What's Changed
* Remove ads and cookie banners from HTML snapshots by @sissbruecker in https://github.com/sissbruecker/linkding/pull/695
* Add button for creating missing HTML snapshots by @sissbruecker in https://github.com/sissbruecker/linkding/pull/696
* Refresh file list when there are queued snapshots by @sissbruecker in https://github.com/sissbruecker/linkding/pull/697
* Bump idna from 3.6 to 3.7 by @dependabot in https://github.com/sissbruecker/linkding/pull/694


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.28.0...v1.29.0

---

## v1.28.0 (09/04/2024)

### What's Changed
* Add option to disable SSL verification for OIDC by @akaSyntaax in https://github.com/sissbruecker/linkding/pull/684
* Add full backup method by @sissbruecker in https://github.com/sissbruecker/linkding/pull/686
* Truncate snapshot filename for long URLs by @sissbruecker in https://github.com/sissbruecker/linkding/pull/687
* Add option for customizing single-file timeout by @pettijohn in https://github.com/sissbruecker/linkding/pull/688
* Add option for passing arguments to single-file command by @pettijohn in https://github.com/sissbruecker/linkding/pull/691
* Fix typo by @tianheg in https://github.com/sissbruecker/linkding/pull/689

### New Contributors
* @akaSyntaax made their first contribution in https://github.com/sissbruecker/linkding/pull/684
* @pettijohn made their first contribution in https://github.com/sissbruecker/linkding/pull/688

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.27.1...v1.28.0

---

## v1.27.1 (07/04/2024)

### What's Changed
* Fix HTML snapshot errors related to single-file-cli by @sissbruecker in https://github.com/sissbruecker/linkding/pull/683
* Replace django-background-tasks with huey by @sissbruecker in https://github.com/sissbruecker/linkding/pull/657
* Add Authelia OIDC example to docs by @hugo-vrijswijk in https://github.com/sissbruecker/linkding/pull/675


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.27.0...v1.27.1

---

## v1.27.0 (01/04/2024)

### What's Changed
* Archive snapshots of websites locally by @sissbruecker in https://github.com/sissbruecker/linkding/pull/672
* Add Railway hosting option by @tianheg in https://github.com/sissbruecker/linkding/pull/661
* Add how to for increasing the font size by @sissbruecker in https://github.com/sissbruecker/linkding/pull/667

### New Contributors
* @tianheg made their first contribution in https://github.com/sissbruecker/linkding/pull/661

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.26.0...v1.27.0

---

## v1.26.0 (30/03/2024)

### What's Changed
* Add option for showing bookmark description as separate block by @sissbruecker in https://github.com/sissbruecker/linkding/pull/663
* Add bookmark details view by @sissbruecker in https://github.com/sissbruecker/linkding/pull/665
* Make bookmark list actions configurable by @sissbruecker in https://github.com/sissbruecker/linkding/pull/666
* Bump black from 24.1.1 to 24.3.0 by @dependabot in https://github.com/sissbruecker/linkding/pull/662


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.25.0...v1.26.0

---

## v1.25.0 (18/03/2024)

### What's Changed
* Improve PWA capabilities by @hugo-vrijswijk in https://github.com/sissbruecker/linkding/pull/630
* build improvements by @hugo-vrijswijk in https://github.com/sissbruecker/linkding/pull/649
* Add support for oidc by @Nighmared in https://github.com/sissbruecker/linkding/pull/389
* Add option for custom CSS by @sissbruecker in https://github.com/sissbruecker/linkding/pull/652
* Update backup location to safe directory by @bphenriques in https://github.com/sissbruecker/linkding/pull/653
* Include web archive link in /api/bookmarks/ by @sissbruecker in https://github.com/sissbruecker/linkding/pull/655
* Add RSS feeds for shared bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/656
* Bump django from 5.0.2 to 5.0.3 by @dependabot in https://github.com/sissbruecker/linkding/pull/658

### New Contributors
* @hugo-vrijswijk made their first contribution in https://github.com/sissbruecker/linkding/pull/630
* @Nighmared made their first contribution in https://github.com/sissbruecker/linkding/pull/389
* @bphenriques made their first contribution in https://github.com/sissbruecker/linkding/pull/653

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.24.2...v1.25.0

---

## v1.24.2 (16/03/2024)

### What's Changed
* Fix logout button by @sissbruecker in https://github.com/sissbruecker/linkding/pull/648


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.24.1...v1.24.2

---

## v1.24.1 (16/03/2024)

### What's Changed
* Bump dependencies by @sissbruecker in https://github.com/sissbruecker/linkding/pull/618
* Persist secret key in data folder by @sissbruecker in https://github.com/sissbruecker/linkding/pull/620
* Group ideographic characters in tag cloud by @jonathan-s in https://github.com/sissbruecker/linkding/pull/613
* Bump django from 5.0.1 to 5.0.2 by @dependabot in https://github.com/sissbruecker/linkding/pull/625
* Add k8s setup to community section by @jzck in https://github.com/sissbruecker/linkding/pull/633
* Added a new Linkding client to community section by @JGeek00 in https://github.com/sissbruecker/linkding/pull/638

### New Contributors
* @jzck made their first contribution in https://github.com/sissbruecker/linkding/pull/633
* @JGeek00 made their first contribution in https://github.com/sissbruecker/linkding/pull/638

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.24.0...v1.24.1

---

## v1.24.0 (27/01/2024)

### What's Changed
* Support Open Graph description by @jonathan-s in https://github.com/sissbruecker/linkding/pull/602
* Add tooltip to truncated bookmark titles by @jonathan-s in https://github.com/sissbruecker/linkding/pull/607
* Improve bulk tag performance by @sissbruecker in https://github.com/sissbruecker/linkding/pull/612
* Increase tag limit in tag autocomplete by @hypebeast in https://github.com/sissbruecker/linkding/pull/581
* Add CapRover as managed hosting option by @adamshand in https://github.com/sissbruecker/linkding/pull/585
* Bump playwright dependencies by @jonathan-s in https://github.com/sissbruecker/linkding/pull/601
* Adjust archive.org donation link in general.html by @JnsDornbusch in https://github.com/sissbruecker/linkding/pull/603

### New Contributors
* @hypebeast made their first contribution in https://github.com/sissbruecker/linkding/pull/581
* @adamshand made their first contribution in https://github.com/sissbruecker/linkding/pull/585
* @jonathan-s made their first contribution in https://github.com/sissbruecker/linkding/pull/601
* @JnsDornbusch made their first contribution in https://github.com/sissbruecker/linkding/pull/603

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.23.1...v1.24.0

---

## v1.23.1 (08/12/2023)

### What's Changed
* Properly encode search query param by @sissbruecker in https://github.com/sissbruecker/linkding/pull/587

> [!WARNING] 
> *This resolves a security vulnerability in linkding. Everyone is encouraged to upgrade to the latest version as soon as possible.*

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.23.0...v1.23.1

---

## v1.23.0 (24/11/2023)

### What's Changed
* Add Alpine based Docker image (experimental) by @sissbruecker in https://github.com/sissbruecker/linkding/pull/570
* Add backup CLI command by @sissbruecker in https://github.com/sissbruecker/linkding/pull/571
* Update browser extension links by @OPerepadia in https://github.com/sissbruecker/linkding/pull/574
* Include archived bookmarks in export by @sissbruecker in https://github.com/sissbruecker/linkding/pull/579

### New Contributors
* @OPerepadia made their first contribution in https://github.com/sissbruecker/linkding/pull/574

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.3...v1.23.0

---

## v1.22.3 (04/11/2023)

### What's Changed
* Fix RSS feed not handling None values  by @vitormarcal in https://github.com/sissbruecker/linkding/pull/569
* Bump django from 4.1.10 to 4.1.13 by @dependabot in https://github.com/sissbruecker/linkding/pull/567

### New Contributors
* @vitormarcal made their first contribution in https://github.com/sissbruecker/linkding/pull/569

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.2...v1.22.3

---

## v1.22.2 (27/10/2023)

### What's Changed
* Fix search options not opening on iOS by @sissbruecker in https://github.com/sissbruecker/linkding/pull/549
* Bump urllib3 from 1.26.11 to 1.26.17 by @dependabot in https://github.com/sissbruecker/linkding/pull/542
* Add iOS shortcut to community section by @andrewdolphin in https://github.com/sissbruecker/linkding/pull/550
* Disable editing of search preferences in user admin by @sissbruecker in https://github.com/sissbruecker/linkding/pull/555
* Add feed2linkding to community section by @Strubbl in https://github.com/sissbruecker/linkding/pull/544
* Sanitize RSS feed to remove control characters by @sissbruecker in https://github.com/sissbruecker/linkding/pull/565
* Bump urllib3 from 1.26.17 to 1.26.18 by @dependabot in https://github.com/sissbruecker/linkding/pull/560

### New Contributors
* @andrewdolphin made their first contribution in https://github.com/sissbruecker/linkding/pull/550
* @Strubbl made their first contribution in https://github.com/sissbruecker/linkding/pull/544

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.1...v1.22.2

---

## v1.22.1 (06/10/2023)

### What's Changed
* Fix memory leak with SQLite by @sissbruecker in https://github.com/sissbruecker/linkding/pull/548


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.22.0...v1.22.1

---

## v1.22.0 (01/10/2023)

### What's Changed
* Fix case-insensitive search for unicode characters in SQLite by @sissbruecker in https://github.com/sissbruecker/linkding/pull/520
* Add sort option to bookmark list by @sissbruecker in https://github.com/sissbruecker/linkding/pull/522
* Add button to show tags on smaller screens by @sissbruecker in https://github.com/sissbruecker/linkding/pull/529
* Make code blocks in notes scrollable by @sissbruecker in https://github.com/sissbruecker/linkding/pull/530
* Add filter for shared state by @sissbruecker in https://github.com/sissbruecker/linkding/pull/531
* Add support for exporting/importing bookmark notes by @sissbruecker in https://github.com/sissbruecker/linkding/pull/532
* Add filter for unread state by @sissbruecker in https://github.com/sissbruecker/linkding/pull/535
* Allow saving search preferences by @sissbruecker in https://github.com/sissbruecker/linkding/pull/540
* Add user profile endpoint by @sissbruecker in https://github.com/sissbruecker/linkding/pull/541


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.21.0...v1.22.0

---

## v1.21.1 (26/09/2023)

### What's Changed
* Fix bulk edit to respect searched tags by @sissbruecker in https://github.com/sissbruecker/linkding/pull/537


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.21.0...v1.21.1

---

## v1.21.0 (25/08/2023)

### What's Changed
* Make search autocomplete respect link target setting by @sissbruecker in https://github.com/sissbruecker/linkding/pull/513
* Various CSS improvements by @sissbruecker in https://github.com/sissbruecker/linkding/pull/514
* Display shared state in bookmark list by @sissbruecker in https://github.com/sissbruecker/linkding/pull/515
* Allow bulk editing unread and shared state of bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/517
* Bump uwsgi from 2.0.20 to 2.0.22 by @dependabot in https://github.com/sissbruecker/linkding/pull/516


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.20.1...v1.21.0

---

## v1.20.1 (23/08/2023)

### What's Changed
* Update cached styles and scripts after version change by @sissbruecker in https://github.com/sissbruecker/linkding/pull/510


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.20.0...v1.20.1

---

## v1.20.0 (22/08/2023)

### What's Changed
* Add option to share bookmarks publicly by @sissbruecker in https://github.com/sissbruecker/linkding/pull/503
* Various improvements to favicons by @sissbruecker in https://github.com/sissbruecker/linkding/pull/504
* Add support for PRIVATE flag in import and export by @sissbruecker in https://github.com/sissbruecker/linkding/pull/505
* Avoid page reload when triggering actions in bookmark list by @sissbruecker in https://github.com/sissbruecker/linkding/pull/506


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.19.1...v1.20.0

---

## v1.19.1 (29/07/2023)

### What's Changed
* Add Postman Collection to Community section of README by @gingerbeardman in https://github.com/sissbruecker/linkding/pull/476
* Added Dev Container support by @acbgbca in https://github.com/sissbruecker/linkding/pull/474
* Added Apple web-app meta tag #358 by @acbgbca in https://github.com/sissbruecker/linkding/pull/359
* Bump requests from 2.28.1 to 2.31.0 by @dependabot in https://github.com/sissbruecker/linkding/pull/478
* Allow passing title and description to new bookmark form by @acbgbca in https://github.com/sissbruecker/linkding/pull/479
* Enable WAL to avoid locked database lock errors by @sissbruecker in https://github.com/sissbruecker/linkding/pull/480
* Fix website loader content encoding detection by @sissbruecker in https://github.com/sissbruecker/linkding/pull/482
* Bump certifi from 2022.12.7 to 2023.7.22 by @dependabot in https://github.com/sissbruecker/linkding/pull/497
* Bump django from 4.1.9 to 4.1.10 by @dependabot in https://github.com/sissbruecker/linkding/pull/494

### New Contributors
* @gingerbeardman made their first contribution in https://github.com/sissbruecker/linkding/pull/476
* @acbgbca made their first contribution in https://github.com/sissbruecker/linkding/pull/474

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.19.0...v1.19.1

---

## v1.19.0 (20/05/2023)

### What's Changed
* Add notes to bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/472


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.18.0...v1.19.0

---

## v1.18.0 (18/05/2023)

### What's Changed
* Make search case-insensitive on Postgres by @sissbruecker in https://github.com/sissbruecker/linkding/pull/432
* Allow searching for tags without hash character by @sissbruecker in https://github.com/sissbruecker/linkding/pull/449
* Prevent zoom-in after focusing an input on small viewports on iOS devices by @puresick in https://github.com/sissbruecker/linkding/pull/440
* Add database options by @plockaby in https://github.com/sissbruecker/linkding/pull/406
* Allow to log real client ip in logs when using a reverse proxy by @fmenabe in https://github.com/sissbruecker/linkding/pull/398
* Add option to display URL below title by @bah0 in https://github.com/sissbruecker/linkding/pull/365
* Add LinkThing iOS app to community section by @amoscardino in https://github.com/sissbruecker/linkding/pull/446
* Bump django from 4.1.7 to 4.1.9 by @dependabot in https://github.com/sissbruecker/linkding/pull/466
* Bump sqlparse from 0.4.2 to 0.4.4 by @dependabot in https://github.com/sissbruecker/linkding/pull/455

### New Contributors
* @amoscardino made their first contribution in https://github.com/sissbruecker/linkding/pull/446
* @puresick made their first contribution in https://github.com/sissbruecker/linkding/pull/440
* @plockaby made their first contribution in https://github.com/sissbruecker/linkding/pull/406
* @fmenabe made their first contribution in https://github.com/sissbruecker/linkding/pull/398
* @bah0 made their first contribution in https://github.com/sissbruecker/linkding/pull/365

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.17.2...v1.18.0

---

## v1.17.2 (18/02/2023)

### What's Changed
* Escape texts in exported HTML by @sissbruecker in https://github.com/sissbruecker/linkding/pull/429
* Bump django from 4.1.2 to 4.1.7 by @dependabot in https://github.com/sissbruecker/linkding/pull/427
* Make health check in Dockerfile honor context path setting by @mrex in https://github.com/sissbruecker/linkding/pull/407
* Disable autocapitalization for tag input form by @joshdick in https://github.com/sissbruecker/linkding/pull/395

### New Contributors
* @mrex made their first contribution in https://github.com/sissbruecker/linkding/pull/407
* @joshdick made their first contribution in https://github.com/sissbruecker/linkding/pull/395

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.17.1...v1.17.2

---

## v1.17.1 (22/01/2023)

### What's Changed
* Fix favicon being cleared by web archive snapshot task by @sissbruecker in https://github.com/sissbruecker/linkding/pull/405


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.17.0...v1.17.1

---

## v1.17.0 (21/01/2023)

### What's Changed
* Add Health Check endpoint  by @mckennajones in https://github.com/sissbruecker/linkding/pull/392
* Cache website metadata to avoid duplicate scraping by @sissbruecker in https://github.com/sissbruecker/linkding/pull/401
* Prefill form if URL is already bookmarked by @sissbruecker in https://github.com/sissbruecker/linkding/pull/402
* Add option for showing bookmark favicons by @sissbruecker in https://github.com/sissbruecker/linkding/pull/390


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.16.1...v1.17.0

---

## v1.16.1 (20/01/2023)

### What's Changed
* Fix bookmark website metadata not being updated when URL changes by @sissbruecker in https://github.com/sissbruecker/linkding/pull/400
* Bump django from 4.1 to 4.1.2 by @dependabot in https://github.com/sissbruecker/linkding/pull/391
* Bump certifi from 2022.6.15 to 2022.12.7 by @dependabot in https://github.com/sissbruecker/linkding/pull/374
* Bump minimatch from 3.0.4 to 3.1.2 by @dependabot in https://github.com/sissbruecker/linkding/pull/366


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.16.0...v1.16.1

---

## v1.16.0 (12/01/2023)

### What's Changed
* Add postgres as database engine by @tomamplius in https://github.com/sissbruecker/linkding/pull/388
* Gracefully stop docker container when it receives SIGTERM by @mckennajones in https://github.com/sissbruecker/linkding/pull/368
* Limit document size for website scraper by @sissbruecker in https://github.com/sissbruecker/linkding/pull/354
* Add error handling for checking latest version by @sissbruecker in https://github.com/sissbruecker/linkding/pull/360
* Trim website metadata title and description by @luca1197 in https://github.com/sissbruecker/linkding/pull/383
* Only show admin link for superusers by @AlexanderS in https://github.com/sissbruecker/linkding/pull/384
* Add apache reverse proxy documentation. by @jhauris in https://github.com/sissbruecker/linkding/pull/371
* Correct LD_ENABLE_AUTH_PROXY documentation by @jhauris in https://github.com/sissbruecker/linkding/pull/379
* Android HTTP shortcuts v3 by @kzshantonu in https://github.com/sissbruecker/linkding/pull/387

### New Contributors
* @jhauris made their first contribution in https://github.com/sissbruecker/linkding/pull/371
* @AlexanderS made their first contribution in https://github.com/sissbruecker/linkding/pull/384
* @mckennajones made their first contribution in https://github.com/sissbruecker/linkding/pull/368
* @tomamplius made their first contribution in https://github.com/sissbruecker/linkding/pull/388
* @luca1197 made their first contribution in https://github.com/sissbruecker/linkding/pull/383

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.15.1...v1.16.0

---

## v1.15.1 (05/10/2022)

### What's Changed
* Fix static file dir warning by @sissbruecker in https://github.com/sissbruecker/linkding/pull/350
* Add setting and documentation for fixing CSRF errors by @sissbruecker in https://github.com/sissbruecker/linkding/pull/349


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.15.0...v1.15.1

---

## v1.15.0 (11/09/2022)

### What's Changed
* Bump Django and other dependencies by @sissbruecker in https://github.com/sissbruecker/linkding/pull/331
* Add option to create initial superuser by @sissbruecker in https://github.com/sissbruecker/linkding/pull/323
* Improved Android HTTP Shortcuts doc by @kzshantonu in https://github.com/sissbruecker/linkding/pull/330
* Minify bookmark list HTML by @sissbruecker in https://github.com/sissbruecker/linkding/pull/332
* Bump python version to 3.10 by @sissbruecker in https://github.com/sissbruecker/linkding/pull/333
* Fix error when deleting all bookmarks in admin by @sissbruecker in https://github.com/sissbruecker/linkding/pull/336
* Improve bookmark query performance by @sissbruecker in https://github.com/sissbruecker/linkding/pull/334
* Prevent rate limit errors in wayback machine API by @sissbruecker in https://github.com/sissbruecker/linkding/pull/339


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.14.0...v1.15.0

---

## v1.14.0 (14/08/2022)

### What's Changed
* Add support for context path by @s2marine in https://github.com/sissbruecker/linkding/pull/313
* Add support for authentication proxies by @sissbruecker in https://github.com/sissbruecker/linkding/pull/321
* Add bookmark list keyboard navigation by @sissbruecker in https://github.com/sissbruecker/linkding/pull/320
* Skip updating website metadata on edit unless URL has changed by @sissbruecker in https://github.com/sissbruecker/linkding/pull/318
* Add simple docs of the new `shared` API parameter by @bachya in https://github.com/sissbruecker/linkding/pull/312
* Add project linka to community section in README by @cmsax in https://github.com/sissbruecker/linkding/pull/319
* Order tags in test_should_create_new_bookmark by @RoGryza in https://github.com/sissbruecker/linkding/pull/310
* Bump django from 3.2.14 to 3.2.15 by @dependabot in https://github.com/sissbruecker/linkding/pull/316

### New Contributors
* @s2marine made their first contribution in https://github.com/sissbruecker/linkding/pull/313
* @RoGryza made their first contribution in https://github.com/sissbruecker/linkding/pull/310
* @cmsax made their first contribution in https://github.com/sissbruecker/linkding/pull/319

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.13.0...v1.14.0

---

## v1.13.0 (04/08/2022)

### What's Changed
* Add bookmark sharing by @sissbruecker in https://github.com/sissbruecker/linkding/pull/311
* Display selected tags in tag cloud by @sissbruecker and @jhauris in https://github.com/sissbruecker/linkding/pull/307
* Update unread flag when saving duplicate URL by @sissbruecker in https://github.com/sissbruecker/linkding/pull/306


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.12.0...v1.13.0

---

## v1.12.0 (23/07/2022)

### What's Changed
* Add read it later functionality by @sissbruecker in https://github.com/sissbruecker/linkding/pull/304
* Add RSS feeds by @sissbruecker in https://github.com/sissbruecker/linkding/pull/305
* Add bookmarklet to community by @ukcuddlyguy in https://github.com/sissbruecker/linkding/pull/293
* Shorten and simplify example bookmarklet in documentation by @FunctionDJ in https://github.com/sissbruecker/linkding/pull/297
* Fix typo by @kianmeng in https://github.com/sissbruecker/linkding/pull/295
* Bump django from 3.2.13 to 3.2.14 by @dependabot in https://github.com/sissbruecker/linkding/pull/294
* Bump svelte from 3.46.4 to 3.49.0 by @dependabot in https://github.com/sissbruecker/linkding/pull/299
* Bump terser from 5.5.1 to 5.14.2 by @dependabot in https://github.com/sissbruecker/linkding/pull/302

### New Contributors
* @ukcuddlyguy made their first contribution in https://github.com/sissbruecker/linkding/pull/293
* @FunctionDJ made their first contribution in https://github.com/sissbruecker/linkding/pull/297
* @kianmeng made their first contribution in https://github.com/sissbruecker/linkding/pull/295

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.11.1...v1.12.0

---

## v1.11.1 (03/07/2022)

### What's Changed
* Fix duplicate tags on import by @wahlm in https://github.com/sissbruecker/linkding/pull/289
* Add apple-touch-icon by @daveonkels in https://github.com/sissbruecker/linkding/pull/282
* Bump waybackpy to 3.0.6 by @dustinblackman in https://github.com/sissbruecker/linkding/pull/281

### New Contributors
* @wahlm made their first contribution in https://github.com/sissbruecker/linkding/pull/289
* @daveonkels made their first contribution in https://github.com/sissbruecker/linkding/pull/282
* @dustinblackman made their first contribution in https://github.com/sissbruecker/linkding/pull/281

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.11.0...v1.11.1

---

## v1.11.0 (26/05/2022)

### What's Changed
* Add background tasks to admin by @sissbruecker in https://github.com/sissbruecker/linkding/pull/264
* Improve about section by @sissbruecker in https://github.com/sissbruecker/linkding/pull/265
* Allow creating archived bookmark through REST API by @kencx in https://github.com/sissbruecker/linkding/pull/268
* Add PATCH support to bookmarks endpoint by @sissbruecker in https://github.com/sissbruecker/linkding/pull/269
* Add community reference to linkding-cli by @bachya in https://github.com/sissbruecker/linkding/pull/270

### New Contributors
* @kencx made their first contribution in https://github.com/sissbruecker/linkding/pull/268

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.10.1...v1.11.0

---

## v1.10.1 (21/05/2022)

### What's Changed
* Fake request headers to reduce bot detection by @sissbruecker in https://github.com/sissbruecker/linkding/pull/263


**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.10.0...v1.10.1

---

## v1.10.0 (21/05/2022)

### What's Changed
* Add to managed hosting options by @m3nu in https://github.com/sissbruecker/linkding/pull/253
* Add community reference to aiolinkding by @bachya in https://github.com/sissbruecker/linkding/pull/259
* Improve import performance by @sissbruecker in https://github.com/sissbruecker/linkding/pull/261
* Update how-to.md to fix unclear/paraphrased Safari action in IOS Shortcuts by @feoh in https://github.com/sissbruecker/linkding/pull/260
* Allow searching for untagged bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/226

### New Contributors
* @m3nu made their first contribution in https://github.com/sissbruecker/linkding/pull/253
* @bachya made their first contribution in https://github.com/sissbruecker/linkding/pull/259
* @feoh made their first contribution in https://github.com/sissbruecker/linkding/pull/260

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.9.0...v1.10.0

---

## v1.9.0 (14/05/2022)

### What's Changed
* Scroll menu items into view when using keyboard by @sissbruecker in https://github.com/sissbruecker/linkding/pull/248
* Add whitespace after auto-completed tag by @sissbruecker in https://github.com/sissbruecker/linkding/pull/249
* Bump django from 3.2.12 to 3.2.13 by @dependabot in https://github.com/sissbruecker/linkding/pull/244
* Add community helm chart reference to readme by @pascaliske in https://github.com/sissbruecker/linkding/pull/242
* Feature: Shortcut key for new bookmark by @rithask in https://github.com/sissbruecker/linkding/pull/241
* Clarify archive.org feature by @clach04 in https://github.com/sissbruecker/linkding/pull/229
* Make Internet Archive integration opt-in by @sissbruecker in https://github.com/sissbruecker/linkding/pull/250

### New Contributors
* @pascaliske made their first contribution in https://github.com/sissbruecker/linkding/pull/242
* @rithask made their first contribution in https://github.com/sissbruecker/linkding/pull/241
* @clach04 made their first contribution in https://github.com/sissbruecker/linkding/pull/229

**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.8.8...v1.9.0

---

## v1.8.8 (27/03/2022)

- [**bug**] Prevent bookmark actions through get requests
- [**bug**] Prevent external redirects

---

## v1.8.7 (26/03/2022)

- [**bug**] Increase request buffer size [#28](https://github.com/sissbruecker/linkding/issues/28)
- [**enhancement**]  Allow specifying port through LINKDING_PORT environment variable [#156](https://github.com/sissbruecker/linkding/pull/156)
- [**chore**] Bump NPM packages [#224](https://github.com/sissbruecker/linkding/pull/224)

---

## v1.8.6 (25/03/2022)

- [bug] fix bookmark access restrictions
- [bug] prevent external redirects
- [chore] bump dependencies

---

## v1.8.5 (12/12/2021)

- [**bug**] Ensure tag names do not contain spaces [#182](https://github.com/sissbruecker/linkding/issues/182)
- [**bug**] Consider not copying whole GIT repository to Docker image [#174](https://github.com/sissbruecker/linkding/issues/174)
- [**enhancement**] Make bookmarks count column in admin sortable [#183](https://github.com/sissbruecker/linkding/pull/183)

---

## v1.8.4 (16/10/2021)

- [**enhancement**] Allow non-admin users to change their password [#166](https://github.com/sissbruecker/linkding/issues/166)

---

## v1.8.3 (03/10/2021)

- [**enhancement**] Enhancement: let user configure to open links in same tab instead on a new window/tab [#27](https://github.com/sissbruecker/linkding/issues/27)

---

## v1.8.2 (02/10/2021)

- [**bug**] Fix jumping search box [#163](https://github.com/sissbruecker/linkding/pull/163)

---

## v1.8.1 (01/10/2021)

- [**enhancement**] Add global shortcut for search [#161](https://github.com/sissbruecker/linkding/pull/161)
  - allows to press `s` to focus the search input

---

## v1.8.0 (04/09/2021)

- [**enhancement**] Wayback Machine Integration [#59](https://github.com/sissbruecker/linkding/issues/59)
  - Automatically creates snapshots of bookmarked websites on [web archive](https://archive.org/web/)
  - This is one of the largest changes yet and adds a task processor that runs as a separate process in the background. If you run into issues with this feature, it can be disabled using the [LD_DISABLE_BACKGROUND_TASKS](https://github.com/sissbruecker/linkding/blob/master/docs/Options.md#ld_disable_background_tasks) option

---

## v1.7.2 (26/08/2021)

- [**enhancement**] Add support for nanosecond resolution timestamps for bookmark import (e.g. Google Bookmarks) [#146](https://github.com/sissbruecker/linkding/issues/146)

---

## v1.7.1 (25/08/2021)

- [**bug**] umlaut/non-ascii characters broken when using bookmarklet (firefox) [#148](https://github.com/sissbruecker/linkding/issues/148)
- [**bug**] Bookmark import accepts empty URL values [#124](https://github.com/sissbruecker/linkding/issues/124)
- [**enhancement**] Show the version in the settings [#104](https://github.com/sissbruecker/linkding/issues/104)

---

## v1.7.0 (17/08/2021)

- Upgrade to Django 3
- Bump other dependencies

---

## v1.6.5 (15/08/2021)

- [**enhancement**] query with multiple hashtags very slow [#112](https://github.com/sissbruecker/linkding/issues/112)

---

## v1.6.4 (13/05/2021)

- Update dependencies for security fixes

---

## v1.6.3 (06/04/2021)

- [**bug**] relative names use the wrong "today" after day change [#107](https://github.com/sissbruecker/linkding/issues/107)

---

## v1.6.2 (04/04/2021)

- [**enhancement**] Expose `date_added` in UI [#85](https://github.com/sissbruecker/linkding/issues/85)
- [**closed**] Archived bookmarks - no result when searching for a word which is used only as tag [#83](https://github.com/sissbruecker/linkding/issues/83)
- [**closed**] Add archive/unarchive button to edit bookmark page [#82](https://github.com/sissbruecker/linkding/issues/82)
- [**enhancement**] Make scraped title and description editable [#80](https://github.com/sissbruecker/linkding/issues/80)

---

## v1.6.1 (31/03/2021)

- Expose date_added in UI [#85](https://github.com/sissbruecker/linkding/issues/85)

---

## v1.6.0 (28/03/2021)

- Bulk edit mode [#101](https://github.com/sissbruecker/linkding/pull/101)

---

## v1.5.0 (28/03/2021)

- [**closed**] Add a dark mode [#49](https://github.com/sissbruecker/linkding/issues/49)

---

## v1.4.1 (20/03/2021)

- Security patches
- Documentation improvements

---

## v1.4.0 (24/02/2021)

- [**enhancement**] Improve admin utilization [#76](https://github.com/sissbruecker/linkding/issues/76)

---

## v1.3.3 (18/02/2021)

- [**closed**] Missing "description" request body parameter in API causes 500 [#78](https://github.com/sissbruecker/linkding/issues/78)

---

## v1.3.2 (18/02/2021)

- [**closed**] /archive and /unarchive API routes return 404 [#77](https://github.com/sissbruecker/linkding/issues/77)
- [**closed**] API - /api/check_url?url= with token authetification [#55](https://github.com/sissbruecker/linkding/issues/55)

---

## v1.3.1 (15/02/2021)

[enhancement] Enhance delete links with inline confirmation

---

## v1.3.0 (14/02/2021)

- [**closed**] Novice help. [#71](https://github.com/sissbruecker/linkding/issues/71)
- [**closed**] Option to create bookmarks public [#70](https://github.com/sissbruecker/linkding/issues/70)
- [**enhancement**] Show URL if title is not available [#64](https://github.com/sissbruecker/linkding/issues/64)
- [**bug**] minor ui nitpicks [#62](https://github.com/sissbruecker/linkding/issues/62)
- [**enhancement**] add an archive function [#46](https://github.com/sissbruecker/linkding/issues/46)
- [**closed**] remove non fqdn check and alert [#36](https://github.com/sissbruecker/linkding/issues/36)
- [**closed**] Add Lotus Notes links [#22](https://github.com/sissbruecker/linkding/issues/22)

---

## v1.2.1 (12/01/2021)

- [**bug**] Bug: Two equal tags with different capitalisation lead to 500 server errors [#65](https://github.com/sissbruecker/linkding/issues/65)
- [**closed**] Enhancement: category and pagination [#11](https://github.com/sissbruecker/linkding/issues/11)

---

## v1.2.0 (09/01/2021)

- [**closed**] Add Favicon [#58](https://github.com/sissbruecker/linkding/issues/58)
- [**closed**] Make tags case-insensitive [#45](https://github.com/sissbruecker/linkding/issues/45)

---

## v1.1.1 (01/01/2021)

- [**enhancement**] Add docker-compose support [#54](https://github.com/sissbruecker/linkding/pull/54)

---

## v1.1.0 (31/12/2020)

- [**enhancement**] Search autocomplete [#52](https://github.com/sissbruecker/linkding/issues/52)
- [**enhancement**] Improve Netscape bookmarks file parsing [#50](https://github.com/sissbruecker/linkding/issues/50)

---

## v1.0.0 (31/12/2020)

- [**bug**] Import does not import bookmark descriptions [#47](https://github.com/sissbruecker/linkding/issues/47)
- [**enhancement**] Enhancement: return to same page we were on after editing a bookmark [#26](https://github.com/sissbruecker/linkding/issues/26)
- [**bug**] Increase limit on bookmark URL length [#25](https://github.com/sissbruecker/linkding/issues/25)
- [**enhancement**] API for app development [#24](https://github.com/sissbruecker/linkding/issues/24)
- [**enhancement**] Enhancement: detect duplicates at entry time [#23](https://github.com/sissbruecker/linkding/issues/23)
- [**bug**] Error importing bookmarks [#18](https://github.com/sissbruecker/linkding/issues/18)
- [**enhancement**] Enhancement: better administration page [#4](https://github.com/sissbruecker/linkding/issues/4)
- [**enhancement**] Bug: Navigation bar active link stays on add bookmark [#3](https://github.com/sissbruecker/linkding/issues/3)
- [**bug**] CSS Stylesheet presented as text/plain [#2](https://github.com/sissbruecker/linkding/issues/2)

================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2019 Sascha Ißbrücker

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

================================================
FILE: Makefile
================================================
.PHONY: serve

init:
	uv sync
	[ -d data ] || mkdir data data/assets data/favicons data/previews
	uv run manage.py migrate
	npm install

serve:
	uv run manage.py runserver

tasks:
	uv run manage.py run_huey

test:
	uv run pytest -n auto

lint:
	uv run ruff check bookmarks

format:
	uv run ruff format bookmarks
	uv run djlint bookmarks/templates --reformat --quiet --warn
	npx prettier bookmarks/frontend --write
	npx prettier bookmarks/styles --write

prepare-e2e:
	uv run playwright install chromium
	rm -rf static
	npm run build
	uv run manage.py collectstatic --no-input

e2e:
	make prepare-e2e
	uv run pytest bookmarks/tests_e2e -n auto -o "python_files=e2e_test_*.py"

frontend:
	npm run dev


================================================
FILE: README.md
================================================
<div align="center">
    <br>
    <a href="https://github.com/sissbruecker/linkding">
        <img src="assets/header.svg" height="50">
    </a>
    <br>
</div>

##  Introduction

linkding is a bookmark manager that you can host yourself.
It's designed be to be minimal, fast, and easy to set up using Docker.

The name comes from:
- *link* which is often used as a synonym for URLs and bookmarks in common language
- *Ding* which is German for thing
- ...so basically something for managing your links

**Feature Overview:**
- Clean UI optimized for readability
- Organize bookmarks with tags
- Bulk editing, Markdown notes, read it later functionality
- Share bookmarks with other users or guests
- Automatically provides titles, descriptions and icons of bookmarked websites
- Automatically archive websites, either as local HTML file or on Internet Archive
- Import and export bookmarks in Netscape HTML format
- Installable as a Progressive Web App (PWA)
- Extensions for [Firefox](https://addons.mozilla.org/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe), as well as a bookmarklet
- SSO support via OIDC or authentication proxies
- REST API for developing 3rd party apps
- Admin panel for user self-service and raw data access


**Demo:** https://demo.linkding.link/

**Screenshot:**

![Screenshot](/docs/public/linkding-screenshot.png?raw=true "Screenshot")

## Getting Started

The following links help you to get started with linkding:
- [Install linkding on your own server](https://linkding.link/installation) or [check managed hosting options](https://linkding.link/managed-hosting)
- [Install the browser extension](https://linkding.link/browser-extension)
- [Check out community projects](https://linkding.link/community), which include mobile apps, browser extensions, libraries and more

## Documentation

The full documentation is now available at [linkding.link](https://linkding.link/).

If you want to contribute to the documentation, you can find the source files in the `docs` folder.

If you want to contribute a community project, feel free to [submit a PR](https://github.com/sissbruecker/linkding/edit/master/docs/src/content/docs/community.md).

## Contributing

Small improvements, bugfixes and documentation improvements are always welcome. If you want to contribute a larger feature, consider opening an issue first to discuss it. I may choose to ignore PRs for features that don't align with the project's goals or that I don't want to maintain.

## Development

The application is built using the Django web framework. You can get started by checking out the excellent [Django docs](https://docs.djangoproject.com/en/4.1/). The `bookmarks` folder contains the actual bookmark application. Other than that the code should be self-explanatory / standard Django stuff 🙂.

### Prerequisites
- Python 3.13
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
- Node.js

### Setup

Initialize the development environment with:
```
make init
```
This sets up a virtual environment using uv, installs NPM dependencies and runs migrations to create the initial database.

Create a user for the frontend:
```
uv run manage.py createsuperuser --username=joe --email=joe@example.com
```

Run the frontend build for bundling frontend components with:
```
make frontend
```

Then start the Django development server with:
```
make serve
```
The frontend is now available under http://localhost:8000

### Tests

Run all tests with pytest:
```
make test
```


### Linting

Run linting with ruff:
```
make lint
```

### Formatting

Format Python code with ruff, Django templates with djlint, and JavaScript code with prettier:
```
make format
```

### DevContainers

This repository also supports DevContainers: [![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/sissbruecker/linkding.git)

Once checked out, only the following commands are required to get started:

Create a user for the frontend:
```
uv run manage.py createsuperuser --username=joe --email=joe@example.com
```
Start the Node.js development server (used for compiling JavaScript components like tag auto-completion) with:
```
make frontend
```
Start the Django development server with:
```
make serve
```
The frontend is now available under http://localhost:8000


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

| Version | Supported          |
| ------- | ------------------ |
| 1.10.x   | :white_check_mark: |

## Reporting a Vulnerability

To report a vulnerability, please send a mail to: 588ex5zl8@mozmail.com

I'll try to get back to you as soon as possible.


================================================
FILE: bookmarks/__init__.py
================================================


================================================
FILE: bookmarks/admin.py
================================================
import os

from django import forms
from django.contrib import admin, messages
from django.contrib.admin import AdminSite
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.core.paginator import Paginator
from django.db.models import Count, QuerySet
from django.shortcuts import render
from django.urls import path
from django.utils.translation import gettext, ngettext
from huey.contrib.djhuey import HUEY as huey

from bookmarks.models import (
    ApiToken,
    Bookmark,
    BookmarkAsset,
    BookmarkBundle,
    FeedToken,
    Tag,
    Toast,
    UserProfile,
)
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark


# Custom paginator to paginate through Huey tasks
class TaskPaginator(Paginator):
    def __init__(self):
        super().__init__(self, 100)
        self.task_count = huey.storage.queue_size()

    @property
    def count(self):
        return self.task_count

    def page(self, number):
        limit = self.per_page
        offset = (number - 1) * self.per_page
        return self._get_page(
            self.enqueued_items(limit, offset),
            number,
            self,
        )

    # Copied from Huey's SqliteStorage with some modifications to allow pagination
    def enqueued_items(self, limit, offset):
        def to_bytes(b):
            return bytes(b) if not isinstance(b, bytes) else b

        sql = "select data from task where queue=? order by priority desc, id limit ? offset ?"
        params = (huey.storage.name, limit, offset)

        serialized_tasks = [
            to_bytes(i) for (i,) in huey.storage.sql(sql, params, results=True)
        ]
        return [huey.deserialize_task(task) for task in serialized_tasks]


# Custom view to display Huey tasks in the admin
def background_task_view(request):
    page_number = int(request.GET.get("p", 1))
    paginator = TaskPaginator()
    page = paginator.get_page(page_number)
    page_range = paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=2)
    context = {
        **linkding_admin_site.each_context(request),
        "title": "Background tasks",
        "page": page,
        "page_range": page_range,
        "tasks": page.object_list,
    }
    return render(request, "admin/background_tasks.html", context)


class LinkdingAdminSite(AdminSite):
    site_header = "linkding administration"
    site_title = "linkding Admin"

    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path("tasks/", background_task_view, name="background_tasks"),
        ]
        return custom_urls + urls

    def get_app_list(self, request, app_label=None):
        app_list = super().get_app_list(request, app_label)
        context_path = os.getenv("LD_CONTEXT_PATH", "")
        app_list += [
            {
                "name": "Huey",
                "app_label": "huey_app",
                "models": [
                    {
                        "name": "Queued tasks",
                        "object_name": "background_tasks",
                        "admin_url": f"/{context_path}admin/tasks/",
                        "view_only": True,
                    }
                ],
            }
        ]
        return app_list


class AdminBookmark(admin.ModelAdmin):
    list_display = ("resolved_title", "url", "is_archived", "owner", "date_added")
    search_fields = (
        "title",
        "description",
        "website_title",
        "website_description",
        "url",
        "tags__name",
    )
    list_filter = (
        "owner__username",
        "is_archived",
        "unread",
        "tags",
    )
    ordering = ("-date_added",)
    actions = [
        "delete_selected_bookmarks",
        "archive_selected_bookmarks",
        "unarchive_selected_bookmarks",
        "mark_as_read",
        "mark_as_unread",
    ]

    def get_actions(self, request):
        actions = super().get_actions(request)
        # Remove default delete action, which gets replaced by delete_selected_bookmarks below
        # The default action shows a confirmation page which can fail in production when selecting all bookmarks and the
        # number of objects to delete exceeds the value in DATA_UPLOAD_MAX_NUMBER_FIELDS (1000 by default)
        del actions["delete_selected"]
        return actions

    def delete_selected_bookmarks(self, request, queryset: QuerySet):
        bookmarks_count = queryset.count()
        for bookmark in queryset:
            bookmark.delete()
        self.message_user(
            request,
            ngettext(
                "%d bookmark was successfully deleted.",
                "%d bookmarks were successfully deleted.",
                bookmarks_count,
            )
            % bookmarks_count,
            messages.SUCCESS,
        )

    def archive_selected_bookmarks(self, request, queryset: QuerySet):
        for bookmark in queryset:
            archive_bookmark(bookmark)
        bookmarks_count = queryset.count()
        self.message_user(
            request,
            ngettext(
                "%d bookmark was successfully archived.",
                "%d bookmarks were successfully archived.",
                bookmarks_count,
            )
            % bookmarks_count,
            messages.SUCCESS,
        )

    def unarchive_selected_bookmarks(self, request, queryset: QuerySet):
        for bookmark in queryset:
            unarchive_bookmark(bookmark)
        bookmarks_count = queryset.count()
        self.message_user(
            request,
            ngettext(
                "%d bookmark was successfully unarchived.",
                "%d bookmarks were successfully unarchived.",
                bookmarks_count,
            )
            % bookmarks_count,
            messages.SUCCESS,
        )

    def mark_as_read(self, request, queryset: QuerySet):
        bookmarks_count = queryset.count()
        queryset.update(unread=False)
        self.message_user(
            request,
            ngettext(
                "%d bookmark marked as read.",
                "%d bookmarks marked as read.",
                bookmarks_count,
            )
            % bookmarks_count,
            messages.SUCCESS,
        )

    def mark_as_unread(self, request, queryset: QuerySet):
        bookmarks_count = queryset.count()
        queryset.update(unread=True)
        self.message_user(
            request,
            ngettext(
                "%d bookmark marked as unread.",
                "%d bookmarks marked as unread.",
                bookmarks_count,
            )
            % bookmarks_count,
            messages.SUCCESS,
        )


class AdminBookmarkAsset(admin.ModelAdmin):
    @admin.display(description="Display Name")
    def custom_display_name(self, obj):
        return str(obj)

    list_display = ("custom_display_name", "date_created", "status")
    search_fields = (
        "display_name",
        "file",
    )
    list_filter = ("status",)


class AdminTag(admin.ModelAdmin):
    list_display = ("name", "bookmarks_count", "owner", "date_added")
    search_fields = ("name", "owner__username")
    list_filter = ("owner__username",)
    ordering = ("-date_added",)
    actions = ["delete_unused_tags"]

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        queryset = queryset.annotate(bookmarks_count=Count("bookmark"))
        return queryset

    def bookmarks_count(self, obj):
        return obj.bookmarks_count

    bookmarks_count.admin_order_field = "bookmarks_count"

    def delete_unused_tags(self, request, queryset: QuerySet):
        unused_tags = queryset.filter(bookmark__isnull=True)
        unused_tags_count = unused_tags.count()
        for tag in unused_tags:
            tag.delete()

        if unused_tags_count > 0:
            self.message_user(
                request,
                ngettext(
                    "%d unused tag was successfully deleted.",
                    "%d unused tags were successfully deleted.",
                    unused_tags_count,
                )
                % unused_tags_count,
                messages.SUCCESS,
            )
        else:
            self.message_user(
                request,
                gettext(
                    "There were no unused tags in the selection",
                ),
                messages.SUCCESS,
            )


class AdminBookmarkBundle(admin.ModelAdmin):
    list_display = (
        "name",
        "owner",
        "order",
        "search",
        "any_tags",
        "all_tags",
        "excluded_tags",
        "filter_shared",
        "filter_unread",
        "date_created",
    )
    search_fields = ["name", "search", "any_tags", "all_tags", "excluded_tags"]
    list_filter = ("owner__username",)


class AdminUserProfileInline(admin.StackedInline):
    model = UserProfile
    can_delete = False
    verbose_name_plural = "Profile"
    fk_name = "user"
    readonly_fields = ("search_preferences",)


class AdminCustomUser(UserAdmin):
    inlines = (AdminUserProfileInline,)

    def get_inline_instances(self, request, obj=None):
        if not obj:
            return list()
        return super().get_inline_instances(request, obj)


class AdminToast(admin.ModelAdmin):
    list_display = ("key", "message", "owner", "acknowledged")
    search_fields = ("key", "message")
    list_filter = ("owner__username",)


class AdminFeedToken(admin.ModelAdmin):
    list_display = ("key", "user")
    search_fields = ["key"]
    list_filter = ("user__username",)


class ApiTokenAdminForm(forms.ModelForm):
    class Meta:
        model = ApiToken
        fields = ("name", "user")


class AdminApiToken(admin.ModelAdmin):
    form = ApiTokenAdminForm
    list_display = ("name", "user", "created")
    search_fields = ["name", "user__username"]
    list_filter = ("user__username",)
    ordering = ("-created",)


linkding_admin_site = LinkdingAdminSite()
linkding_admin_site.register(Bookmark, AdminBookmark)
linkding_admin_site.register(BookmarkAsset, AdminBookmarkAsset)
linkding_admin_site.register(Tag, AdminTag)
linkding_admin_site.register(BookmarkBundle, AdminBookmarkBundle)
linkding_admin_site.register(User, AdminCustomUser)
linkding_admin_site.register(ApiToken, AdminApiToken)
linkding_admin_site.register(Toast, AdminToast)
linkding_admin_site.register(FeedToken, AdminFeedToken)


================================================
FILE: bookmarks/api/__init__.py
================================================


================================================
FILE: bookmarks/api/auth.py
================================================
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication, get_authorization_header

from bookmarks.models import ApiToken


class LinkdingTokenAuthentication(TokenAuthentication):
    """
    Extends DRF TokenAuthentication to add support for multiple keywords and
    multiple tokens per user.
    """

    model = ApiToken
    keywords = [keyword.lower().encode() for keyword in ["Token", "Bearer"]]

    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() not in self.keywords:
            return None

        if len(auth) == 1:
            msg = _("Invalid token header. No credentials provided.")
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _("Invalid token header. Token string should not contain spaces.")
            raise exceptions.AuthenticationFailed(msg)

        try:
            token = auth[1].decode()
        except UnicodeError:
            msg = _(
                "Invalid token header. Token string should not contain invalid characters."
            )
            raise exceptions.AuthenticationFailed(msg) from None

        return self.authenticate_credentials(token)


================================================
FILE: bookmarks/api/routes.py
================================================
import gzip
import logging
import os

from django.conf import settings
from django.http import Http404, StreamingHttpResponse
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter, SimpleRouter

from bookmarks import queries
from bookmarks.api.serializers import (
    BookmarkAssetSerializer,
    BookmarkBundleSerializer,
    BookmarkSerializer,
    TagSerializer,
    UserProfileSerializer,
)
from bookmarks.models import (
    Bookmark,
    BookmarkAsset,
    BookmarkBundle,
    BookmarkSearch,
    Tag,
    User,
)
from bookmarks.services import assets, auto_tagging, bookmarks, bundles, website_loader
from bookmarks.type_defs import HttpRequest
from bookmarks.views import access

logger = logging.getLogger(__name__)


class BookmarkViewSet(
    viewsets.GenericViewSet,
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.CreateModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
):
    request: HttpRequest
    serializer_class = BookmarkSerializer

    def get_permissions(self):
        # Allow unauthenticated access to shared bookmarks.
        # The shared action should still filter bookmarks so that
        # unauthenticated users only see bookmarks from users that have public
        # sharing explicitly enabled
        if self.action == "shared":
            return [AllowAny()]

        # Otherwise use default permissions which should require authentication
        return super().get_permissions()

    def get_queryset(self):
        # Provide filtered queryset for list actions
        user = self.request.user
        search = BookmarkSearch.from_request(self.request, self.request.GET)
        if self.action == "list":
            return queries.query_bookmarks(user, user.profile, search)
        elif self.action == "archived":
            return queries.query_archived_bookmarks(user, user.profile, search)
        elif self.action == "shared":
            user = User.objects.filter(username=search.user).first()
            public_only = not self.request.user.is_authenticated
            return queries.query_shared_bookmarks(
                user, self.request.user_profile, search, public_only
            )

        # For single entity actions return user owned bookmarks
        return Bookmark.objects.all().filter(owner=user)

    def get_serializer_context(self):
        disable_scraping = "disable_scraping" in self.request.GET
        disable_html_snapshot = "disable_html_snapshot" in self.request.GET
        return {
            "request": self.request,
            "user": self.request.user,
            "disable_scraping": disable_scraping,
            "disable_html_snapshot": disable_html_snapshot,
        }

    @action(methods=["get"], detail=False)
    def archived(self, request: HttpRequest):
        return self.list(request)

    @action(methods=["get"], detail=False)
    def shared(self, request: HttpRequest):
        return self.list(request)

    @action(methods=["post"], detail=True)
    def archive(self, request: HttpRequest, pk):
        bookmark = self.get_object()
        bookmarks.archive_bookmark(bookmark)
        return Response(status=status.HTTP_204_NO_CONTENT)

    @action(methods=["post"], detail=True)
    def unarchive(self, request: HttpRequest, pk):
        bookmark = self.get_object()
        bookmarks.unarchive_bookmark(bookmark)
        return Response(status=status.HTTP_204_NO_CONTENT)

    @action(methods=["get"], detail=False)
    def check(self, request: HttpRequest):
        url = request.GET.get("url")
        ignore_cache = request.GET.get("ignore_cache", False) in ["true"]
        bookmark = Bookmark.query_existing(request.user, url).first()
        existing_bookmark_data = (
            self.get_serializer(bookmark).data if bookmark else None
        )

        metadata = website_loader.load_website_metadata(url, ignore_cache=ignore_cache)

        # Return tags that would be automatically applied to the bookmark
        profile = request.user.profile
        auto_tags = []
        if profile.auto_tagging_rules:
            try:
                auto_tags = auto_tagging.get_tags(profile.auto_tagging_rules, url)
            except Exception as e:
                logger.error(
                    f"Failed to auto-tag bookmark. url={url}",
                    exc_info=e,
                )

        return Response(
            {
                "bookmark": existing_bookmark_data,
                "metadata": metadata.to_dict(),
                "auto_tags": auto_tags,
            },
            status=status.HTTP_200_OK,
        )

    @action(methods=["post"], detail=False)
    def singlefile(self, request: HttpRequest):
        if settings.LD_DISABLE_ASSET_UPLOAD:
            return Response(
                {"error": "Asset upload is disabled."},
                status=status.HTTP_403_FORBIDDEN,
            )
        url = request.POST.get("url")
        file = request.FILES.get("file")

        if not url or not file:
            return Response(
                {"error": "Both 'url' and 'file' parameters are required."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        bookmark = Bookmark.query_existing(request.user, url).first()

        if not bookmark:
            bookmark = Bookmark(url=url)
            bookmark = bookmarks.create_bookmark(
                bookmark, "", request.user, disable_html_snapshot=True
            )
            bookmarks.enhance_with_website_metadata(bookmark)

        assets.upload_snapshot(bookmark, file.read())

        return Response(
            {"message": "Snapshot uploaded successfully."},
            status=status.HTTP_201_CREATED,
        )


class BookmarkAssetViewSet(
    viewsets.GenericViewSet,
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
):
    request: HttpRequest
    serializer_class = BookmarkAssetSerializer

    def get_queryset(self):
        user = self.request.user
        # limit access to assets to the owner of the bookmark for now
        bookmark = access.bookmark_write(self.request, self.kwargs["bookmark_id"])
        return BookmarkAsset.objects.filter(
            bookmark_id=bookmark.id, bookmark__owner=user
        )

    def get_serializer_context(self):
        return {"user": self.request.user}

    @action(detail=True, methods=["get"], url_path="download")
    def download(self, request: HttpRequest, bookmark_id, pk):
        asset = self.get_object()
        try:
            file_path = os.path.join(settings.LD_ASSET_FOLDER, asset.file)
            content_type = asset.content_type
            file_stream = (
                gzip.GzipFile(file_path, mode="rb")
                if asset.gzip
                else open(file_path, "rb")  # noqa: SIM115
            )
            response = StreamingHttpResponse(file_stream, content_type=content_type)
            response["Content-Disposition"] = (
                f'attachment; filename="{asset.download_name}"'
            )
            return response
        except FileNotFoundError:
            raise Http404("Asset file does not exist") from None
        except Exception as e:
            logger.error(
                f"Failed to download asset. bookmark_id={bookmark_id}, asset_id={pk}",
                exc_info=e,
            )
            return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    @action(methods=["post"], detail=False)
    def upload(self, request: HttpRequest, bookmark_id):
        if settings.LD_DISABLE_ASSET_UPLOAD:
            return Response(
                {"error": "Asset upload is disabled."},
                status=status.HTTP_403_FORBIDDEN,
            )
        bookmark = access.bookmark_write(request, bookmark_id)

        upload_file = request.FILES.get("file")
        if not upload_file:
            return Response(
                {"error": "No file provided."}, status=status.HTTP_400_BAD_REQUEST
            )

        try:
            asset = assets.upload_asset(bookmark, upload_file)
            serializer = self.get_serializer(asset)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        except Exception as e:
            logger.error(
                f"Failed to upload asset file. bookmark_id={bookmark_id}, file={upload_file.name}",
                exc_info=e,
            )
            return Response(
                {"error": "Failed to upload asset."},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
            )

    def perform_destroy(self, instance):
        assets.remove_asset(instance)


class TagViewSet(
    viewsets.GenericViewSet,
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.CreateModelMixin,
):
    request: HttpRequest
    serializer_class = TagSerializer

    def get_queryset(self):
        user = self.request.user
        return Tag.objects.all().filter(owner=user)

    def get_serializer_context(self):
        return {"user": self.request.user}


class UserViewSet(viewsets.GenericViewSet):
    @action(methods=["get"], detail=False)
    def profile(self, request: HttpRequest):
        return Response(UserProfileSerializer(request.user.profile).data)


class BookmarkBundleViewSet(
    viewsets.GenericViewSet,
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.CreateModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
):
    request: HttpRequest
    serializer_class = BookmarkBundleSerializer

    def get_queryset(self):
        user = self.request.user
        return BookmarkBundle.objects.filter(owner=user).order_by("order")

    def get_serializer_context(self):
        return {"user": self.request.user}

    def perform_destroy(self, instance):
        bundles.delete_bundle(instance)


# DRF routers do not support nested view sets such as /bookmarks/<id>/assets/<id>/
# Instead create separate routers for each view set and manually register them in urls.py
# The default router is only used to allow reversing a URL for the API root
default_router = DefaultRouter()

bookmark_router = SimpleRouter()
bookmark_router.register("", BookmarkViewSet, basename="bookmark")

tag_router = SimpleRouter()
tag_router.register("", TagViewSet, basename="tag")

user_router = SimpleRouter()
user_router.register("", UserViewSet, basename="user")

bundle_router = SimpleRouter()
bundle_router.register("", BookmarkBundleViewSet, basename="bundle")

bookmark_asset_router = SimpleRouter()
bookmark_asset_router.register("", BookmarkAssetViewSet, basename="bookmark_asset")


================================================
FILE: bookmarks/api/serializers.py
================================================
from django.db.models import prefetch_related_objects
from django.templatetags.static import static
from rest_framework import serializers
from rest_framework.serializers import ListSerializer

from bookmarks.models import (
    Bookmark,
    BookmarkAsset,
    BookmarkBundle,
    Tag,
    UserProfile,
    build_tag_string,
)
from bookmarks.services import bookmarks, bundles
from bookmarks.services.tags import get_or_create_tag
from bookmarks.services.wayback import generate_fallback_webarchive_url
from bookmarks.utils import app_version


class TagListField(serializers.ListField):
    child = serializers.CharField()


class BookmarkListSerializer(ListSerializer):
    def to_representation(self, data):
        # Prefetch nested relations to avoid n+1 queries
        prefetch_related_objects(data, "tags")

        return super().to_representation(data)


class EmtpyField(serializers.ReadOnlyField):
    def to_representation(self, value):
        return None


class BookmarkBundleSerializer(serializers.ModelSerializer):
    class Meta:
        model = BookmarkBundle
        fields = [
            "id",
            "name",
            "search",
            "any_tags",
            "all_tags",
            "excluded_tags",
            "filter_unread",
            "filter_shared",
            "order",
            "date_created",
            "date_modified",
        ]
        read_only_fields = [
            "id",
            "date_created",
            "date_modified",
        ]

    def create(self, validated_data):
        bundle = BookmarkBundle(**validated_data)
        bundle.order = validated_data.get("order", None)
        return bundles.create_bundle(bundle, self.context["user"])


class BookmarkSerializer(serializers.ModelSerializer):
    class Meta:
        model = Bookmark
        fields = [
            "id",
            "url",
            "title",
            "description",
            "notes",
            "web_archive_snapshot_url",
            "favicon_url",
            "preview_image_url",
            "is_archived",
            "unread",
            "shared",
            "tag_names",
            "date_added",
            "date_modified",
            "website_title",
            "website_description",
        ]
        read_only_fields = [
            "web_archive_snapshot_url",
            "favicon_url",
            "preview_image_url",
            "tag_names",
            "website_title",
            "website_description",
        ]
        list_serializer_class = BookmarkListSerializer

    # Custom tag_names field to allow passing a list of tag names to create/update
    tag_names = TagListField(required=False)
    # Custom fields to generate URLs for favicon, preview image, and web archive snapshot
    favicon_url = serializers.SerializerMethodField()
    preview_image_url = serializers.SerializerMethodField()
    web_archive_snapshot_url = serializers.SerializerMethodField()
    # Add dummy website title and description fields for backwards compatibility but keep them empty
    website_title = EmtpyField()
    website_description = EmtpyField()
    # these are optional
    date_added = serializers.DateTimeField(required=False)
    date_modified = serializers.DateTimeField(required=False)

    def get_favicon_url(self, obj: Bookmark):
        if not obj.favicon_file:
            return None
        request = self.context.get("request")
        favicon_file_path = static(obj.favicon_file)
        favicon_url = request.build_absolute_uri(favicon_file_path)
        return favicon_url

    def get_preview_image_url(self, obj: Bookmark):
        if not obj.preview_image_file:
            return None
        request = self.context.get("request")
        preview_image_file_path = static(obj.preview_image_file)
        preview_image_url = request.build_absolute_uri(preview_image_file_path)
        return preview_image_url

    def get_web_archive_snapshot_url(self, obj: Bookmark):
        if obj.web_archive_snapshot_url:
            return obj.web_archive_snapshot_url

        return generate_fallback_webarchive_url(obj.url, obj.date_added)

    def create(self, validated_data):
        tag_names = validated_data.pop("tag_names", [])
        tag_string = build_tag_string(tag_names)
        bookmark = Bookmark(**validated_data)

        disable_scraping = self.context.get("disable_scraping", False)
        disable_html_snapshot = self.context.get("disable_html_snapshot", False)

        saved_bookmark = bookmarks.create_bookmark(
            bookmark,
            tag_string,
            self.context["user"],
            disable_html_snapshot=disable_html_snapshot,
        )
        # Unless scraping is explicitly disabled, enhance bookmark with website
        # metadata to preserve backwards compatibility with clients that expect
        # title and description to be populated automatically when left empty
        if not disable_scraping:
            bookmarks.enhance_with_website_metadata(saved_bookmark)
        return saved_bookmark

    def update(self, instance: Bookmark, validated_data):
        tag_names = validated_data.pop("tag_names", instance.tag_names)
        tag_string = build_tag_string(tag_names)

        for field_name, field in self.fields.items():
            if not field.read_only and field_name in validated_data:
                setattr(instance, field_name, validated_data[field_name])

        return bookmarks.update_bookmark(instance, tag_string, self.context["user"])

    def validate(self, attrs):
        # When creating a bookmark, the service logic prevents duplicate URLs by
        # updating the existing bookmark instead. When editing a bookmark,
        # there is no assumption that it would update a different bookmark if
        # the URL is a duplicate, so raise a validation error in that case.
        if self.instance and "url" in attrs:
            is_duplicate = (
                Bookmark.objects.filter(owner=self.instance.owner, url=attrs["url"])
                .exclude(pk=self.instance.pk)
                .exists()
            )
            if is_duplicate:
                raise serializers.ValidationError(
                    {"url": "A bookmark with this URL already exists."}
                )

        return attrs


class BookmarkAssetSerializer(serializers.ModelSerializer):
    class Meta:
        model = BookmarkAsset
        fields = [
            "id",
            "bookmark",
            "date_created",
            "file_size",
            "asset_type",
            "content_type",
            "display_name",
            "status",
        ]


class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ["id", "name", "date_added"]
        read_only_fields = ["date_added"]

    def create(self, validated_data):
        return get_or_create_tag(validated_data["name"], self.context["user"])


class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        fields = [
            "theme",
            "bookmark_date_display",
            "bookmark_link_target",
            "web_archive_integration",
            "tag_search",
            "enable_sharing",
            "enable_public_sharing",
            "enable_favicons",
            "display_url",
            "permanent_notes",
            "search_preferences",
            "version",
        ]

    version = serializers.ReadOnlyField(default=app_version)


================================================
FILE: bookmarks/apps.py
================================================
from django.apps import AppConfig


class BookmarksConfig(AppConfig):
    name = "bookmarks"

    def ready(self):
        # Register signal handlers
        # noinspection PyUnusedImports
        import bookmarks.signals  # noqa: F401


================================================
FILE: bookmarks/context_processors.py
================================================
from bookmarks import utils
from bookmarks.models import Toast


def toasts(request):
    user = request.user
    toast_messages = (
        Toast.objects.filter(owner=user, acknowledged=False)
        if user.is_authenticated
        else []
    )
    has_toasts = len(toast_messages) > 0

    return {
        "has_toasts": has_toasts,
        "toast_messages": toast_messages,
    }


def app_version(request):
    return {"app_version": utils.app_version}


================================================
FILE: bookmarks/feeds.py
================================================
import unicodedata
from dataclasses import dataclass

from django.contrib.syndication.views import Feed
from django.db.models import QuerySet, prefetch_related_objects
from django.http import HttpRequest
from django.urls import reverse

from bookmarks import queries
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken, UserProfile
from bookmarks.views import access


@dataclass
class FeedContext:
    request: HttpRequest
    feed_token: FeedToken | None
    query_set: QuerySet[Bookmark]


def sanitize(text: str):
    if not text:
        return ""
    # remove control characters
    valid_chars = ["\n", "\r", "\t"]
    return "".join(
        ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != "C"
    )


class BaseBookmarksFeed(Feed):
    def get_object(self, request, feed_key: str | None):
        feed_token = FeedToken.objects.get(key__exact=feed_key) if feed_key else None
        bundle = None
        bundle_id = request.GET.get("bundle")
        if bundle_id:
            bundle = access.bundle_read(request, bundle_id)

        search = BookmarkSearch(
            q=request.GET.get("q", ""),
            unread=request.GET.get("unread", ""),
            shared=request.GET.get("shared", ""),
            bundle=bundle,
        )
        query_set = self.get_query_set(feed_token, search)
        return FeedContext(request, feed_token, query_set)

    def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
        raise NotImplementedError

    def items(self, context: FeedContext):
        limit = context.request.GET.get("limit", 100)
        data = context.query_set[: int(limit)] if limit else list(context.query_set)
        prefetch_related_objects(data, "tags")
        return data

    def item_title(self, item: Bookmark):
        return sanitize(item.resolved_title)

    def item_description(self, item: Bookmark):
        return sanitize(item.resolved_description)

    def item_link(self, item: Bookmark):
        return item.url

    def item_pubdate(self, item: Bookmark):
        return item.date_added

    def item_categories(self, item: Bookmark):
        return item.tag_names


class AllBookmarksFeed(BaseBookmarksFeed):
    title = "All bookmarks"
    description = "All bookmarks"

    def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
        return queries.query_bookmarks(feed_token.user, feed_token.user.profile, search)

    def link(self, context: FeedContext):
        return reverse("linkding:feeds.all", args=[context.feed_token.key])


class UnreadBookmarksFeed(BaseBookmarksFeed):
    title = "Unread bookmarks"
    description = "All unread bookmarks"

    def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
        return queries.query_bookmarks(
            feed_token.user, feed_token.user.profile, search
        ).filter(unread=True)

    def link(self, context: FeedContext):
        return reverse("linkding:feeds.unread", args=[context.feed_token.key])


class SharedBookmarksFeed(BaseBookmarksFeed):
    title = "Shared bookmarks"
    description = "All shared bookmarks"

    def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
        return queries.query_shared_bookmarks(
            None, feed_token.user.profile, search, False
        )

    def link(self, context: FeedContext):
        return reverse("linkding:feeds.shared", args=[context.feed_token.key])


class PublicSharedBookmarksFeed(BaseBookmarksFeed):
    title = "Public shared bookmarks"
    description = "All public shared bookmarks"

    def get_object(self, request):
        return super().get_object(request, None)

    def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
        return queries.query_shared_bookmarks(None, UserProfile(), search, True)

    def link(self, context: FeedContext):
        return reverse("linkding:feeds.public_shared")


================================================
FILE: bookmarks/forms.py
================================================
from django import forms
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone

from bookmarks.models import (
    Bookmark,
    BookmarkBundle,
    BookmarkSearch,
    GlobalSettings,
    Tag,
    UserProfile,
    build_tag_string,
    parse_tag_string,
    sanitize_tag_name,
)
from bookmarks.services.bookmarks import create_bookmark, update_bookmark
from bookmarks.type_defs import HttpRequest
from bookmarks.validators import BookmarkURLValidator
from bookmarks.widgets import (
    FormCheckbox,
    FormErrorList,
    FormInput,
    FormNumberInput,
    FormSelect,
    FormTextarea,
    TagAutocomplete,
)


class BookmarkForm(forms.ModelForm):
    # Use URLField for URL
    url = forms.CharField(validators=[BookmarkURLValidator()], widget=FormInput)
    tag_string = forms.CharField(required=False, widget=TagAutocomplete)
    # Do not require title and description as they may be empty
    title = forms.CharField(max_length=512, required=False, widget=FormInput)
    description = forms.CharField(required=False, widget=FormTextarea)
    notes = forms.CharField(required=False, widget=FormTextarea)
    unread = forms.BooleanField(required=False, widget=FormCheckbox)
    shared = forms.BooleanField(required=False, widget=FormCheckbox)
    # Hidden field that determines whether to close window/tab after saving the bookmark
    auto_close = forms.CharField(required=False, widget=forms.HiddenInput)

    class Meta:
        model = Bookmark
        fields = [
            "url",
            "tag_string",
            "title",
            "description",
            "notes",
            "unread",
            "shared",
            "auto_close",
        ]

    def __init__(self, request: HttpRequest, instance: Bookmark = None):
        self.request = request

        initial = None
        if instance is None and request.method == "GET":
            initial = {
                "url": request.GET.get("url"),
                "title": request.GET.get("title"),
                "description": request.GET.get("description"),
                "notes": request.GET.get("notes"),
                "tag_string": request.GET.get("tags"),
                "auto_close": "auto_close" in request.GET,
                "unread": request.user_profile.default_mark_unread,
                "shared": request.user_profile.default_mark_shared,
            }
        if instance is not None and request.method == "GET":
            initial = {"tag_string": build_tag_string(instance.tag_names, " ")}
        data = request.POST if request.method == "POST" else None
        super().__init__(
            data, instance=instance, initial=initial, error_class=FormErrorList
        )

    @property
    def is_auto_close(self):
        return self.data.get("auto_close", False) == "True" or self.initial.get(
            "auto_close", False
        )

    @property
    def has_notes(self):
        return self.initial.get("notes", None) or (
            self.instance and self.instance.notes
        )

    def save(self, commit=False):
        tag_string = convert_tag_string(self.data["tag_string"])
        bookmark = super().save(commit=False)
        if self.instance.pk:
            return update_bookmark(bookmark, tag_string, self.request.user)
        else:
            return create_bookmark(bookmark, tag_string, self.request.user)

    def clean_url(self):
        # When creating a bookmark, the service logic prevents duplicate URLs by
        # updating the existing bookmark instead, which is also communicated in
        # the form's UI. When editing a bookmark, there is no assumption that
        # it would update a different bookmark if the URL is a duplicate, so
        # raise a validation error in that case.
        url = self.cleaned_data["url"]
        if self.instance.pk:
            is_duplicate = (
                Bookmark.query_existing(self.instance.owner, url)
                .exclude(pk=self.instance.pk)
                .exists()
            )
            if is_duplicate:
                raise forms.ValidationError("A bookmark with this URL already exists.")

        return url


def convert_tag_string(tag_string: str):
    # Tag strings coming from inputs are space-separated, however services.bookmarks functions expect comma-separated
    # strings
    return tag_string.replace(" ", ",")


class TagForm(forms.ModelForm):
    name = forms.CharField(widget=FormInput)

    class Meta:
        model = Tag
        fields = ["name"]

    def __init__(self, user, *args, **kwargs):
        super().__init__(*args, **kwargs, error_class=FormErrorList)
        self.user = user

    def clean_name(self):
        name = self.cleaned_data.get("name", "").strip()

        name = sanitize_tag_name(name)

        queryset = Tag.objects.filter(name__iexact=name, owner=self.user)
        if self.instance.pk:
            queryset = queryset.exclude(pk=self.instance.pk)

        if queryset.exists():
            raise forms.ValidationError(f'Tag "{name}" already exists.')

        return name

    def save(self, commit=True):
        tag = super().save(commit=False)
        if not self.instance.pk:
            tag.owner = self.user
            tag.date_added = timezone.now()
        else:
            tag.date_modified = timezone.now()
        if commit:
            tag.save()
        return tag


class TagMergeForm(forms.Form):
    target_tag = forms.CharField(widget=TagAutocomplete)
    merge_tags = forms.CharField(widget=TagAutocomplete)

    def __init__(self, user, *args, **kwargs):
        super().__init__(*args, **kwargs, error_class=FormErrorList)
        self.user = user

    def clean_target_tag(self):
        target_tag_name = self.cleaned_data.get("target_tag", "")

        target_tag_names = parse_tag_string(target_tag_name, " ")
        if len(target_tag_names) != 1:
            raise forms.ValidationError(
                "Please enter only one tag name for the target tag."
            )

        target_tag_name = target_tag_names[0]

        try:
            target_tag = Tag.objects.get(name__iexact=target_tag_name, owner=self.user)
        except Tag.DoesNotExist:
            raise forms.ValidationError(
                f'Tag "{target_tag_name}" does not exist.'
            ) from None

        return target_tag

    def clean_merge_tags(self):
        merge_tags_string = self.cleaned_data.get("merge_tags", "")

        merge_tag_names = parse_tag_string(merge_tags_string, " ")
        if not merge_tag_names:
            raise forms.ValidationError("Please enter at least one tag to merge.")

        merge_tags = []
        for tag_name in merge_tag_names:
            try:
                tag = Tag.objects.get(name__iexact=tag_name, owner=self.user)
                merge_tags.append(tag)
            except Tag.DoesNotExist:
                raise forms.ValidationError(
                    f'Tag "{tag_name}" does not exist.'
                ) from None

        target_tag = self.cleaned_data.get("target_tag")
        if target_tag and target_tag in merge_tags:
            raise forms.ValidationError(
                "The target tag cannot be selected for merging."
            )

        return merge_tags


class BookmarkBundleForm(forms.ModelForm):
    name = forms.CharField(max_length=256, widget=FormInput)
    search = forms.CharField(max_length=256, required=False, widget=FormInput)
    any_tags = forms.CharField(required=False, widget=TagAutocomplete)
    all_tags = forms.CharField(required=False, widget=TagAutocomplete)
    excluded_tags = forms.CharField(required=False, widget=TagAutocomplete)
    filter_unread = forms.ChoiceField(
        choices=BookmarkBundle.FILTER_UNREAD_CHOICES,
        required=False,
        widget=FormSelect,
    )
    filter_shared = forms.ChoiceField(
        choices=BookmarkBundle.FILTER_SHARED_CHOICES,
        required=False,
        widget=FormSelect,
    )

    class Meta:
        model = BookmarkBundle
        fields = [
            "name",
            "search",
            "any_tags",
            "all_tags",
            "excluded_tags",
            "filter_unread",
            "filter_shared",
        ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs, error_class=FormErrorList)


class BookmarkSearchForm(forms.Form):
    SORT_CHOICES = [
        (BookmarkSearch.SORT_ADDED_ASC, "Added ↑"),
        (BookmarkSearch.SORT_ADDED_DESC, "Added ↓"),
        (BookmarkSearch.SORT_TITLE_ASC, "Title ↑"),
        (BookmarkSearch.SORT_TITLE_DESC, "Title ↓"),
    ]
    FILTER_SHARED_CHOICES = [
        (BookmarkSearch.FILTER_SHARED_OFF, "Off"),
        (BookmarkSearch.FILTER_SHARED_SHARED, "Shared"),
        (BookmarkSearch.FILTER_SHARED_UNSHARED, "Unshared"),
    ]
    FILTER_UNREAD_CHOICES = [
        (BookmarkSearch.FILTER_UNREAD_OFF, "Off"),
        (BookmarkSearch.FILTER_UNREAD_YES, "Unread"),
        (BookmarkSearch.FILTER_UNREAD_NO, "Read"),
    ]

    q = forms.CharField()
    user = forms.ChoiceField(required=False, widget=FormSelect)
    bundle = forms.CharField(required=False)
    sort = forms.ChoiceField(choices=SORT_CHOICES, widget=FormSelect)
    shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect)
    unread = forms.ChoiceField(choices=FILTER_UNREAD_CHOICES, widget=forms.RadioSelect)
    modified_since = forms.CharField(required=False)
    added_since = forms.CharField(required=False)

    def __init__(
        self,
        search: BookmarkSearch,
        editable_fields: list[str] = None,
        users: list[User] = None,
    ):
        super().__init__()
        editable_fields = editable_fields or []
        self.editable_fields = editable_fields

        # set choices for user field if users are provided
        if users:
            user_choices = [(user.username, user.username) for user in users]
            user_choices.insert(0, ("", "Everyone"))
            self.fields["user"].choices = user_choices

        for param in search.params:
            # set initial values for modified params
            value = search.__dict__.get(param)
            if isinstance(value, models.Model):
                self.fields[param].initial = value.id
            else:
                self.fields[param].initial = value

            # Mark non-editable modified fields as hidden. That way, templates
            # rendering a form can just loop over hidden_fields to ensure that
            # all necessary search options are kept when submitting the form.
            if search.is_modified(param) and param not in editable_fields:
                self.fields[param].widget = forms.HiddenInput()


class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = [
            "theme",
            "bookmark_date_display",
            "bookmark_description_display",
            "bookmark_description_max_lines",
            "bookmark_link_target",
            "web_archive_integration",
            "tag_search",
            "tag_grouping",
            "enable_sharing",
            "enable_public_sharing",
            "enable_favicons",
            "enable_preview_images",
            "enable_automatic_html_snapshots",
            "display_url",
            "display_view_bookmark_action",
            "display_edit_bookmark_action",
            "display_archive_bookmark_action",
            "display_remove_bookmark_action",
            "permanent_notes",
            "default_mark_unread",
            "default_mark_shared",
            "custom_css",
            "auto_tagging_rules",
            "items_per_page",
            "sticky_pagination",
            "collapse_side_panel",
            "hide_bundles",
            "legacy_search",
        ]
        widgets = {
            "theme": FormSelect,
            "bookmark_date_display": FormSelect,
            "bookmark_description_display": FormSelect,
            "bookmark_description_max_lines": FormNumberInput,
            "bookmark_link_target": FormSelect,
            "web_archive_integration": FormSelect,
            "tag_search": FormSelect,
            "tag_grouping": FormSelect,
            "auto_tagging_rules": FormTextarea,
            "custom_css": FormTextarea,
            "items_per_page": FormNumberInput,
            "display_url": FormCheckbox,
            "permanent_notes": FormCheckbox,
            "display_view_bookmark_action": FormCheckbox,
            "display_edit_bookmark_action": FormCheckbox,
            "display_archive_bookmark_action": FormCheckbox,
            "display_remove_bookmark_action": FormCheckbox,
            "sticky_pagination": FormCheckbox,
            "collapse_side_panel": FormCheckbox,
            "hide_bundles": FormCheckbox,
            "legacy_search": FormCheckbox,
            "enable_favicons": FormCheckbox,
            "enable_preview_images": FormCheckbox,
            "enable_sharing": FormCheckbox,
            "enable_public_sharing": FormCheckbox,
            "enable_automatic_html_snapshots": FormCheckbox,
            "default_mark_unread": FormCheckbox,
            "default_mark_shared": FormCheckbox,
        }


class GlobalSettingsForm(forms.ModelForm):
    class Meta:
        model = GlobalSettings
        fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"]
        widgets = {
            "landing_page": FormSelect,
            "guest_profile_user": FormSelect,
            "enable_link_prefetch": FormCheckbox,
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["guest_profile_user"].empty_label = "Standard profile"


================================================
FILE: bookmarks/frontend/api.js
================================================
export class Api {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  listBookmarks(search, options = { limit: 100, offset: 0, path: "" }) {
    const query = [`limit=${options.limit}`, `offset=${options.offset}`];
    Object.keys(search).forEach((key) => {
      const value = search[key];
      if (value) {
        query.push(`${key}=${encodeURIComponent(value)}`);
      }
    });
    const queryString = query.join("&");
    const url = `${this.baseUrl}bookmarks${options.path}/?${queryString}`;

    return fetch(url)
      .then((response) => response.json())
      .then((data) => data.results);
  }

  getTags(options = { limit: 100, offset: 0 }) {
    const url = `${this.baseUrl}tags/?limit=${options.limit}&offset=${options.offset}`;

    return fetch(url)
      .then((response) => response.json())
      .then((data) => data.results);
  }
}

const apiBaseUrl = document.documentElement.dataset.apiBaseUrl || "";
export const api = new Api(apiBaseUrl);


================================================
FILE: bookmarks/frontend/components/bookmark-page.js
================================================
import { HeadlessElement } from "../utils/element.js";

class BookmarkPage extends HeadlessElement {
  init() {
    this.update = this.update.bind(this);
    this.onToggleNotes = this.onToggleNotes.bind(this);
    this.onToggleBulkEdit = this.onToggleBulkEdit.bind(this);
    this.onBulkActionChange = this.onBulkActionChange.bind(this);
    this.onToggleAll = this.onToggleAll.bind(this);
    this.onToggleBookmark = this.onToggleBookmark.bind(this);

    this.oldItems = [];
    this.update();
    document.addEventListener("bookmark-list-updated", this.update);
  }

  disconnectedCallback() {
    document.removeEventListener("bookmark-list-updated", this.update);
  }

  update() {
    const items = this.querySelectorAll("ul.bookmark-list > li");
    this.updateTooltips(items);
    this.updateNotesToggles(items, this.oldItems);
    this.updateBulkEdit(items, this.oldItems);
    this.oldItems = items;
  }

  updateTooltips(items) {
    // Add tooltip to title if it is truncated
    items.forEach((item) => {
      const titleAnchor = item.querySelector(".title > a");
      const titleSpan = titleAnchor.querySelector("span");
      if (titleSpan.offsetWidth > titleAnchor.offsetWidth) {
        titleAnchor.dataset.tooltip = titleSpan.textContent;
      } else {
        delete titleAnchor.dataset.tooltip;
      }
    });
  }

  updateNotesToggles(items, oldItems) {
    oldItems.forEach((oldItem) => {
      const oldToggle = oldItem.querySelector(".toggle-notes");
      if (oldToggle) {
        oldToggle.removeEventListener("click", this.onToggleNotes);
      }
    });

    items.forEach((item) => {
      const notesToggle = item.querySelector(".toggle-notes");
      if (notesToggle) {
        notesToggle.addEventListener("click", this.onToggleNotes);
      }
    });
  }

  onToggleNotes(event) {
    event.preventDefault();
    event.stopPropagation();
    event.target.closest("li").classList.toggle("show-notes");
  }

  updateBulkEdit() {
    if (this.hasAttribute("no-bulk-edit")) {
      return;
    }

    // Remove existing listeners
    this.activeToggle?.removeEventListener("click", this.onToggleBulkEdit);
    this.actionSelect?.removeEventListener("change", this.onBulkActionChange);
    this.allCheckbox?.removeEventListener("change", this.onToggleAll);
    this.bookmarkCheckboxes?.forEach((checkbox) => {
      checkbox.removeEventListener("change", this.onToggleBookmark);
    });

    // Re-query elements
    this.activeToggle = this.querySelector(".bulk-edit-active-toggle");
    this.actionSelect = this.querySelector("select[name='bulk_action']");
    this.allCheckbox = this.querySelector(".bulk-edit-checkbox.all input");
    this.bookmarkCheckboxes = Array.from(
      this.querySelectorAll(".bulk-edit-checkbox:not(.all) input"),
    );
    this.selectAcross = this.querySelector("label.select-across");
    this.executeButton = this.querySelector("button[name='bulk_execute']");

    // Add listeners
    this.activeToggle.addEventListener("click", this.onToggleBulkEdit);
    this.actionSelect.addEventListener("change", this.onBulkActionChange);
    this.allCheckbox.addEventListener("change", this.onToggleAll);
    this.bookmarkCheckboxes.forEach((checkbox) => {
      checkbox.addEventListener("change", this.onToggleBookmark);
    });

    // Reset checkbox states
    this.allCheckbox.checked = false;
    this.bookmarkCheckboxes.forEach((checkbox) => {
      checkbox.checked = false;
    });
    this.updateSelectAcross(false);
    this.updateExecuteButton();

    // Update total number of bookmarks
    const totalHolder = this.querySelector("[data-bookmarks-total]");
    const total = totalHolder?.dataset.bookmarksTotal || 0;
    const totalSpan = this.selectAcross.querySelector("span.total");
    totalSpan.textContent = total;
  }

  onToggleBulkEdit() {
    this.classList.toggle("active");
  }

  onBulkActionChange() {
    this.dataset.bulkAction = this.actionSelect.value;
  }

  onToggleAll() {
    const allChecked = this.allCheckbox.checked;
    this.bookmarkCheckboxes.forEach((checkbox) => {
      checkbox.checked = allChecked;
    });
    this.updateSelectAcross(allChecked);
    this.updateExecuteButton();
  }

  onToggleBookmark() {
    const allChecked = this.bookmarkCheckboxes.every((checkbox) => {
      return checkbox.checked;
    });
    this.allCheckbox.checked = allChecked;
    this.updateSelectAcross(allChecked);
    this.updateExecuteButton();
  }

  updateSelectAcross(allChecked) {
    if (allChecked) {
      this.selectAcross.classList.remove("d-none");
    } else {
      this.selectAcross.classList.add("d-none");
      this.selectAcross.querySelector("input").checked = false;
    }
  }

  updateExecuteButton() {
    const anyChecked = this.bookmarkCheckboxes.some((checkbox) => {
      return checkbox.checked;
    });
    this.executeButton.disabled = !anyChecked;
  }
}

customElements.define("ld-bookmark-page", BookmarkPage);


================================================
FILE: bookmarks/frontend/components/clear-button.js
================================================
import { HeadlessElement } from "../utils/element";

class ClearButton extends HeadlessElement {
  init() {
    this.field = document.getElementById(this.dataset.for);
    if (!this.field) {
      console.error(`Field with ID ${this.dataset.for} not found`);
      return;
    }
    this.update = this.update.bind(this);
    this.clear = this.clear.bind(this);

    this.addEventListener("click", this.clear);
    this.field.addEventListener("input", this.update);
    this.field.addEventListener("value-changed", this.update);
    this.update();
  }

  update() {
    this.style.display = this.field.value ? "inline" : "none";
  }

  clear() {
    this.field.value = "";
    this.field.focus();
    this.update();
  }
}

customElements.define("ld-clear-button", ClearButton);


================================================
FILE: bookmarks/frontend/components/confirm-dropdown.js
================================================
import { html, LitElement } from "lit";
import { FocusTrapController, isKeyboardActive } from "../utils/focus.js";
import { PositionController } from "../utils/position-controller.js";

let confirmId = 0;

function nextConfirmId() {
  return `confirm-${confirmId++}`;
}

function removeAll() {
  document
    .querySelectorAll("ld-confirm-dropdown")
    .forEach((dropdown) => dropdown.close());
}

// Create a confirm dropdown whenever a button with the data-confirm attribute is clicked
document.addEventListener("click", (event) => {
  // Check if the clicked element is a button with data-confirm
  const button = event.target.closest("button[data-confirm]");
  if (!button) return;

  // Remove any existing confirm dropdowns
  removeAll();

  // Show confirmation dropdown
  event.preventDefault();

  const dropdown = document.createElement("ld-confirm-dropdown");
  dropdown.button = button;
  document.body.appendChild(dropdown);
});

// Remove all confirm dropdowns when:
// - Turbo caches the page
// - The escape key is pressed
document.addEventListener("turbo:before-cache", removeAll);
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape") {
    removeAll();
  }
});

class ConfirmDropdown extends LitElement {
  constructor() {
    super();
    this.confirmId = nextConfirmId();
  }

  createRenderRoot() {
    return this;
  }

  firstUpdated(props) {
    super.firstUpdated(props);
    this.classList.add("dropdown", "confirm-dropdown", "active");

    const menu = this.querySelector(".menu");
    this.positionController = new PositionController({
      anchor: this.button,
      overlay: menu,
      arrow: this.querySelector(".menu-arrow"),
      offset: 12,
    });
    this.positionController.enable();
    this.focusTrap = new FocusTrapController(menu);
  }

  render() {
    const questionText = this.button.dataset.confirmQuestion || "Are you sure?";
    return html`
      <div
        class="menu with-arrow"
        role="alertdialog"
        aria-modal="true"
        aria-labelledby=${this.confirmId}
      >
        <span id=${this.confirmId} style="font-weight: bold;">
          ${questionText}
        </span>
        <button type="button" class="btn" @click=${this.close}>Cancel</button>
        <button type="submit" class="btn btn-error" @click=${this.confirm}>
          Confirm
        </button>
        <div class="menu-arrow"></div>
      </div>
    `;
  }

  confirm() {
    this.button.closest("form").requestSubmit(this.button);
    this.close();
  }

  close() {
    this.positionController.disable();
    this.focusTrap.destroy();
    this.remove();
    this.button.focus({ focusVisible: isKeyboardActive() });
  }
}

customElements.define("ld-confirm-dropdown", ConfirmDropdown);


================================================
FILE: bookmarks/frontend/components/details-modal.js
================================================
import { setAfterPageLoadFocusTarget } from "../utils/focus.js";
import { Modal } from "./modal.js";

class DetailsModal extends Modal {
  doClose() {
    super.doClose();

    // Try restore focus to view details to view details link of respective bookmark
    const bookmarkId = this.dataset.bookmarkId;
    setAfterPageLoadFocusTarget(
      `ul.bookmark-list li[data-bookmark-id='${bookmarkId}'] a.view-action`,
    );
  }
}

customElements.define("ld-details-modal", DetailsModal);


================================================
FILE: bookmarks/frontend/components/dev-tool.js
================================================
import { LitElement, html, css } from "lit";

class DevTool extends LitElement {
  static properties = {
    profile: { type: Object, state: true },
    formAction: { type: String, attribute: "data-form-action" },
    csrfToken: { type: String, attribute: "data-csrf-token" },
    isOpen: { type: Boolean, state: true },
  };

  static styles = css`
    :host {
      position: fixed;
      bottom: 1rem;
      right: 1rem;
      z-index: 10000;
    }

    .button {
      background: var(--btn-primary-bg-color);
      color: var(--btn-primary-text-color);
      border: none;
      padding: var(--unit-2);
      border-radius: var(--border-radius);
      box-shadow: var(--btn-box-shadow);
      cursor: pointer;
      height: auto;
      line-height: 0;
    }

    .overlay {
      display: none;
      position: absolute;
      bottom: 100%;
      right: 0;
      background: var(--body-color);
      color: var(--text-color);
      border: 1px solid var(--border-color);
      border-radius: var(--border-radius);
      padding: var(--unit-2);
      margin-bottom: var(--unit-2);
      min-width: 220px;
      box-shadow: var(--box-shadow-lg);
      font-size: var(--font-size-sm);
    }

    :host([open]) .overlay {
      display: block;
    }

    h3 {
      margin: 0 0 var(--unit-2) 0;
    }

    label {
      display: flex;
      align-items: center;
      gap: var(--unit-1);
      cursor: pointer;
    }

    label:has(select) {
      margin-bottom: var(--unit-1);
    }

    label:has(select) span {
      min-width: 100px;
    }

    hr {
      margin: var(--unit-2) 0;
      border: none;
      border-top: 1px solid var(--border-color);
    }
  `;

  static fields = [
    {
      type: "select",
      key: "theme",
      label: "Theme",
      options: [
        { value: "auto", label: "Auto" },
        { value: "light", label: "Light" },
        { value: "dark", label: "Dark" },
      ],
    },
    {
      type: "select",
      key: "bookmark_date_display",
      label: "Date",
      options: [
        { value: "relative", label: "Relative" },
        { value: "absolute", label: "Absolute" },
        { value: "hidden", label: "Hidden" },
      ],
    },
    {
      type: "select",
      key: "bookmark_description_display",
      label: "Description",
      options: [
        { value: "inline", label: "Inline" },
        { value: "separate", label: "Separate" },
      ],
    },
    { type: "checkbox", key: "enable_favicons", label: "Favicons" },
    { type: "checkbox", key: "enable_preview_images", label: "Preview images" },
    { type: "checkbox", key: "display_url", label: "Display URL" },
    { type: "checkbox", key: "permanent_notes", label: "Permanent notes" },
    { type: "checkbox", key: "collapse_side_panel", label: "Collapse sidebar" },
    { type: "checkbox", key: "sticky_pagination", label: "Sticky pagination" },
    { type: "checkbox", key: "hide_bundles", label: "Hide bundles" },
  ];

  constructor() {
    super();
    this.isOpen = false;
    this.profile = {};
    this._onOutsideClick = this._onOutsideClick.bind(this);
  }

  connectedCallback() {
    super.connectedCallback();
    const profileData = document.getElementById("json_profile");
    this.profile = JSON.parse(profileData.textContent || "{}");
    document.addEventListener("click", this._onOutsideClick);
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    document.removeEventListener("click", this._onOutsideClick);
  }

  _onOutsideClick(e) {
    if (!this.contains(e.target) && this.isOpen) {
      this.isOpen = false;
      this.removeAttribute("open");
    }
  }

  _toggle() {
    this.isOpen = !this.isOpen;
    if (this.isOpen) {
      this.setAttribute("open", "");
    } else {
      this.removeAttribute("open");
    }
  }

  _handleChange(key, value) {
    this.profile = { ...this.profile, [key]: value };
    if (key === "theme") {
      const themeLinks = document.head.querySelectorAll('link[href*="theme"]');
      themeLinks.forEach((link) => link.remove());
    }
    this._submitForm();
  }

  _renderField(field) {
    switch (field.type) {
      case "checkbox":
        return html`
          <label>
            <input
              type="checkbox"
              .checked=${this.profile[field.key] || false}
              @change=${(e) => this._handleChange(field.key, e.target.checked)}
            />
            ${field.label}
          </label>
        `;
      case "select":
        return html`
          <label>
            <span>${field.label}:</span>
            <select
              @change=${(e) => this._handleChange(field.key, e.target.value)}
            >
              ${field.options.map(
                (opt) => html`
                  <option
                    value=${opt.value}
                    ?selected=${this.profile[field.key] === opt.value}
                  >
                    ${opt.label}
                  </option>
                `,
              )}
            </select>
          </label>
        `;
      case "divider":
        return html`<hr />`;
      default:
        return null;
    }
  }

  async _submitForm() {
    const formData = new FormData();
    formData.append("csrfmiddlewaretoken", this.csrfToken);

    // Profile fields
    for (const [key, value] of Object.entries(this.profile)) {
      if (typeof value === "boolean" && value) {
        formData.append(key, "on");
      } else if (typeof value !== "boolean") {
        formData.append(key, value);
      }
    }

    // Submit button name that settings.update expects
    formData.append("update_profile", "1");

    await fetch(this.formAction, {
      method: "POST",
      body: formData,
    });

    const url = new URL(window.location);
    url.searchParams.set("ts", Date.now().toString());
    window.history.replaceState({}, "", url);

    Turbo.visit(url.toString());
  }

  render() {
    return html`
      <button class="button" @click=${() => this._toggle()}>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path stroke="none" d="M0 0h24v24H0z" fill="none" />
          <path
            d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065"
          />
          <path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
        </svg>
      </button>
      <div class="overlay">
        <h3>Dev Tools</h3>
        ${DevTool.fields.map((field) => this._renderField(field))}
      </div>
    `;
  }
}

customElements.define("ld-dev-tool", DevTool);


================================================
FILE: bookmarks/frontend/components/dropdown.js
================================================
import { HeadlessElement } from "../utils/element.js";

class Dropdown extends HeadlessElement {
  constructor() {
    super();
    this.opened = false;
    this.onClick = this.onClick.bind(this);
    this.onOutsideClick = this.onOutsideClick.bind(this);
    this.onEscape = this.onEscape.bind(this);
    this.onFocusOut = this.onFocusOut.bind(this);
  }

  init() {
    // Prevent opening the dropdown automatically on focus, so that it only
    // opens on click when JS is enabled
    this.style.setProperty("--dropdown-focus-display", "none");
    this.addEventListener("keydown", this.onEscape);
    this.addEventListener("focusout", this.onFocusOut);

    this.toggle = this.querySelector(".dropdown-toggle");
    this.toggle.setAttribute("aria-expanded", "false");
    this.toggle.addEventListener("click", this.onClick);
  }

  disconnectedCallback() {
    this.close();
  }

  open() {
    this.opened = true;
    this.classList.add("active");
    this.toggle.setAttribute("aria-expanded", "true");
    document.addEventListener("click", this.onOutsideClick);
  }

  close() {
    this.opened = false;
    this.classList.remove("active");
    this.toggle?.setAttribute("aria-expanded", "false");
    document.removeEventListener("click", this.onOutsideClick);
  }

  onClick() {
    if (this.opened) {
      this.close();
    } else {
      this.open();
    }
  }

  onOutsideClick(event) {
    if (!this.contains(event.target)) {
      this.close();
    }
  }

  onEscape(event) {
    if (event.key === "Escape" && this.opened) {
      event.preventDefault();
      this.close();
      this.toggle.focus();
    }
  }

  onFocusOut(event) {
    if (!this.contains(event.relatedTarget)) {
      this.close();
    }
  }
}

customElements.define("ld-dropdown", Dropdown);


================================================
FILE: bookmarks/frontend/components/filter-drawer.js
================================================
import { html, render } from "lit";
import { Modal } from "./modal.js";
import { HeadlessElement } from "../utils/element.js";
import { isKeyboardActive } from "../utils/focus.js";

class FilterDrawerTrigger extends HeadlessElement {
  init() {
    this.onClick = this.onClick.bind(this);
    this.addEventListener("click", this.onClick.bind(this));
  }

  onClick() {
    const modal = document.createElement("ld-filter-drawer");
    document.body.querySelector(".modals").appendChild(modal);
  }
}

customElements.define("ld-filter-drawer-trigger", FilterDrawerTrigger);

class FilterDrawer extends Modal {
  connectedCallback() {
    this.classList.add("modal", "drawer");

    // Render modal structure
    render(
      html`
        <div class="modal-overlay" data-close-modal></div>
        <div class="modal-container" role="dialog" aria-modal="true">
          <div class="modal-header">
            <h2>Filters</h2>
            <button
              class="btn btn-noborder close"
              aria-label="Close dialog"
              data-close-modal
            >
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="24"
                height="24"
                viewBox="0 0 24 24"
                stroke-width="2"
                stroke="currentColor"
                fill="none"
                stroke-linecap="round"
                stroke-linejoin="round"
              >
                <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
                <path d="M18 6l-12 12"></path>
                <path d="M6 6l12 12"></path>
              </svg>
            </button>
          </div>
          <div class="modal-body"></div>
        </div>
      `,
      this,
    );
    // Teleport filter content
    this.teleport();
    // Force close on turbo cache to restore content
    this.doClose = this.doClose.bind(this);
    document.addEventListener("turbo:before-cache", this.doClose);
    // Force reflow to make transform transition work
    this.getBoundingClientRect();
    // Add active class to start slide-in animation
    requestAnimationFrame(() => this.classList.add("active"));
    // Call super.init() after rendering to ensure elements are available
    super.init();
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.teleportBack();
    document.removeEventListener("turbo:before-cache", this.doClose);
  }

  mapHeading(container, from, to) {
    const headings = container.querySelectorAll(from);
    headings.forEach((heading) => {
      const newHeading = document.createElement(to);
      newHeading.textContent = heading.textContent;
      heading.replaceWith(newHeading);
    });
  }

  teleport() {
    const content = this.querySelector(".modal-body");
    const sidePanel = document.querySelector(".side-panel");
    content.append(...sidePanel.children);
    this.mapHeading(content, "h2", "h3");
  }

  teleportBack() {
    const sidePanel = document.querySelector(".side-panel");
    const content = this.querySelector(".modal-body");
    sidePanel.append(...content.children);
    this.mapHeading(sidePanel, "h3", "h2");
  }

  doClose() {
    super.doClose();

    // Try restore focus to drawer trigger
    const restoreFocusElement =
      document.querySelector("ld-filter-drawer-trigger") || document.body;
    restoreFocusElement.focus({ focusVisible: isKeyboardActive() });
  }
}

customElements.define("ld-filter-drawer", FilterDrawer);


================================================
FILE: bookmarks/frontend/components/form.js
================================================
import { HeadlessElement } from "../utils/element.js";

class Form extends HeadlessElement {
  constructor() {
    super();
    this.onKeyDown = this.onKeyDown.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  init() {
    this.addEventListener("keydown", this.onKeyDown);
    this.addEventListener("change", this.onChange);

    if (this.hasAttribute("data-form-reset")) {
      // Resets form controls to their initial values before Turbo caches the DOM.
      // Useful for filter forms where navigating back would otherwise still show
      // values from after the form submission, which means the filters would be out
      // of sync with the URL.
      this.initFormReset();
    }
  }

  disconnectedCallback() {
    if (this.hasAttribute("data-form-reset")) {
      this.resetForm();
    }
  }

  onChange(event) {
    if (event.target.hasAttribute("data-submit-on-change")) {
      this.querySelector("form")?.requestSubmit();
    }
  }

  onKeyDown(event) {
    // Check for Ctrl/Cmd + Enter combination
    if (
      this.hasAttribute("data-submit-on-ctrl-enter") &&
      event.key === "Enter" &&
      (event.metaKey || event.ctrlKey)
    ) {
      event.preventDefault();
      event.stopPropagation();
      this.querySelector("form")?.requestSubmit();
    }
  }

  initFormReset() {
    this.controls = this.querySelectorAll("input, select");
    this.controls.forEach((control) => {
      if (control.type === "checkbox" || control.type === "radio") {
        control.__initialValue = control.checked;
      } else {
        control.__initialValue = control.value;
      }
    });
  }

  resetForm() {
    this.controls.forEach((control) => {
      if (control.type === "checkbox" || control.type === "radio") {
        control.checked = control.__initialValue;
      } else {
        control.value = control.__initialValue;
      }
      delete control.__initialValue;
    });
  }
}

customElements.define("ld-form", Form);


================================================
FILE: bookmarks/frontend/components/modal.js
================================================
import { FocusTrapController } from "../utils/focus.js";
import { HeadlessElement } from "../utils/element.js";

export class Modal extends HeadlessElement {
  init() {
    this.onClose = this.onClose.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);

    this.querySelectorAll("[data-close-modal]").forEach((btn) => {
      btn.addEventListener("click", this.onClose);
    });
    this.addEventListener("keydown", this.onKeyDown);

    this.setupScrollLock();
    this.focusTrap = new FocusTrapController(
      this.querySelector(".modal-container"),
    );
  }

  disconnectedCallback() {
    this.removeScrollLock();
    this.focusTrap.destroy();
  }

  setupScrollLock() {
    document.body.classList.add("scroll-lock");
  }

  removeScrollLock() {
    document.body.classList.remove("scroll-lock");
  }

  onKeyDown(event) {
    // Skip if event occurred within an input element
    const targetNodeName = event.target.nodeName;
    const isInputTarget =
      targetNodeName === "INPUT" ||
      targetNodeName === "SELECT" ||
      targetNodeName === "TEXTAREA";

    if (isInputTarget) {
      return;
    }

    if (event.key === "Escape") {
      this.onClose(event);
    }
  }

  onClose(event) {
    event.preventDefault();
    this.classList.add("closing");
    this.addEventListener(
      "animationend",
      (event) => {
        if (event.animationName === "fade-out") {
          this.doClose();
        }
      },
      { once: true },
    );
  }

  doClose() {
    this.remove();
    this.dispatchEvent(new CustomEvent("modal:close"));

    // Navigate to close URL
    const closeUrl = this.dataset.closeUrl;
    const frame = this.dataset.turboFrame;
    const action = this.dataset.turboAction || "replace";
    if (closeUrl) {
      Turbo.visit(closeUrl, { action, frame: frame });
    }
  }
}

customElements.define("ld-modal", Modal);


================================================
FILE: bookmarks/frontend/components/search-autocomplete.js
================================================
import { html } from "lit";
import { api } from "../api.js";
import { TurboLitElement } from "../utils/element.js";
import {
  clampText,
  debounce,
  getCurrentWord,
  getCurrentWordBounds,
} from "../utils/input.js";
import { PositionController } from "../utils/position-controller.js";
import { SearchHistory } from "../utils/search-history.js";
import { cache } from "../utils/tag-cache.js";

export class SearchAutocomplete extends TurboLitElement {
  static properties = {
    inputName: { type: String, attribute: "input-name" },
    inputPlaceholder: { type: String, attribute: "input-placeholder" },
    inputValue: { type: String, attribute: "input-value" },
    mode: { type: String },
    user: { type: String },
    shared: { type: String },
    unread: { type: String },
    target: { type: String },
    isFocus: { state: true },
    isOpen: { state: true },
    suggestions: { state: true },
    selectedIndex: { state: true },
  };

  constructor() {
    super();
    this.inputName = "";
    this.inputPlaceholder = "";
    this.inputValue = "";
    this.mode = "";
    this.target = "_blank";
    this.isFocus = false;
    this.isOpen = false;
    this.suggestions = {
      recentSearches: [],
      bookmarks: [],
      tags: [],
      total: [],
    };
    this.selectedIndex = undefined;
    this.input = null;
    this.menu = null;
    this.searchHistory = new SearchHistory();
    this.debouncedLoadSuggestions = debounce(() => this.loadSuggestions());
  }

  firstUpdated() {
    this.style.setProperty("--menu-max-height", "400px");
    this.input = this.querySelector("input");
    this.menu = this.querySelector(".menu");
    // Track current search query after loading the page
    this.searchHistory.pushCurrent();
    this.updateSuggestions();
    this.positionController = new PositionController({
      anchor: this.input,
      overlay: this.menu,
      autoWidth: true,
      placement: "bottom-start",
    });
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.close();
  }

  handleFocus() {
    this.isFocus = true;
  }

  handleBlur() {
    this.isFocus = false;
    this.close();
  }

  handleInput(e) {
    this.inputValue = e.target.value;
    this.debouncedLoadSuggestions();
  }

  handleKeyDown(e) {
    // Enter
    if (
      this.isOpen &&
      this.selectedIndex !== undefined &&
      (e.keyCode === 13 || e.keyCode === 9)
    ) {
      const suggestion = this.suggestions.total[this.selectedIndex];
      if (suggestion) this.completeSuggestion(suggestion);
      e.preventDefault();
    }
    // Escape
    if (e.keyCode === 27) {
      this.close();
      e.preventDefault();
    }
    // Up arrow
    if (e.keyCode === 38) {
      this.updateSelection(-1);
      e.preventDefault();
    }
    // Down arrow
    if (e.keyCode === 40) {
      if (!this.isOpen) {
        this.loadSuggestions();
      } else {
        this.updateSelection(1);
      }
      e.preventDefault();
    }
  }

  open() {
    this.isOpen = true;
    this.positionController.enable();
  }

  close() {
    this.isOpen = false;
    this.updateSuggestions();
    this.selectedIndex = undefined;
    this.positionController.disable();
  }

  hasSuggestions() {
    return this.suggestions.total.length > 0;
  }

  async loadSuggestions() {
    let suggestionIndex = 0;

    function nextIndex() {
      return suggestionIndex++;
    }

    // Tag suggestions
    const tags = await cache.getTags();
    let tagSuggestions = [];
    const currentWord = getCurrentWord(this.input);
    if (currentWord && currentWord.length > 1 && currentWord[0] === "#") {
      const searchTag = currentWord.substring(1, currentWord.length);
      tagSuggestions = (tags || [])
        .filter(
          (tag) =>
            tag.name.toLowerCase().indexOf(searchTag.toLowerCase()) === 0,
        )
        .slice(0, 5)
        .map((tag) => ({
          type: "tag",
          index: nextIndex(),
          label: `#${tag.name}`,
          tagName: tag.name,
        }));
    }

    // Recent search suggestions
    const recentSearches = this.searchHistory
      .getRecentSearches(this.inputValue, 5)
      .map((value) => ({
        type: "search",
        index: nextIndex(),
        label: value,
        value,
      }));

    // Bookmark suggestions
    let bookmarks = [];

    if (this.inputValue && this.inputValue.length >= 3) {
      const path = this.mode ? `/${this.mode}` : "";
      const suggestionSearch = {
        user: this.user,
        shared: this.shared,
        unread: this.unread,
        q: this.inputValue,
      };
      const fetchedBookmarks = await api.listBookmarks(suggestionSearch, {
        limit: 5,
        offset: 0,
        path,
      });
      bookmarks = fetchedBookmarks.map((bookmark) => {
        const fullLabel = bookmark.title || bookmark.url;
        const label = clampText(fullLabel, 60);
        return {
          type: "bookmark",
          index: nextIndex(),
          label,
          bookmark,
        };
      });
    }

    this.updateSuggestions(recentSearches, bookmarks, tagSuggestions);

    if (this.hasSuggestions()) {
      this.open();
    } else {
      this.close();
    }
  }

  updateSuggestions(recentSearches, bookmarks, tagSuggestions) {
    recentSearches = recentSearches || [];
    bookmarks = bookmarks || [];
    tagSuggestions = tagSuggestions || [];
    this.suggestions = {
      recentSearches,
      bookmarks,
      tags: tagSuggestions,
      total: [...tagSuggestions, ...recentSearches, ...bookmarks],
    };
  }

  completeSuggestion(suggestion) {
    if (suggestion.type === "search") {
      this.inputValue = suggestion.value;
      this.close();
    }
    if (suggestion.type === "bookmark") {
      window.open(suggestion.bookmark.url, this.target);
      this.close();
    }
    if (suggestion.type === "tag") {
      const bounds = getCurrentWordBounds(this.input);
      const inputValue = this.input.value;
      this.input.value =
        inputValue.substring(0, bounds.start) +
        `#${suggestion.tagName} ` +
        inputValue.substring(bounds.end);
      this.close();
    }
  }

  updateSelection(dir) {
    const length = this.suggestions.total.length;

    if (length === 0) return;

    if (this.selectedIndex === undefined) {
      this.selectedIndex = dir > 0 ? 0 : Math.max(length - 1, 0);
      return;
    }

    let newIndex = this.selectedIndex + dir;

    if (newIndex < 0) newIndex = Math.max(length - 1, 0);
    if (newIndex >= length) newIndex = 0;

    this.selectedIndex = newIndex;
  }

  renderSuggestions(suggestions, title) {
    if (suggestions.length === 0) return "";

    return html`
      <li class="menu-item group-item">${title}</li>
      ${suggestions.map(
        (suggestion) => html`
          <li
            class="menu-item ${this.selectedIndex === suggestion.index
              ? "selected"
              : ""}"
          >
            <a
              href="#"
              @mousedown=${(e) => {
                e.preventDefault();
                this.completeSuggestion(suggestion);
              }}
            >
              ${suggestion.label}
            </a>
          </li>
        `,
      )}
    `;
  }

  render() {
    return html`
      <div class="form-autocomplete">
        <div
          class="form-autocomplete-input form-input ${this.isFocus
            ? "is-focused"
            : ""}"
        >
          <input
            type="search"
            class="form-input"
            name="${this.inputName}"
            placeholder="${this.inputPlaceholder}"
            autocomplete="off"
            .value="${this.inputValue}"
            @input=${this.handleInput}
            @keydown=${this.handleKeyDown}
            @focus=${this.handleFocus}
            @blur=${this.handleBlur}
          />
        </div>

        <ul class="menu ${this.isOpen ? "open" : ""}">
          ${this.renderSuggestions(this.suggestions.tags, "Tags")}
          ${this.renderSuggestions(
            this.suggestions.recentSearches,
            "Recent Searches",
          )}
          ${this.renderSuggestions(this.suggestions.bookmarks, "Bookmarks")}
        </ul>
      </div>
    `;
  }
}

customElements.define("ld-search-autocomplete", SearchAutocomplete);


================================================
FILE: bookmarks/frontend/components/tag-autocomplete.js
================================================
import { html, nothing } from "lit";
import { TurboLitElement } from "../utils/element.js";
import { getCurrentWord, getCurrentWordBounds } from "../utils/input.js";
import { PositionController } from "../utils/position-controller.js";
import { cache } from "../utils/tag-cache.js";

export class TagAutocomplete extends TurboLitElement {
  static properties = {
    inputId: { type: String, attribute: "input-id" },
    inputName: { type: String, attribute: "input-name" },
    inputValue: { type: String, attribute: "input-value" },
    inputClass: { type: String, attribute: "input-class" },
    inputPlaceholder: { type: String, attribute: "input-placeholder" },
    inputAriaDescribedBy: { type: String, attribute: "input-aria-describedby" },
    variant: { type: String },
    isFocus: { state: true },
    isOpen: { state: true },
    suggestions: { state: true },
    selectedIndex: { state: true },
  };

  constructor() {
    super();
    this.inputId = "";
    this.inputName = "";
    this.inputValue = "";
    this.inputPlaceholder = "";
    this.inputAriaDescribedBy = "";
    this.variant = "default";
    this.isFocus = false;
    this.isOpen = false;
    this.suggestions = [];
    this.selectedIndex = 0;
    this.input = null;
    this.suggestionList = null;
  }

  firstUpdated() {
    this.input = this.querySelector("input");
    this.suggestionList = this.querySelector(".menu");
    this.positionController = new PositionController({
      anchor: this.input,
      overlay: this.suggestionList,
      autoWidth: true,
      placement: "bottom-start",
    });
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.close();
  }

  handleFocus() {
    this.isFocus = true;
  }

  handleBlur() {
    this.isFocus = false;
    this.close();
  }

  async handleInput(e) {
    this.input = e.target;

    const tags = await cache.getTags();
    const word = getCurrentWord(this.input);

    this.suggestions = word
      ? tags.filter(
          (tag) => tag.name.toLowerCase().indexOf(word.toLowerCase()) === 0,
        )
      : [];

    if (word && this.suggestions.length > 0) {
      this.open();
    } else {
      this.close();
    }
  }

  handleKeyDown(e) {
    if (this.isOpen && (e.keyCode === 13 || e.keyCode === 9)) {
      const suggestion = this.suggestions[this.selectedIndex];
      this.complete(suggestion);
      e.preventDefault();
    }
    if (e.keyCode === 27) {
      this.close();
      e.preventDefault();
    }
    if (e.keyCode === 38) {
      this.updateSelection(-1);
      e.preventDefault();
    }
    if (e.keyCode === 40) {
      this.updateSelection(1);
      e.preventDefault();
    }
  }

  open() {
    this.isOpen = true;
    this.selectedIndex = 0;
    this.positionController.enable();
  }

  close() {
    this.isOpen = false;
    this.suggestions = [];
    this.selectedIndex = 0;
    this.positionController.disable();
  }

  complete(suggestion) {
    const bounds = getCurrentWordBounds(this.input);
    const value = this.input.value;
    this.input.value =
      value.substring(0, bounds.start) +
      suggestion.name +
      " " +
      value.substring(bounds.end);
    this.dispatchEvent(new CustomEvent("input", { bubbles: true }));

    this.close();
  }

  updateSelection(dir) {
    const length = this.suggestions.length;
    let newIndex = this.selectedIndex + dir;

    if (newIndex < 0) newIndex = Math.max(length - 1, 0);
    if (newIndex >= length) newIndex = 0;

    this.selectedIndex = newIndex;

    // Scroll to selected list item
    setTimeout(() => {
      if (this.suggestionList) {
        const selectedListItem =
          this.suggestionList.querySelector("li.selected");
        if (selectedListItem) {
          selectedListItem.scrollIntoView({ block: "center" });
        }
      }
    }, 0);
  }

  render() {
    return html`
      <div class="form-autocomplete ${this.variant === "small" ? "small" : ""}">
        <!-- autocomplete input container -->
        <div
          class="form-autocomplete-input form-input ${this.isFocus
            ? "is-focused"
            : ""}"
        >
          <!-- autocomplete real input box -->
          <input
            id="${this.inputId || nothing}"
            name="${this.inputName || nothing}"
            .value="${this.inputValue || ""}"
            placeholder="${this.inputPlaceholder || " "}"
            class="form-input ${this.inputClass || ""}"
            type="text"
            autocomplete="off"
            autocapitalize="off"
            aria-describedby="${this.inputAriaDescribedBy || nothing}"
            @input=${this.handleInput}
            @keydown=${this.handleKeyDown}
            @focus=${this.handleFocus}
            @blur=${this.handleBlur}
          />
        </div>

        <!-- autocomplete suggestion list -->
        <ul
          class="menu ${this.isOpen && this.suggestions.length > 0
            ? "open"
            : ""}"
        >
          <!-- menu list items -->
          ${this.suggestions.map(
            (tag, i) => html`
              <li
                class="menu-item ${this.selectedIndex === i ? "selected" : ""}"
              >
                <a
                  href="#"
                  @mousedown=${(e) => {
                    e.preventDefault();
                    this.complete(tag);
                  }}
                >
                  ${tag.name}
                </a>
              </li>
            `,
          )}
        </ul>
      </div>
    `;
  }
}

customElements.define("ld-tag-autocomplete", TagAutocomplete);


================================================
FILE: bookmarks/frontend/components/upload-button.js
================================================
import { HeadlessElement } from "../utils/element.js";

class UploadButton extends HeadlessElement {
  init() {
    this.onClick = this.onClick.bind(this);
    this.onChange = this.onChange.bind(this);

    this.button = this.querySelector('button[type="submit"]');
    this.button.addEventListener("click", this.onClick);

    this.fileInput = this.querySelector('input[type="file"]');
    this.fileInput.addEventListener("change", this.onChange);
  }

  onClick(event) {
    event.preventDefault();
    this.fileInput.click();
  }

  onChange() {
    // Check if the file input has a file selected
    if (!this.fileInput.files.length) {
      return;
    }
    this.closest("form").requestSubmit(this.button);
    // remove selected file so it doesn't get submitted again
    this.fileInput.value = "";
  }
}

customElements.define("ld-upload-button", UploadButton);


================================================
FILE: bookmarks/frontend/index.js
================================================
import "@hotwired/turbo";
import "./components/bookmark-page.js";
import "./components/clear-button.js";
import "./components/confirm-dropdown.js";
import "./components/details-modal.js";
import "./components/dev-tool.js";
import "./components/dropdown.js";
import "./components/filter-drawer.js";
import "./components/form.js";
import "./components/modal.js";
import "./components/search-autocomplete.js";
import "./components/tag-autocomplete.js";
import "./components/upload-button.js";
import "./shortcuts.js";


================================================
FILE: bookmarks/frontend/shortcuts.js
================================================
document.addEventListener("keydown", (event) => {
  // Skip if event occurred within an input element
  const targetNodeName = event.target.nodeName;
  const isInputTarget =
    targetNodeName === "INPUT" ||
    targetNodeName === "SELECT" ||
    targetNodeName === "TEXTAREA";

  if (isInputTarget) {
    return;
  }

  // Handle shortcuts for navigating bookmarks with arrow keys
  const isArrowUp = event.key === "ArrowUp";
  const isArrowDown = event.key === "ArrowDown";
  if (isArrowUp || isArrowDown) {
    event.preventDefault();

    // Detect current bookmark list item
    const items = [...document.querySelectorAll("ul.bookmark-list > li")];
    const path = event.composedPath();
    const currentItem = path.find((item) => items.includes(item));

    // Find next item
    let nextItem;
    if (currentItem) {
      nextItem = isArrowUp
        ? currentItem.previousElementSibling
        : currentItem.nextElementSibling;
    } else {
      // Select first item
      nextItem = items[0];
    }
    // Focus first link
    if (nextItem) {
      nextItem.querySelector("a").focus();
    }
  }

  // Handle shortcut for toggling all notes
  if (event.key === "e") {
    const list = document.querySelector(".bookmark-list");
    if (list) {
      list.classList.toggle("show-notes");
    }
  }

  // Handle shortcut for focusing search input
  if (event.key === "s") {
    const searchInput = document.querySelector('input[type="search"]');

    if (searchInput) {
      searchInput.focus();
      event.preventDefault();
    }
  }

  // Handle shortcut for adding new bookmark
  if (event.key === "n") {
    window.location.assign("/bookmarks/new");
  }
});


================================================
FILE: bookmarks/frontend/utils/element.js
================================================
import { LitElement } from "lit";

/**
 * Base class for custom elements that wrap existing server-rendered DOM.
 *
 * Handles timing issues where connectedCallback fires before child elements
 * are parsed during initial page load. With Turbo navigation, children are
 * always available, but on fresh page loads they may not be.
 *
 * Subclasses should override init() instead of connectedCallback().
 */
export class HeadlessElement extends HTMLElement {
  connectedCallback() {
    if (this.__initialized) {
      return;
    }
    this.__initialized = true;
    if (document.readyState === "loading") {
      document.addEventListener("turbo:load", () => this.init(), {
        once: true,
      });
    } else {
      this.init();
    }
  }

  init() {
    // Override in subclass
  }
}

let isTopFrameVisit = false;

document.addEventListener("turbo:visit", (event) => {
  const url = event.detail.url;
  isTopFrameVisit =
    document.querySelector(`turbo-frame[src="${url}"][target="_top"]`) !== null;
});

document.addEventListener("turbo:render", () => {
  isTopFrameVisit = false;
});

document.addEventListener("turbo:before-morph-element", (event) => {
  const parent = event.target?.parentElement;
  if (parent instanceof TurboLitElement) {
    // Prevent Turbo from morphing Lit elements contents, which would remove
    // elements rendered on the client side.
    event.preventDefault();
  }
});

export class TurboLitElement extends LitElement {
  constructor() {
    super();
    this.__prepareForCache = this.__prepareForCache.bind(this);
  }

  createRenderRoot() {
    return this; // Render to light DOM
  }

  connectedCallback() {
    document.addEventListener("turbo:before-cache", this.__prepareForCache);
    super.connectedCallback();
  }

  disconnectedCallback() {
    document.removeEventListener("turbo:before-cache", this.__prepareForCache);
    super.disconnectedCallback();
  }

  __prepareForCache() {
    // Remove rendered contents before caching, otherwise restoring the DOM from
    // cache will result in duplicated contents. Turbo also fires before-cache
    // when rendering a frame that does target the top frame, in which case we
    // want to keep the contents.
    if (!isTopFrameVisit) {
      this.innerHTML = "";
    }
  }
}


================================================
FILE: bookmarks/frontend/utils/focus.js
================================================
let keyboardActive = false;

window.addEventListener(
  "keydown",
  () => {
    keyboardActive = true;
  },
  { capture: true },
);

window.addEventListener(
  "mousedown",
  () => {
    keyboardActive = false;
  },
  { capture: true },
);

export function isKeyboardActive() {
  return keyboardActive;
}

export class FocusTrapController {
  constructor(element) {
    this.element = element;
    this.focusableElements = this.element.querySelectorAll(
      'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])',
    );
    this.firstFocusableElement = this.focusableElements[0];
    this.lastFocusableElement =
      this.focusableElements[this.focusableElements.length - 1];

    this.onKeyDown = this.onKeyDown.bind(this);

    this.firstFocusableElement.focus({ focusVisible: keyboardActive });
    this.element.addEventListener("keydown", this.onKeyDown);
  }

  destroy() {
    this.element.removeEventListener("keydown", this.onKeyDown);
  }

  onKeyDown(event) {
    if (event.key !== "Tab") {
      return;
    }
    if (event.shiftKey) {
      if (document.activeElement === this.firstFocusableElement) {
        event.preventDefault();
        this.lastFocusableElement.focus();
      }
    } else {
      if (document.activeElement === this.lastFocusableElement) {
        event.preventDefault();
        this.firstFocusableElement.focus();
      }
    }
  }
}

let afterPageLoadFocusTarget = [];
let firstPageLoad = true;

export function setAfterPageLoadFocusTarget(...targets) {
  afterPageLoadFocusTarget = targets;
}

function programmaticFocus(element) {
  // Ensure element is focusable
  // Hide focus outline if element is not focusable by default - might
  // reconsider this later
  const isFocusable = element.tabIndex >= 0;
  if (!isFocusable) {
    // Apparently the default tabIndex is -1, even though an element is still
    // not focusable with that. Setting an explicit -1 also sets the attribute
    // and the element becomes focusable.
    element.tabIndex = -1;
    // `focusVisible` is not supported in all browsers, so hide the outline manually
    element.style["outline"] = "none";
  }
  element.focus({
    focusVisible: isKeyboardActive() && isFocusable,
    preventScroll: true,
  });
}

// Register global listener for navigation and try to focus an element that
// results in a meaningful announcement.
document.addEventListener("turbo:load", () => {
  // Ignore initial page load to let the browser handle announcements
  if (firstPageLoad) {
    firstPageLoad = false;
    return;
  }

  // Ignore if there is a modal dialog, which should handle its own focus
  const modal = document.querySelector("[aria-modal='true']");
  if (modal) {
    return;
  }

  // Check if there is an explicit focus target for the next page load
  for (const target of afterPageLoadFocusTarget) {
    const element = document.querySelector(target);
    if (element) {
      programmaticFocus(element);
      return;
    }
  }
  afterPageLoadFocusTarget = [];

  // If there is some autofocus element, let the browser handle it
  const autofocus = document.querySelector("[autofocus]");
  if (autofocus) {
    return;
  }

  // If there is a toast as a result of some action, focus it
  const toast = document.querySelector(".toast");
  if (toast) {
    programmaticFocus(toast);
    return;
  }

  // Otherwise go with main
  const main = document.querySelector("main");
  if (main) {
    programmaticFocus(main);
  }
});


================================================
FILE: bookmarks/frontend/utils/input.js
================================================
export function debounce(callback, delay = 250) {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      timeoutId = null;
      callback(...args);
    }, delay);
  };
}

export function clampText(text, maxChars = 30) {
  if (!text || text.length <= 30) return text;

  return text.substr(0, maxChars) + "...";
}

export function getCurrentWordBounds(input) {
  const text = input.value;
  const end = input.selectionStart;
  let start = end;

  let currentChar = text.charAt(start - 1);

  while (currentChar && currentChar !== " " && start > 0) {
    start--;
    currentChar = text.charAt(start - 1);
  }

  return { start, end };
}

export function getCurrentWord(input) {
  const bounds = getCurrentWordBounds(input);

  return input.value.substring(bounds.start, bounds.end);
}


================================================
FILE: bookmarks/frontend/utils/position-controller.js
================================================
import {
  arrow,
  autoUpdate,
  computePosition,
  flip,
  offset,
  shift,
} from "@floating-ui/dom";

export class PositionController {
  constructor(options) {
    this.anchor = options.anchor;
    this.overlay = options.overlay;
    this.arrow = options.arrow;
    this.placement = options.placement || "bottom";
    this.offset = options.offset;
    this.autoWidth = options.autoWidth || false;
    this.autoUpdateCleanup = null;
  }

  enable() {
    if (!this.autoUpdateCleanup) {
      this.autoUpdateCleanup = autoUpdate(this.anchor, this.overlay, () =>
        this.updatePosition(),
      );
    }
  }

  disable() {
    if (this.autoUpdateCleanup) {
      this.autoUpdateCleanup();
      this.autoUpdateCleanup = null;
    }
  }

  updatePosition() {
    const middleware = [flip(), shift()];
    if (this.arrow) {
      middleware.push(arrow({ element: this.arrow }));
    }
    if (this.offset) {
      middleware.push(offset(this.offset));
    }
    computePosition(this.anchor, this.overlay, {
      placement: this.placement,
      strategy: "fixed",
      middleware,
    }).then(({ x, y, placement, middlewareData }) => {
      Object.assign(this.overlay.style, {
        left: `${x}px`,
        top: `${y}px`,
      });

      this.overlay.classList.remove("top-aligned", "bottom-aligned");
      this.overlay.classList.add(`${placement}-aligned`);

      if (this.arrow) {
        const { x, y } = middlewareData.arrow;
        Object.assign(this.arrow.style, {
          left: x != null ? `${x}px` : "",
          top: y != null ? `${y}px` : "",
        });
      }
    });

    if (this.autoWidth) {
      const width = this.anchor.offsetWidth;
      this.overlay.style.width = `${width}px`;
    }
  }
}


================================================
FILE: bookmarks/frontend/utils/search-history.js
================================================
const SEARCH_HISTORY_KEY = "searchHistory";
const MAX_ENTRIES = 30;

export class SearchHistory {
  getHistory() {
    const historyJson = localStorage.getItem(SEARCH_HISTORY_KEY);
    return historyJson
      ? JSON.parse(historyJson)
      : {
          recent: [],
        };
  }

  pushCurrent() {
    // Skip if browser is not compatible
    if (!window.URLSearchParams) return;
    const urlParams = new URLSearchParams(window.location.search);
    const searchParam = urlParams.get("q");

    if (!searchParam) return;

    this.push(searchParam);
  }

  push(search) {
    const history = this.getHistory();

    history.recent.unshift(search);

    // Remove duplicates and clamp to max entries
    history.recent = history.recent.reduce((acc, cur) => {
      if (acc.length >= MAX_ENTRIES) return acc;
      if (acc.indexOf(cur) >= 0) return acc;
      acc.push(cur);
      return acc;
    }, []);

    const newHistoryJson = JSON.stringify(history);
    localStorage.setItem(SEARCH_HISTORY_KEY, newHistoryJson);
  }

  getRecentSearches(query, max) {
    const history = this.getHistory();

    return history.recent
      .filter(
        (search) =>
          !query || search.toLowerCase().indexOf(query.toLowerCase()) >= 0,
      )
      .slice(0, max);
  }
}


================================================
FILE: bookmarks/frontend/utils/tag-cache.js
================================================
import { api } from "../api.js";

class TagCache {
  constructor(api) {
    this.api = api;

    // Reset cached tags after a form submission
    document.addEventListener("turbo:submit-end", () => {
      this.tagsPromise = null;
    });
  }

  getTags() {
    if (!this.tagsPromise) {
      this.tagsPromise = this.api
        .getTags({
          limit: 5000,
          offset: 0,
        })
        .then((tags) =>
          tags.sort((left, right) =>
            left.name.toLowerCase().localeCompare(right.name.toLowerCase()),
          ),
        )
        .catch((e) => {
          console.warn("Cache: Error loading tags", e);
          return [];
        });
    }

    return this.tagsPromise;
  }
}

export const cache = new TagCache(api);


================================================
FILE: bookmarks/management/commands/backup.py
================================================
import os
import sqlite3

from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help = "Creates a backup of the linkding database"

    def add_arguments(self, parser):
        parser.add_argument("destination", type=str, help="Backup file destination")

    def handle(self, *args, **options):
        destination = options["destination"]

        def progress(status, remaining, total):
            self.stdout.write(f"Copied {total - remaining} of {total} pages...")

        source_db = sqlite3.connect(os.path.join("data", "db.sqlite3"))
        backup_db = sqlite3.connect(destination)
        with backup_db:
            source_db.backup(backup_db, pages=50, progress=progress)
        backup_db.close()
        source_db.close()

        self.stdout.write(self.style.SUCCESS(f"Backup created at {destination}"))
        self.stdout.write(
            self.style.WARNING(
                "This backup method is deprecated and may be removed in the future. Please use the full_backup command instead, which creates backup zip file with all contents of the data folder."
            )
        )


================================================
FILE: bookmarks/management/commands/create_initial_superuser.py
================================================
import logging
import os

from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand

logger = logging.getLogger(__name__)


class Command(BaseCommand):
    help = "Creates an initial superuser for a deployment using env variables"

    def handle(self, *args, **options):
        User = get_user_model()
        superuser_name = os.getenv("LD_SUPERUSER_NAME", None)
        superuser_password = os.getenv("LD_SUPERUSER_PASSWORD", None)

        # Skip if option is undefined
        if not superuser_name:
            logger.info(
                "Skip creating initial superuser, LD_SUPERUSER_NAME option is not defined"
            )
            return

        # Skip if user already exists
        user_exists = User.objects.filter(username=superuser_name).exists()
        if user_exists:
            logger.info("Skip creating initial superuser, user already exists")
            return

        user = User(username=superuser_name, is_superuser=True, is_staff=True)

        if superuser_password:
            user.set_password(superuser_password)
        else:
            user.set_unusable_password()

        user.save()
        logger.info("Created initial superuser")


================================================
FILE: bookmarks/management/commands/enable_wal.py
================================================
import logging

from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import connections

logger = logging.getLogger(__name__)


class Command(BaseCommand):
    help = "Enable WAL journal mode when using an SQLite database"

    def handle(self, *args, **options):
        if not settings.USE_SQLITE:
            return

        connection = connections["default"]
        with connection.cursor() as cursor:
            cursor.execute("PRAGMA journal_mode")
            current_mode = cursor.fetchone()[0]
            logger.info(f"Current journal mode: {current_mode}")
            if current_mode != "wal":
                cursor.execute("PRAGMA journal_mode=wal;")
                logger.info("Switched to WAL journal mode")


================================================
FILE: bookmarks/management/commands/ensure_superuser.py
================================================
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help = "Creates an admin user non-interactively if it doesn't exist"

    def add_arguments(self, parser):
        parser.add_argument("--username", help="Admin's username")
        parser.add_argument("--email", help="Admin's email")
        parser.add_argument("--password", help="Admin's password")

    def handle(self, *args, **options):
        User = get_user_model()
        if not User.objects.filter(username=options["username"]).exists():
            User.objects.create_superuser(
                username=options["username"],
                email=options["email"],
                password=options["password"],
            )


================================================
FILE: bookmarks/management/commands/full_backup.py
================================================
import os
import sqlite3
import tempfile
import zipfile

from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help = "Creates a backup of the linkding data folder"

    def add_arguments(self, parser):
        parser.add_argument("backup_file", type=str, help="Backup zip file destination")

    def handle(self, *args, **options):
        backup_file = options["backup_file"]
        with zipfile.ZipFile(backup_file, "w", zipfile.ZIP_DEFLATED) as zip_file:
            # Backup the database
            self.stdout.write("Create database backup...")
            with tempfile.TemporaryDirectory() as temp_dir:
                backup_db_file = os.path.join(temp_dir, "db.sqlite3")
                self.backup_database(backup_db_file)
                zip_file.write(backup_db_file, "db.sqlite3")

            # Backup the assets folder
            if not os.path.exists(os.path.join("data", "assets")):
                self.stdout.write(
                    self.style.WARNING("No assets folder found. Skipping...")
                )
            else:
                self.stdout.write("Backup bookmark assets...")
                assets_folder = os.path.join("data", "assets")
                for root, _, files in os.walk(assets_folder):
                    for file in files:
                        file_path = os.path.join(root, file)
                        zip_file.write(file_path, os.path.join("assets", file))

            # Backup the favicons folder
            if not os.path.exists(os.path.join("data", "favicons")):
                self.stdout.write(
                    self.style.WARNING("No favicons folder found. Skipping...")
                )
            else:
                self.stdout.write("Backup bookmark favicons...")
                favicons_folder = os.path.join("data", "favicons")
                for root, _, files in os.walk(favicons_folder):
                    for file in files:
                        file_path = os.path.join(root, file)
                        zip_file.write(file_path, os.path.join("favicons", file))

            # Backup the previews folder
            if not os.path.exists(os.path.join("data", "previews")):
                self.stdout.write(
                    self.style.WARNING("No previews folder found. 
Download .txt
gitextract_7gdsh6yf/

├── .coveragerc
├── .devcontainer/
│   └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── build-test.yaml
│       ├── build.yaml
│       └── main.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── Makefile
├── README.md
├── SECURITY.md
├── assets/
│   ├── header.afdesign
│   ├── logo-inset.afdesign
│   ├── logo.afdesign
│   └── social-preview.afdesign
├── bookmarks/
│   ├── __init__.py
│   ├── admin.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   ├── routes.py
│   │   └── serializers.py
│   ├── apps.py
│   ├── context_processors.py
│   ├── feeds.py
│   ├── forms.py
│   ├── frontend/
│   │   ├── api.js
│   │   ├── components/
│   │   │   ├── bookmark-page.js
│   │   │   ├── clear-button.js
│   │   │   ├── confirm-dropdown.js
│   │   │   ├── details-modal.js
│   │   │   ├── dev-tool.js
│   │   │   ├── dropdown.js
│   │   │   ├── filter-drawer.js
│   │   │   ├── form.js
│   │   │   ├── modal.js
│   │   │   ├── search-autocomplete.js
│   │   │   ├── tag-autocomplete.js
│   │   │   └── upload-button.js
│   │   ├── index.js
│   │   ├── shortcuts.js
│   │   └── utils/
│   │       ├── element.js
│   │       ├── focus.js
│   │       ├── input.js
│   │       ├── position-controller.js
│   │       ├── search-history.js
│   │       └── tag-cache.js
│   ├── management/
│   │   └── commands/
│   │       ├── backup.py
│   │       ├── create_initial_superuser.py
│   │       ├── enable_wal.py
│   │       ├── ensure_superuser.py
│   │       ├── full_backup.py
│   │       ├── generate_secret_key.py
│   │       ├── import_netscape.py
│   │       └── migrate_tasks.py
│   ├── middlewares.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0002_auto_20190629_2303.py
│   │   ├── 0003_auto_20200913_0656.py
│   │   ├── 0004_auto_20200926_1028.py
│   │   ├── 0005_auto_20210103_1212.py
│   │   ├── 0006_bookmark_is_archived.py
│   │   ├── 0007_userprofile.py
│   │   ├── 0008_userprofile_bookmark_date_display.py
│   │   ├── 0009_bookmark_web_archive_snapshot_url.py
│   │   ├── 0010_userprofile_bookmark_link_target.py
│   │   ├── 0011_userprofile_web_archive_integration.py
│   │   ├── 0012_toast.py
│   │   ├── 0013_web_archive_optin_toast.py
│   │   ├── 0014_alter_bookmark_unread.py
│   │   ├── 0015_feedtoken.py
│   │   ├── 0016_bookmark_shared.py
│   │   ├── 0017_userprofile_enable_sharing.py
│   │   ├── 0018_bookmark_favicon_file.py
│   │   ├── 0019_userprofile_enable_favicons.py
│   │   ├── 0020_userprofile_tag_search.py
│   │   ├── 0021_userprofile_display_url.py
│   │   ├── 0022_bookmark_notes.py
│   │   ├── 0023_userprofile_permanent_notes.py
│   │   ├── 0024_userprofile_enable_public_sharing.py
│   │   ├── 0025_userprofile_search_preferences.py
│   │   ├── 0026_userprofile_custom_css.py
│   │   ├── 0027_userprofile_bookmark_description_display_and_more.py
│   │   ├── 0028_userprofile_display_archive_bookmark_action_and_more.py
│   │   ├── 0029_bookmark_list_actions_toast.py
│   │   ├── 0030_bookmarkasset.py
│   │   ├── 0031_userprofile_enable_automatic_html_snapshots.py
│   │   ├── 0032_html_snapshots_hint_toast.py
│   │   ├── 0033_userprofile_default_mark_unread.py
│   │   ├── 0034_bookmark_preview_image_file_and_more.py
│   │   ├── 0035_userprofile_tag_grouping.py
│   │   ├── 0036_userprofile_auto_tagging_rules.py
│   │   ├── 0037_globalsettings.py
│   │   ├── 0038_globalsettings_guest_profile_user.py
│   │   ├── 0039_globalsettings_enable_link_prefetch.py
│   │   ├── 0040_userprofile_items_per_page_and_more.py
│   │   ├── 0041_merge_metadata.py
│   │   ├── 0042_userprofile_custom_css_hash.py
│   │   ├── 0043_userprofile_collapse_side_panel.py
│   │   ├── 0044_bookmark_latest_snapshot.py
│   │   ├── 0045_userprofile_hide_bundles_bookmarkbundle.py
│   │   ├── 0046_add_url_normalized_field.py
│   │   ├── 0047_populate_url_normalized_field.py
│   │   ├── 0048_userprofile_default_mark_shared.py
│   │   ├── 0049_userprofile_legacy_search.py
│   │   ├── 0050_new_search_toast.py
│   │   ├── 0051_fix_normalized_url.py
│   │   ├── 0052_apitoken.py
│   │   ├── 0053_migrate_api_tokens.py
│   │   ├── 0054_bookmarkbundle_filter_shared_and_more.py
│   │   └── __init__.py
│   ├── models.py
│   ├── queries.py
│   ├── services/
│   │   ├── __init__.py
│   │   ├── assets.py
│   │   ├── auto_tagging.py
│   │   ├── bookmarks.py
│   │   ├── bundles.py
│   │   ├── exporter.py
│   │   ├── favicon_loader.py
│   │   ├── importer.py
│   │   ├── monolith.py
│   │   ├── parser.py
│   │   ├── preview_image_loader.py
│   │   ├── search_query_parser.py
│   │   ├── singlefile.py
│   │   ├── tags.py
│   │   ├── tasks.py
│   │   ├── wayback.py
│   │   └── website_loader.py
│   ├── settings/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── custom.py
│   │   ├── dev.py
│   │   └── prod.py
│   ├── signals.py
│   ├── static/
│   │   ├── live-reload.js
│   │   ├── robots.txt
│   │   └── vendor/
│   │       └── Readability.js
│   ├── styles/
│   │   ├── auth.css
│   │   ├── bookmark-details.css
│   │   ├── bookmark-form.css
│   │   ├── bookmark-page.css
│   │   ├── bundles.css
│   │   ├── components.css
│   │   ├── crud.css
│   │   ├── layout.css
│   │   ├── markdown.css
│   │   ├── reader-mode.css
│   │   ├── responsive.css
│   │   ├── settings.css
│   │   ├── tags.css
│   │   ├── theme/
│   │   │   ├── LICENSE
│   │   │   ├── _normalize.css
│   │   │   ├── animations.css
│   │   │   ├── asian.css
│   │   │   ├── autocomplete.css
│   │   │   ├── badges.css
│   │   │   ├── base.css
│   │   │   ├── buttons.css
│   │   │   ├── code.css
│   │   │   ├── dropdowns.css
│   │   │   ├── empty.css
│   │   │   ├── forms.css
│   │   │   ├── menus.css
│   │   │   ├── modals.css
│   │   │   ├── pagination.css
│   │   │   ├── tables.css
│   │   │   ├── tabs.css
│   │   │   ├── toasts.css
│   │   │   ├── typography.css
│   │   │   ├── utilities.css
│   │   │   └── variables.css
│   │   ├── theme-dark.css
│   │   └── theme-light.css
│   ├── tasks.py
│   ├── templates/
│   │   ├── admin/
│   │   │   └── background_tasks.html
│   │   ├── bookmarks/
│   │   │   ├── bookmark_list.html
│   │   │   ├── bookmark_page.html
│   │   │   ├── bulk_edit_bar.html
│   │   │   ├── bundle_section.html
│   │   │   ├── close.html
│   │   │   ├── details/
│   │   │   │   ├── asset_icon.html
│   │   │   │   ├── assets.html
│   │   │   │   ├── form.html
│   │   │   │   └── modal.html
│   │   │   ├── edit.html
│   │   │   ├── empty_bookmarks.html
│   │   │   ├── form.html
│   │   │   ├── new.html
│   │   │   ├── read.html
│   │   │   ├── search.html
│   │   │   ├── tag_cloud.html
│   │   │   ├── tag_section.html
│   │   │   └── user_section.html
│   │   ├── bundles/
│   │   │   ├── edit.html
│   │   │   ├── form.html
│   │   │   ├── index.html
│   │   │   ├── new.html
│   │   │   └── preview.html
│   │   ├── opensearch.xml
│   │   ├── registration/
│   │   │   ├── login.html
│   │   │   ├── password_change_done.html
│   │   │   └── password_change_form.html
│   │   ├── settings/
│   │   │   ├── bookmarklet.js
│   │   │   ├── bookmarklet_clientside.js
│   │   │   ├── create_api_token_modal.html
│   │   │   ├── general.html
│   │   │   └── integrations.html
│   │   ├── shared/
│   │   │   ├── dev_tool.html
│   │   │   ├── error_list.html
│   │   │   ├── head.html
│   │   │   ├── layout.html
│   │   │   ├── messages.html
│   │   │   ├── modal_header.html
│   │   │   ├── nav_menu.html
│   │   │   ├── pagination.html
│   │   │   └── top_frame.html
│   │   └── tags/
│   │       ├── edit.html
│   │       ├── form.html
│   │       ├── index.html
│   │       ├── merge.html
│   │       └── new.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   ├── bookmarks.py
│   │   ├── pagination.py
│   │   └── shared.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── helpers.py
│   │   ├── resources/
│   │   │   ├── simple_valid_import_file.html
│   │   │   └── simple_valid_import_file_with_one_invalid_bookmark.html
│   │   ├── test_app_options.py
│   │   ├── test_assets_service.py
│   │   ├── test_auth_api.py
│   │   ├── test_auth_proxy_support.py
│   │   ├── test_auto_tagging.py
│   │   ├── test_bookmark_action_view.py
│   │   ├── test_bookmark_archived_view.py
│   │   ├── test_bookmark_archived_view_performance.py
│   │   ├── test_bookmark_asset_view.py
│   │   ├── test_bookmark_assets.py
│   │   ├── test_bookmark_assets_api.py
│   │   ├── test_bookmark_details_modal.py
│   │   ├── test_bookmark_edit_view.py
│   │   ├── test_bookmark_index_view.py
│   │   ├── test_bookmark_index_view_performance.py
│   │   ├── test_bookmark_new_view.py
│   │   ├── test_bookmark_previews.py
│   │   ├── test_bookmark_search_form.py
│   │   ├── test_bookmark_search_model.py
│   │   ├── test_bookmark_search_tag.py
│   │   ├── test_bookmark_shared_view.py
│   │   ├── test_bookmark_shared_view_performance.py
│   │   ├── test_bookmark_validation.py
│   │   ├── test_bookmarks_api.py
│   │   ├── test_bookmarks_api_performance.py
│   │   ├── test_bookmarks_api_permissions.py
│   │   ├── test_bookmarks_list_template.py
│   │   ├── test_bookmarks_model.py
│   │   ├── test_bookmarks_service.py
│   │   ├── test_bookmarks_tasks.py
│   │   ├── test_bundles_api.py
│   │   ├── test_bundles_edit_view.py
│   │   ├── test_bundles_index_view.py
│   │   ├── test_bundles_new_view.py
│   │   ├── test_bundles_preview_view.py
│   │   ├── test_context_path.py
│   │   ├── test_create_initial_superuser_command.py
│   │   ├── test_custom_css_view.py
│   │   ├── test_exporter.py
│   │   ├── test_exporter_performance.py
│   │   ├── test_favicon_loader.py
│   │   ├── test_feeds.py
│   │   ├── test_feeds_performance.py
│   │   ├── test_health_view.py
│   │   ├── test_importer.py
│   │   ├── test_layout.py
│   │   ├── test_linkding_middleware.py
│   │   ├── test_login_view.py
│   │   ├── test_metadata_view.py
│   │   ├── test_monolith_service.py
│   │   ├── test_oidc_support.py
│   │   ├── test_opensearch_view.py
│   │   ├── test_pagination_tag.py
│   │   ├── test_parser.py
│   │   ├── test_password_change_view.py
│   │   ├── test_preview_image_loader.py
│   │   ├── test_queries.py
│   │   ├── test_root_view.py
│   │   ├── test_search_query_parser.py
│   │   ├── test_settings_export_view.py
│   │   ├── test_settings_general_view.py
│   │   ├── test_settings_import_view.py
│   │   ├── test_settings_integrations_view.py
│   │   ├── test_singlefile_service.py
│   │   ├── test_tag_cloud_template.py
│   │   ├── test_tags_edit_view.py
│   │   ├── test_tags_index_view.py
│   │   ├── test_tags_merge_view.py
│   │   ├── test_tags_model.py
│   │   ├── test_tags_new_view.py
│   │   ├── test_tags_service.py
│   │   ├── test_toasts_view.py
│   │   ├── test_user_profile_model.py
│   │   ├── test_user_select_tag.py
│   │   ├── test_utils.py
│   │   └── test_website_loader.py
│   ├── tests_e2e/
│   │   ├── __init__.py
│   │   ├── e2e_test_a11y_navigation_focus.py
│   │   ├── e2e_test_bookmark_details_modal.py
│   │   ├── e2e_test_bookmark_item.py
│   │   ├── e2e_test_bookmark_page_bulk_edit.py
│   │   ├── e2e_test_bookmark_page_partial_updates.py
│   │   ├── e2e_test_bundle_preview.py
│   │   ├── e2e_test_collapse_side_panel.py
│   │   ├── e2e_test_dropdown.py
│   │   ├── e2e_test_edit_bookmark_form.py
│   │   ├── e2e_test_filter_drawer.py
│   │   ├── e2e_test_global_shortcuts.py
│   │   ├── e2e_test_new_bookmark_form.py
│   │   ├── e2e_test_settings_general.py
│   │   ├── e2e_test_settings_integrations.py
│   │   ├── e2e_test_tag_management.py
│   │   └── helpers.py
│   ├── type_defs.py
│   ├── urls.py
│   ├── utils.py
│   ├── validators.py
│   ├── views/
│   │   ├── __init__.py
│   │   ├── access.py
│   │   ├── assets.py
│   │   ├── auth.py
│   │   ├── bookmarks.py
│   │   ├── bundles.py
│   │   ├── contexts.py
│   │   ├── custom_css.py
│   │   ├── health.py
│   │   ├── manifest.py
│   │   ├── opensearch.py
│   │   ├── reload.py
│   │   ├── root.py
│   │   ├── settings.py
│   │   ├── tags.py
│   │   ├── toasts.py
│   │   └── turbo.py
│   ├── widgets.py
│   └── wsgi.py
├── bootstrap.sh
├── docker/
│   ├── alpine.Dockerfile
│   └── default.Dockerfile
├── docker-compose.yml
├── docs/
│   ├── .gitignore
│   ├── README.md
│   ├── astro.config.mjs
│   ├── package.json
│   ├── src/
│   │   ├── assets/
│   │   │   ├── Add To Linkding.shortcut
│   │   │   └── linkding_shortcut.json
│   │   ├── components/
│   │   │   ├── Card.astro
│   │   │   └── icons.ts
│   │   ├── content/
│   │   │   ├── config.ts
│   │   │   └── docs/
│   │   │       ├── acknowledgements.md
│   │   │       ├── admin.md
│   │   │       ├── api.md
│   │   │       ├── archiving.md
│   │   │       ├── auto-tagging.md
│   │   │       ├── backups.md
│   │   │       ├── browser-extension.md
│   │   │       ├── community.md
│   │   │       ├── how-to.md
│   │   │       ├── index.mdx
│   │   │       ├── installation.md
│   │   │       ├── managed-hosting.md
│   │   │       ├── options.md
│   │   │       ├── search.md
│   │   │       ├── shortcuts.md
│   │   │       └── troubleshooting.md
│   │   ├── env.d.ts
│   │   └── styles/
│   │       └── custom.css
│   └── tsconfig.json
├── install-linkding.sh
├── manage.py
├── package.json
├── postcss.config.js
├── pyproject.toml
├── pytest.ini
├── rollup.config.mjs
├── scripts/
│   ├── build-docker.sh
│   ├── coverage.sh
│   ├── generate-changelog.py
│   ├── release.sh
│   ├── run-docker.sh
│   ├── run-postgres.sh
│   ├── setup-ublock.sh
│   ├── test-environments/
│   │   ├── authelia-oidc/
│   │   │   ├── authelia/
│   │   │   │   ├── configuration.yml
│   │   │   │   └── users_database.yml
│   │   │   ├── compose.yml
│   │   │   ├── setup.sh
│   │   │   └── traefik/
│   │   │       └── certificates.yml
│   │   └── postgres/
│   │       └── compose.yml
│   └── test-postgres.sh
├── supervisord-all.conf
├── supervisord-tasks.conf
├── uwsgi.ini
└── version.txt
Download .txt
Showing preview only (257K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2534 symbols across 230 files)

FILE: bookmarks/admin.py
  class TaskPaginator (line 29) | class TaskPaginator(Paginator):
    method __init__ (line 30) | def __init__(self):
    method count (line 35) | def count(self):
    method page (line 38) | def page(self, number):
    method enqueued_items (line 48) | def enqueued_items(self, limit, offset):
  function background_task_view (line 62) | def background_task_view(request):
  class LinkdingAdminSite (line 77) | class LinkdingAdminSite(AdminSite):
    method get_urls (line 81) | def get_urls(self):
    method get_app_list (line 88) | def get_app_list(self, request, app_label=None):
  class AdminBookmark (line 108) | class AdminBookmark(admin.ModelAdmin):
    method get_actions (line 133) | def get_actions(self, request):
    method delete_selected_bookmarks (line 141) | def delete_selected_bookmarks(self, request, queryset: QuerySet):
    method archive_selected_bookmarks (line 156) | def archive_selected_bookmarks(self, request, queryset: QuerySet):
    method unarchive_selected_bookmarks (line 171) | def unarchive_selected_bookmarks(self, request, queryset: QuerySet):
    method mark_as_read (line 186) | def mark_as_read(self, request, queryset: QuerySet):
    method mark_as_unread (line 200) | def mark_as_unread(self, request, queryset: QuerySet):
  class AdminBookmarkAsset (line 215) | class AdminBookmarkAsset(admin.ModelAdmin):
    method custom_display_name (line 217) | def custom_display_name(self, obj):
  class AdminTag (line 228) | class AdminTag(admin.ModelAdmin):
    method get_queryset (line 235) | def get_queryset(self, request):
    method bookmarks_count (line 240) | def bookmarks_count(self, obj):
    method delete_unused_tags (line 245) | def delete_unused_tags(self, request, queryset: QuerySet):
  class AdminBookmarkBundle (line 272) | class AdminBookmarkBundle(admin.ModelAdmin):
  class AdminUserProfileInline (line 289) | class AdminUserProfileInline(admin.StackedInline):
  class AdminCustomUser (line 297) | class AdminCustomUser(UserAdmin):
    method get_inline_instances (line 300) | def get_inline_instances(self, request, obj=None):
  class AdminToast (line 306) | class AdminToast(admin.ModelAdmin):
  class AdminFeedToken (line 312) | class AdminFeedToken(admin.ModelAdmin):
  class ApiTokenAdminForm (line 318) | class ApiTokenAdminForm(forms.ModelForm):
    class Meta (line 319) | class Meta:
  class AdminApiToken (line 324) | class AdminApiToken(admin.ModelAdmin):

FILE: bookmarks/api/auth.py
  class LinkdingTokenAuthentication (line 8) | class LinkdingTokenAuthentication(TokenAuthentication):
    method authenticate (line 17) | def authenticate(self, request):

FILE: bookmarks/api/routes.py
  class BookmarkViewSet (line 36) | class BookmarkViewSet(
    method get_permissions (line 47) | def get_permissions(self):
    method get_queryset (line 58) | def get_queryset(self):
    method get_serializer_context (line 76) | def get_serializer_context(self):
    method archived (line 87) | def archived(self, request: HttpRequest):
    method shared (line 91) | def shared(self, request: HttpRequest):
    method archive (line 95) | def archive(self, request: HttpRequest, pk):
    method unarchive (line 101) | def unarchive(self, request: HttpRequest, pk):
    method check (line 107) | def check(self, request: HttpRequest):
    method singlefile (line 139) | def singlefile(self, request: HttpRequest):
  class BookmarkAssetViewSet (line 171) | class BookmarkAssetViewSet(
    method get_queryset (line 180) | def get_queryset(self):
    method get_serializer_context (line 188) | def get_serializer_context(self):
    method download (line 192) | def download(self, request: HttpRequest, bookmark_id, pk):
    method upload (line 217) | def upload(self, request: HttpRequest, bookmark_id):
    method perform_destroy (line 245) | def perform_destroy(self, instance):
  class TagViewSet (line 249) | class TagViewSet(
    method get_queryset (line 258) | def get_queryset(self):
    method get_serializer_context (line 262) | def get_serializer_context(self):
  class UserViewSet (line 266) | class UserViewSet(viewsets.GenericViewSet):
    method profile (line 268) | def profile(self, request: HttpRequest):
  class BookmarkBundleViewSet (line 272) | class BookmarkBundleViewSet(
    method get_queryset (line 283) | def get_queryset(self):
    method get_serializer_context (line 287) | def get_serializer_context(self):
    method perform_destroy (line 290) | def perform_destroy(self, instance):

FILE: bookmarks/api/serializers.py
  class TagListField (line 20) | class TagListField(serializers.ListField):
  class BookmarkListSerializer (line 24) | class BookmarkListSerializer(ListSerializer):
    method to_representation (line 25) | def to_representation(self, data):
  class EmtpyField (line 32) | class EmtpyField(serializers.ReadOnlyField):
    method to_representation (line 33) | def to_representation(self, value):
  class BookmarkBundleSerializer (line 37) | class BookmarkBundleSerializer(serializers.ModelSerializer):
    class Meta (line 38) | class Meta:
    method create (line 59) | def create(self, validated_data):
  class BookmarkSerializer (line 65) | class BookmarkSerializer(serializers.ModelSerializer):
    class Meta (line 66) | class Meta:
    method get_favicon_url (line 109) | def get_favicon_url(self, obj: Bookmark):
    method get_preview_image_url (line 117) | def get_preview_image_url(self, obj: Bookmark):
    method get_web_archive_snapshot_url (line 125) | def get_web_archive_snapshot_url(self, obj: Bookmark):
    method create (line 131) | def create(self, validated_data):
    method update (line 152) | def update(self, instance: Bookmark, validated_data):
    method validate (line 162) | def validate(self, attrs):
  class BookmarkAssetSerializer (line 181) | class BookmarkAssetSerializer(serializers.ModelSerializer):
    class Meta (line 182) | class Meta:
  class TagSerializer (line 196) | class TagSerializer(serializers.ModelSerializer):
    class Meta (line 197) | class Meta:
    method create (line 202) | def create(self, validated_data):
  class UserProfileSerializer (line 206) | class UserProfileSerializer(serializers.ModelSerializer):
    class Meta (line 207) | class Meta:

FILE: bookmarks/apps.py
  class BookmarksConfig (line 4) | class BookmarksConfig(AppConfig):
    method ready (line 7) | def ready(self):

FILE: bookmarks/context_processors.py
  function toasts (line 5) | def toasts(request):
  function app_version (line 20) | def app_version(request):

FILE: bookmarks/feeds.py
  class FeedContext (line 15) | class FeedContext:
  function sanitize (line 21) | def sanitize(text: str):
  class BaseBookmarksFeed (line 31) | class BaseBookmarksFeed(Feed):
    method get_object (line 32) | def get_object(self, request, feed_key: str | None):
    method get_query_set (line 48) | def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
    method items (line 51) | def items(self, context: FeedContext):
    method item_title (line 57) | def item_title(self, item: Bookmark):
    method item_description (line 60) | def item_description(self, item: Bookmark):
    method item_link (line 63) | def item_link(self, item: Bookmark):
    method item_pubdate (line 66) | def item_pubdate(self, item: Bookmark):
    method item_categories (line 69) | def item_categories(self, item: Bookmark):
  class AllBookmarksFeed (line 73) | class AllBookmarksFeed(BaseBookmarksFeed):
    method get_query_set (line 77) | def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
    method link (line 80) | def link(self, context: FeedContext):
  class UnreadBookmarksFeed (line 84) | class UnreadBookmarksFeed(BaseBookmarksFeed):
    method get_query_set (line 88) | def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
    method link (line 93) | def link(self, context: FeedContext):
  class SharedBookmarksFeed (line 97) | class SharedBookmarksFeed(BaseBookmarksFeed):
    method get_query_set (line 101) | def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
    method link (line 106) | def link(self, context: FeedContext):
  class PublicSharedBookmarksFeed (line 110) | class PublicSharedBookmarksFeed(BaseBookmarksFeed):
    method get_object (line 114) | def get_object(self, request):
    method get_query_set (line 117) | def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
    method link (line 120) | def link(self, context: FeedContext):

FILE: bookmarks/forms.py
  class BookmarkForm (line 31) | class BookmarkForm(forms.ModelForm):
    class Meta (line 44) | class Meta:
    method __init__ (line 57) | def __init__(self, request: HttpRequest, instance: Bookmark = None):
    method is_auto_close (line 80) | def is_auto_close(self):
    method has_notes (line 86) | def has_notes(self):
    method save (line 91) | def save(self, commit=False):
    method clean_url (line 99) | def clean_url(self):
  function convert_tag_string (line 118) | def convert_tag_string(tag_string: str):
  class TagForm (line 124) | class TagForm(forms.ModelForm):
    class Meta (line 127) | class Meta:
    method __init__ (line 131) | def __init__(self, user, *args, **kwargs):
    method clean_name (line 135) | def clean_name(self):
    method save (line 149) | def save(self, commit=True):
  class TagMergeForm (line 161) | class TagMergeForm(forms.Form):
    method __init__ (line 165) | def __init__(self, user, *args, **kwargs):
    method clean_target_tag (line 169) | def clean_target_tag(self):
    method clean_merge_tags (line 189) | def clean_merge_tags(self):
  class BookmarkBundleForm (line 215) | class BookmarkBundleForm(forms.ModelForm):
    class Meta (line 232) | class Meta:
    method __init__ (line 244) | def __init__(self, *args, **kwargs):
  class BookmarkSearchForm (line 248) | class BookmarkSearchForm(forms.Form):
    method __init__ (line 275) | def __init__(
  class UserProfileForm (line 306) | class UserProfileForm(forms.ModelForm):
    class Meta (line 307) | class Meta:
  class GlobalSettingsForm (line 371) | class GlobalSettingsForm(forms.ModelForm):
    class Meta (line 372) | class Meta:
    method __init__ (line 381) | def __init__(self, *args, **kwargs):

FILE: bookmarks/frontend/api.js
  class Api (line 1) | class Api {
    method constructor (line 2) | constructor(baseUrl) {
    method listBookmarks (line 6) | listBookmarks(search, options = { limit: 100, offset: 0, path: "" }) {
    method getTags (line 22) | getTags(options = { limit: 100, offset: 0 }) {

FILE: bookmarks/frontend/components/bookmark-page.js
  class BookmarkPage (line 3) | class BookmarkPage extends HeadlessElement {
    method init (line 4) | init() {
    method disconnectedCallback (line 17) | disconnectedCallback() {
    method update (line 21) | update() {
    method updateTooltips (line 29) | updateTooltips(items) {
    method updateNotesToggles (line 42) | updateNotesToggles(items, oldItems) {
    method onToggleNotes (line 58) | onToggleNotes(event) {
    method updateBulkEdit (line 64) | updateBulkEdit() {
    method onToggleBulkEdit (line 110) | onToggleBulkEdit() {
    method onBulkActionChange (line 114) | onBulkActionChange() {
    method onToggleAll (line 118) | onToggleAll() {
    method onToggleBookmark (line 127) | onToggleBookmark() {
    method updateSelectAcross (line 136) | updateSelectAcross(allChecked) {
    method updateExecuteButton (line 145) | updateExecuteButton() {

FILE: bookmarks/frontend/components/clear-button.js
  class ClearButton (line 3) | class ClearButton extends HeadlessElement {
    method init (line 4) | init() {
    method update (line 19) | update() {
    method clear (line 23) | clear() {

FILE: bookmarks/frontend/components/confirm-dropdown.js
  function nextConfirmId (line 7) | function nextConfirmId() {
  function removeAll (line 11) | function removeAll() {
  class ConfirmDropdown (line 44) | class ConfirmDropdown extends LitElement {
    method constructor (line 45) | constructor() {
    method createRenderRoot (line 50) | createRenderRoot() {
    method firstUpdated (line 54) | firstUpdated(props) {
    method render (line 69) | render() {
    method confirm (line 90) | confirm() {
    method close (line 95) | close() {

FILE: bookmarks/frontend/components/details-modal.js
  class DetailsModal (line 4) | class DetailsModal extends Modal {
    method doClose (line 5) | doClose() {

FILE: bookmarks/frontend/components/dev-tool.js
  class DevTool (line 3) | class DevTool extends LitElement {
    method constructor (line 116) | constructor() {
    method connectedCallback (line 123) | connectedCallback() {
    method disconnectedCallback (line 130) | disconnectedCallback() {
    method _onOutsideClick (line 135) | _onOutsideClick(e) {
    method _toggle (line 142) | _toggle() {
    method _handleChange (line 151) | _handleChange(key, value) {
    method _renderField (line 160) | _renderField(field) {
    method _submitForm (line 200) | async _submitForm() {
    method render (line 228) | render() {

FILE: bookmarks/frontend/components/dropdown.js
  class Dropdown (line 3) | class Dropdown extends HeadlessElement {
    method constructor (line 4) | constructor() {
    method init (line 13) | init() {
    method disconnectedCallback (line 25) | disconnectedCallback() {
    method open (line 29) | open() {
    method close (line 36) | close() {
    method onClick (line 43) | onClick() {
    method onOutsideClick (line 51) | onOutsideClick(event) {
    method onEscape (line 57) | onEscape(event) {
    method onFocusOut (line 65) | onFocusOut(event) {

FILE: bookmarks/frontend/components/filter-drawer.js
  class FilterDrawerTrigger (line 6) | class FilterDrawerTrigger extends HeadlessElement {
    method init (line 7) | init() {
    method onClick (line 12) | onClick() {
  class FilterDrawer (line 20) | class FilterDrawer extends Modal {
    method connectedCallback (line 21) | connectedCallback() {
    method disconnectedCallback (line 71) | disconnectedCallback() {
    method mapHeading (line 77) | mapHeading(container, from, to) {
    method teleport (line 86) | teleport() {
    method teleportBack (line 93) | teleportBack() {
    method doClose (line 100) | doClose() {

FILE: bookmarks/frontend/components/form.js
  class Form (line 3) | class Form extends HeadlessElement {
    method constructor (line 4) | constructor() {
    method init (line 10) | init() {
    method disconnectedCallback (line 23) | disconnectedCallback() {
    method onChange (line 29) | onChange(event) {
    method onKeyDown (line 35) | onKeyDown(event) {
    method initFormReset (line 48) | initFormReset() {
    method resetForm (line 59) | resetForm() {

FILE: bookmarks/frontend/components/modal.js
  class Modal (line 4) | class Modal extends HeadlessElement {
    method init (line 5) | init() {
    method disconnectedCallback (line 20) | disconnectedCallback() {
    method setupScrollLock (line 25) | setupScrollLock() {
    method removeScrollLock (line 29) | removeScrollLock() {
    method onKeyDown (line 33) | onKeyDown(event) {
    method onClose (line 50) | onClose(event) {
    method doClose (line 64) | doClose() {

FILE: bookmarks/frontend/components/search-autocomplete.js
  class SearchAutocomplete (line 14) | class SearchAutocomplete extends TurboLitElement {
    method constructor (line 30) | constructor() {
    method firstUpdated (line 52) | firstUpdated() {
    method disconnectedCallback (line 67) | disconnectedCallback() {
    method handleFocus (line 72) | handleFocus() {
    method handleBlur (line 76) | handleBlur() {
    method handleInput (line 81) | handleInput(e) {
    method handleKeyDown (line 86) | handleKeyDown(e) {
    method open (line 118) | open() {
    method close (line 123) | close() {
    method hasSuggestions (line 130) | hasSuggestions() {
    method loadSuggestions (line 134) | async loadSuggestions() {
    method updateSuggestions (line 208) | updateSuggestions(recentSearches, bookmarks, tagSuggestions) {
    method completeSuggestion (line 220) | completeSuggestion(suggestion) {
    method updateSelection (line 240) | updateSelection(dir) {
    method renderSuggestions (line 258) | renderSuggestions(suggestions, title) {
    method render (line 285) | render() {

FILE: bookmarks/frontend/components/tag-autocomplete.js
  class TagAutocomplete (line 7) | class TagAutocomplete extends TurboLitElement {
    method constructor (line 22) | constructor() {
    method firstUpdated (line 38) | firstUpdated() {
    method disconnectedCallback (line 49) | disconnectedCallback() {
    method handleFocus (line 54) | handleFocus() {
    method handleBlur (line 58) | handleBlur() {
    method handleInput (line 63) | async handleInput(e) {
    method handleKeyDown (line 82) | handleKeyDown(e) {
    method open (line 102) | open() {
    method close (line 108) | close() {
    method complete (line 115) | complete(suggestion) {
    method updateSelection (line 128) | updateSelection(dir) {
    method render (line 149) | render() {

FILE: bookmarks/frontend/components/upload-button.js
  class UploadButton (line 3) | class UploadButton extends HeadlessElement {
    method init (line 4) | init() {
    method onClick (line 15) | onClick(event) {
    method onChange (line 20) | onChange() {

FILE: bookmarks/frontend/utils/element.js
  class HeadlessElement (line 12) | class HeadlessElement extends HTMLElement {
    method connectedCallback (line 13) | connectedCallback() {
    method init (line 27) | init() {
  class TurboLitElement (line 53) | class TurboLitElement extends LitElement {
    method constructor (line 54) | constructor() {
    method createRenderRoot (line 59) | createRenderRoot() {
    method connectedCallback (line 63) | connectedCallback() {
    method disconnectedCallback (line 68) | disconnectedCallback() {
    method __prepareForCache (line 73) | __prepareForCache() {

FILE: bookmarks/frontend/utils/focus.js
  function isKeyboardActive (line 19) | function isKeyboardActive() {
  class FocusTrapController (line 23) | class FocusTrapController {
    method constructor (line 24) | constructor(element) {
    method destroy (line 39) | destroy() {
    method onKeyDown (line 43) | onKeyDown(event) {
  function setAfterPageLoadFocusTarget (line 64) | function setAfterPageLoadFocusTarget(...targets) {
  function programmaticFocus (line 68) | function programmaticFocus(element) {

FILE: bookmarks/frontend/utils/input.js
  function debounce (line 1) | function debounce(callback, delay = 250) {
  function clampText (line 12) | function clampText(text, maxChars = 30) {
  function getCurrentWordBounds (line 18) | function getCurrentWordBounds(input) {
  function getCurrentWord (line 33) | function getCurrentWord(input) {

FILE: bookmarks/frontend/utils/position-controller.js
  class PositionController (line 10) | class PositionController {
    method constructor (line 11) | constructor(options) {
    method enable (line 21) | enable() {
    method disable (line 29) | disable() {
    method updatePosition (line 36) | updatePosition() {

FILE: bookmarks/frontend/utils/search-history.js
  constant SEARCH_HISTORY_KEY (line 1) | const SEARCH_HISTORY_KEY = "searchHistory";
  constant MAX_ENTRIES (line 2) | const MAX_ENTRIES = 30;
  class SearchHistory (line 4) | class SearchHistory {
    method getHistory (line 5) | getHistory() {
    method pushCurrent (line 14) | pushCurrent() {
    method push (line 25) | push(search) {
    method getRecentSearches (line 42) | getRecentSearches(query, max) {

FILE: bookmarks/frontend/utils/tag-cache.js
  class TagCache (line 3) | class TagCache {
    method constructor (line 4) | constructor(api) {
    method getTags (line 13) | getTags() {

FILE: bookmarks/management/commands/backup.py
  class Command (line 7) | class Command(BaseCommand):
    method add_arguments (line 10) | def add_arguments(self, parser):
    method handle (line 13) | def handle(self, *args, **options):

FILE: bookmarks/management/commands/create_initial_superuser.py
  class Command (line 10) | class Command(BaseCommand):
    method handle (line 13) | def handle(self, *args, **options):

FILE: bookmarks/management/commands/enable_wal.py
  class Command (line 10) | class Command(BaseCommand):
    method handle (line 13) | def handle(self, *args, **options):

FILE: bookmarks/management/commands/ensure_superuser.py
  class Command (line 5) | class Command(BaseCommand):
    method add_arguments (line 8) | def add_arguments(self, parser):
    method handle (line 13) | def handle(self, *args, **options):

FILE: bookmarks/management/commands/full_backup.py
  class Command (line 9) | class Command(BaseCommand):
    method add_arguments (line 12) | def add_arguments(self, parser):
    method handle (line 15) | def handle(self, *args, **options):
    method backup_database (line 66) | def backup_database(self, backup_db_file):

FILE: bookmarks/management/commands/generate_secret_key.py
  class Command (line 10) | class Command(BaseCommand):
    method handle (line 13) | def handle(self, *args, **options):

FILE: bookmarks/management/commands/import_netscape.py
  class Command (line 7) | class Command(BaseCommand):
    method add_arguments (line 10) | def add_arguments(self, parser):
    method handle (line 16) | def handle(self, *args, **kwargs):

FILE: bookmarks/management/commands/migrate_tasks.py
  class Command (line 9) | class Command(BaseCommand):
    method handle (line 12) | def handle(self, *args, **options):

FILE: bookmarks/middlewares.py
  class CustomRemoteUserMiddleware (line 7) | class CustomRemoteUserMiddleware(RemoteUserMiddleware):
  class LinkdingMiddleware (line 17) | class LinkdingMiddleware:
    method __init__ (line 18) | def __init__(self, get_response):
    method __call__ (line 21) | def __call__(self, request):

FILE: bookmarks/migrations/0001_initial.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0002_auto_20190629_2303.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0003_auto_20200913_0656.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0004_auto_20200926_1028.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0005_auto_20210103_1212.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0006_bookmark_is_archived.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0007_userprofile.py
  function forwards (line 8) | def forwards(apps, schema_editor):
  function reverse (line 20) | def reverse(apps, schema_editor):
  class Migration (line 24) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0008_userprofile_bookmark_date_display.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0009_bookmark_web_archive_snapshot_url.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0010_userprofile_bookmark_link_target.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0011_userprofile_web_archive_integration.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0012_toast.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0013_web_archive_optin_toast.py
  function forwards (line 11) | def forwards(apps, schema_editor):
  function reverse (line 21) | def reverse(apps, schema_editor):
  class Migration (line 25) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0014_alter_bookmark_unread.py
  function forwards (line 6) | def forwards(apps, schema_editor):
  function reverse (line 11) | def reverse(apps, schema_editor):
  class Migration (line 15) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0015_feedtoken.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0016_bookmark_shared.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0017_userprofile_enable_sharing.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0018_bookmark_favicon_file.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0019_userprofile_enable_favicons.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0020_userprofile_tag_search.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0021_userprofile_display_url.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0022_bookmark_notes.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0023_userprofile_permanent_notes.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0024_userprofile_enable_public_sharing.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0025_userprofile_search_preferences.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0026_userprofile_custom_css.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0027_userprofile_bookmark_description_display_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0028_userprofile_display_archive_bookmark_action_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0029_bookmark_list_actions_toast.py
  function forwards (line 11) | def forwards(apps, schema_editor):
  function reverse (line 21) | def reverse(apps, schema_editor):
  class Migration (line 25) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0030_bookmarkasset.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0031_userprofile_enable_automatic_html_snapshots.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0032_html_snapshots_hint_toast.py
  function forwards (line 11) | def forwards(apps, schema_editor):
  function reverse (line 21) | def reverse(apps, schema_editor):
  class Migration (line 25) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0033_userprofile_default_mark_unread.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0034_bookmark_preview_image_file_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0035_userprofile_tag_grouping.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0036_userprofile_auto_tagging_rules.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0037_globalsettings.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0038_globalsettings_guest_profile_user.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0039_globalsettings_enable_link_prefetch.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0040_userprofile_items_per_page_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0041_merge_metadata.py
  function forwards (line 10) | def forwards(apps, schema_editor):
  function reverse (line 24) | def reverse(apps, schema_editor):
  class Migration (line 28) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0042_userprofile_custom_css_hash.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0043_userprofile_collapse_side_panel.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0044_bookmark_latest_snapshot.py
  function forwards (line 8) | def forwards(apps, schema_editor):
  function reverse (line 23) | def reverse(apps, schema_editor):
  class Migration (line 27) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0045_userprofile_hide_bundles_bookmarkbundle.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0046_add_url_normalized_field.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0047_populate_url_normalized_field.py
  function populate_url_normalized (line 8) | def populate_url_normalized(apps, schema_editor):
  function reverse_populate_url_normalized (line 23) | def reverse_populate_url_normalized(apps, schema_editor):
  class Migration (line 28) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0048_userprofile_default_mark_shared.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0049_userprofile_legacy_search.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0050_new_search_toast.py
  function forwards (line 11) | def forwards(apps, schema_editor):
  function reverse (line 21) | def reverse(apps, schema_editor):
  class Migration (line 25) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0051_fix_normalized_url.py
  function fix_url_normalized (line 8) | def fix_url_normalized(apps, schema_editor):
  function reverse_fix_url_normalized (line 20) | def reverse_fix_url_normalized(apps, schema_editor):
  class Migration (line 24) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0052_apitoken.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0053_migrate_api_tokens.py
  function migrate_tokens_forward (line 5) | def migrate_tokens_forward(apps, schema_editor):
  function migrate_tokens_reverse (line 18) | def migrate_tokens_reverse(apps, schema_editor):
  class Migration (line 23) | class Migration(migrations.Migration):

FILE: bookmarks/migrations/0054_bookmarkbundle_filter_shared_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: bookmarks/models.py
  class Tag (line 21) | class Tag(models.Model):
    method __str__ (line 26) | def __str__(self):
  function sanitize_tag_name (line 30) | def sanitize_tag_name(tag_name: str):
  function parse_tag_string (line 36) | def parse_tag_string(tag_string: str, delimiter: str = ","):
  function build_tag_string (line 49) | def build_tag_string(tag_names: list[str], delimiter: str = ","):
  class Bookmark (line 53) | class Bookmark(models.Model):
    method resolved_title (line 83) | def resolved_title(self):
    method resolved_description (line 90) | def resolved_description(self):
    method tag_names (line 94) | def tag_names(self):
    method save (line 98) | def save(self, *args, **kwargs):
    method __str__ (line 102) | def __str__(self):
    method query_existing (line 106) | def query_existing(owner: User, url: str) -> models.QuerySet:
  function bookmark_deleted (line 117) | def bookmark_deleted(sender, instance, **kwargs):
  class BookmarkAsset (line 129) | class BookmarkAsset(models.Model):
    method download_name (line 151) | def download_name(self):
    method save (line 158) | def save(self, *args, **kwargs):
    method __str__ (line 168) | def __str__(self):
  function bookmark_asset_deleted (line 173) | def bookmark_asset_deleted(sender, instance, **kwargs):
  class BookmarkBundle (line 183) | class BookmarkBundle(models.Model):
    method __str__ (line 220) | def __str__(self):
  class BookmarkSearch (line 224) | class BookmarkSearch:
    method __init__ (line 260) | def __init__(
    method is_modified (line 287) | def is_modified(self, param):
    method modified_params (line 292) | def modified_params(self):
    method modified_preferences (line 296) | def modified_preferences(self):
    method has_modifications (line 304) | def has_modifications(self):
    method has_modified_preferences (line 308) | def has_modified_preferences(self):
    method query_params (line 312) | def query_params(self):
    method preferences_dict (line 323) | def preferences_dict(self):
    method from_request (line 329) | def from_request(request: any, query_dict: QueryDict, preferences: dic...
  class UserProfile (line 346) | class UserProfile(models.Model):
    method save (line 462) | def save(self, *args, **kwargs):
  function create_user_profile (line 473) | def create_user_profile(sender, instance, created, **kwargs):
  function save_user_profile (line 479) | def save_user_profile(sender, instance, **kwargs):
  class Toast (line 483) | class Toast(models.Model):
  class FeedToken (line 490) | class FeedToken(models.Model):
    method save (line 503) | def save(self, *args, **kwargs):
    method generate_key (line 509) | def generate_key(cls):
    method __str__ (line 512) | def __str__(self):
  class ApiToken (line 516) | class ApiToken(models.Model):
    method save (line 526) | def save(self, *args, **kwargs):
    method generate_key (line 532) | def generate_key(cls):
    method __str__ (line 535) | def __str__(self):
  class GlobalSettings (line 539) | class GlobalSettings(models.Model):
    method get (line 559) | def get(cls):
    method save (line 566) | def save(self, *args, **kwargs):

FILE: bookmarks/queries.py
  function query_bookmarks (line 33) | def query_bookmarks(
  function query_archived_bookmarks (line 41) | def query_archived_bookmarks(
  function query_shared_bookmarks (line 47) | def query_shared_bookmarks(
  function _convert_ast_to_q_object (line 60) | def _convert_ast_to_q_object(ast_node: SearchExpression, profile: UserPr...
  function _filter_search_query (line 122) | def _filter_search_query(
  function _filter_search_query_legacy (line 139) | def _filter_search_query_legacy(
  function _filter_bundle (line 176) | def _filter_bundle(query_set: QuerySet, bundle: BookmarkBundle) -> Query...
  function _base_bookmarks_query (line 227) | def _base_bookmarks_query(
  function query_bookmark_tags (line 308) | def query_bookmark_tags(
  function query_archived_bookmark_tags (line 318) | def query_archived_bookmark_tags(
  function query_shared_bookmark_tags (line 328) | def query_shared_bookmark_tags(
  function query_shared_bookmark_users (line 341) | def query_shared_bookmark_users(
  function get_user_tags (line 351) | def get_user_tags(user: User):
  function get_tags_for_query (line 355) | def get_tags_for_query(user: User, profile: UserProfile, query: str) -> ...
  function get_shared_tags_for_query (line 368) | def get_shared_tags_for_query(
  function parse_query_string (line 394) | def parse_query_string(query_string):

FILE: bookmarks/services/assets.py
  class PdfTooLargeError (line 24) | class PdfTooLargeError(Exception):
  function create_snapshot_asset (line 28) | def create_snapshot_asset(bookmark: Bookmark) -> BookmarkAsset:
  function create_snapshot (line 40) | def create_snapshot(asset: BookmarkAsset):
  function _create_html_snapshot (line 55) | def _create_html_snapshot(asset: BookmarkAsset):
  function _create_pdf_snapshot (line 88) | def _create_pdf_snapshot(asset: BookmarkAsset):
  function upload_snapshot (line 145) | def upload_snapshot(bookmark: Bookmark, html: bytes):
  function upload_asset (line 170) | def upload_asset(bookmark: Bookmark, upload_file: UploadedFile):
  function remove_asset (line 221) | def remove_asset(asset: BookmarkAsset):
  function _generate_asset_filename (line 243) | def _generate_asset_filename(

FILE: bookmarks/services/auto_tagging.py
  function get_tags (line 7) | def get_tags(script: str, url: str):
  function _path_matches (line 59) | def _path_matches(expected_path: str, actual_path: str) -> bool:
  function _domains_matches (line 63) | def _domains_matches(expected_domain: str, actual_domain: str) -> bool:
  function _qs_matches (line 70) | def _qs_matches(expected_qs: str, actual_qs: str) -> bool:
  function _fragment_matches (line 84) | def _fragment_matches(expected_fragment: str, actual_fragment: str) -> b...

FILE: bookmarks/services/bookmarks.py
  function create_bookmark (line 12) | def create_bookmark(
  function update_bookmark (line 55) | def update_bookmark(bookmark: Bookmark, tag_string, current_user: User):
  function enhance_with_website_metadata (line 76) | def enhance_with_website_metadata(bookmark: Bookmark):
  function archive_bookmark (line 87) | def archive_bookmark(bookmark: Bookmark):
  function archive_bookmarks (line 94) | def archive_bookmarks(bookmark_ids: [int | str], current_user: User):
  function unarchive_bookmark (line 102) | def unarchive_bookmark(bookmark: Bookmark):
  function unarchive_bookmarks (line 109) | def unarchive_bookmarks(bookmark_ids: [int | str], current_user: User):
  function delete_bookmarks (line 117) | def delete_bookmarks(bookmark_ids: [int | str], current_user: User):
  function tag_bookmarks (line 123) | def tag_bookmarks(bookmark_ids: [int | str], tag_string: str, current_us...
  function untag_bookmarks (line 146) | def untag_bookmarks(bookmark_ids: [int | str], tag_string: str, current_...
  function mark_bookmarks_as_read (line 166) | def mark_bookmarks_as_read(bookmark_ids: [int | str], current_user: User):
  function mark_bookmarks_as_unread (line 174) | def mark_bookmarks_as_unread(bookmark_ids: [int | str], current_user: Us...
  function share_bookmarks (line 182) | def share_bookmarks(bookmark_ids: [int | str], current_user: User):
  function unshare_bookmarks (line 190) | def unshare_bookmarks(bookmark_ids: [int | str], current_user: User):
  function refresh_bookmarks_metadata (line 198) | def refresh_bookmarks_metadata(bookmark_ids: [int | str], current_user: ...
  function create_html_snapshots (line 209) | def create_html_snapshots(bookmark_ids: list[int | str], current_user: U...
  function _merge_bookmark_data (line 218) | def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
  function _update_bookmark_tags (line 226) | def _update_bookmark_tags(bookmark: Bookmark, tag_string: str, user: User):
  function _sanitize_id_list (line 247) | def _sanitize_id_list(bookmark_ids: [int | str]) -> [int]:

FILE: bookmarks/services/bundles.py
  function create_bundle (line 6) | def create_bundle(bundle: BookmarkBundle, current_user: User):
  function move_bundle (line 17) | def move_bundle(bundle_to_move: BookmarkBundle, new_order: int):
  function delete_bundle (line 31) | def delete_bundle(bundle: BookmarkBundle):

FILE: bookmarks/services/exporter.py
  function export_netscape_html (line 8) | def export_netscape_html(bookmarks: list[Bookmark]):
  function append_header (line 18) | def append_header(doc: BookmarkDocument):
  function append_list_start (line 25) | def append_list_start(doc: BookmarkDocument):
  function append_bookmark (line 29) | def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
  function append_list_end (line 52) | def append_list_end(doc: BookmarkDocument):

FILE: bookmarks/services/favicon_loader.py
  function _ensure_favicon_folder (line 21) | def _ensure_favicon_folder():
  function _url_to_filename (line 25) | def _url_to_filename(url: str) -> str:
  function _get_url_parameters (line 29) | def _get_url_parameters(url: str) -> dict:
  function _get_favicon_path (line 39) | def _get_favicon_path(favicon_file: str) -> Path:
  function _check_existing_favicon (line 43) | def _check_existing_favicon(favicon_name: str):
  function _is_stale (line 54) | def _is_stale(path: Path) -> bool:
  function load_favicon (line 60) | def load_favicon(url: str) -> str:

FILE: bookmarks/services/importer.py
  class ImportResult (line 16) | class ImportResult:
  class ImportOptions (line 23) | class ImportOptions:
  class TagCache (line 27) | class TagCache:
    method __init__ (line 28) | def __init__(self, user: User):
    method get (line 36) | def get(self, tag_name: str):
    method get_all (line 43) | def get_all(self, tag_names: list[str]):
    method put (line 54) | def put(self, tag: Tag):
  function import_netscape_html (line 58) | def import_netscape_html(
  function _create_missing_tags (line 95) | def _create_missing_tags(netscape_bookmarks: list[NetscapeBookmark], use...
  function _get_batches (line 118) | def _get_batches(items: list, batch_size: int):
  function _import_batch (line 132) | def _import_batch(
  function _copy_bookmark_data (line 236) | def _copy_bookmark_data(

FILE: bookmarks/services/monolith.py
  class MonolithError (line 9) | class MonolithError(Exception):
  function create_snapshot (line 16) | def create_snapshot(url: str, filepath: str):

FILE: bookmarks/services/parser.py
  class NetscapeBookmark (line 9) | class NetscapeBookmark:
  class BookmarkParser (line 22) | class BookmarkParser(HTMLParser):
    method __init__ (line 23) | def __init__(self):
    method handle_starttag (line 39) | def handle_starttag(self, tag: str, attrs: list):
    method handle_endtag (line 45) | def handle_endtag(self, tag: str):
    method handle_data (line 51) | def handle_data(self, data):
    method handle_end_dl (line 56) | def handle_end_dl(self):
    method handle_start_dt (line 59) | def handle_start_dt(self, attrs: dict[str, str]):
    method handle_start_a (line 62) | def handle_start_a(self, attrs: dict[str, str]):
    method handle_a_data (line 83) | def handle_a_data(self, data):
    method handle_dd_data (line 86) | def handle_dd_data(self, data):
    method add_bookmark (line 92) | def add_bookmark(self):
  function parse (line 110) | def parse(html: str) -> list[NetscapeBookmark]:

FILE: bookmarks/services/preview_image_loader.py
  function _ensure_preview_folder (line 15) | def _ensure_preview_folder():
  function _url_to_filename (line 19) | def _url_to_filename(preview_image: str) -> str:
  function _get_image_path (line 23) | def _get_image_path(preview_image_file: str) -> Path:
  function load_preview_image (line 27) | def load_preview_image(url: str) -> str | None:

FILE: bookmarks/services/search_query_parser.py
  class TokenType (line 7) | class TokenType(Enum):
  class Token (line 20) | class Token:
  class SearchQueryTokenizer (line 26) | class SearchQueryTokenizer:
    method __init__ (line 27) | def __init__(self, query: str):
    method advance (line 32) | def advance(self):
    method skip_whitespace (line 40) | def skip_whitespace(self):
    method read_term (line 45) | def read_term(self) -> str:
    method read_quoted_string (line 59) | def read_quoted_string(self, quote_char: str) -> str:
    method read_tag (line 96) | def read_tag(self) -> str:
    method read_special_keyword (line 111) | def read_special_keyword(self) -> str:
    method tokenize (line 126) | def tokenize(self) -> list[Token]:
  class SearchExpression (line 179) | class SearchExpression:
  class TermExpression (line 184) | class TermExpression(SearchExpression):
  class TagExpression (line 189) | class TagExpression(SearchExpression):
  class SpecialKeywordExpression (line 194) | class SpecialKeywordExpression(SearchExpression):
  class AndExpression (line 199) | class AndExpression(SearchExpression):
  class OrExpression (line 205) | class OrExpression(SearchExpression):
  class NotExpression (line 211) | class NotExpression(SearchExpression):
  class SearchQueryParseError (line 215) | class SearchQueryParseError(Exception):
    method __init__ (line 216) | def __init__(self, message: str, position: int):
  class SearchQueryParser (line 222) | class SearchQueryParser:
    method __init__ (line 223) | def __init__(self, tokens: list[Token]):
    method advance (line 228) | def advance(self):
    method consume (line 234) | def consume(self, expected_type: TokenType) -> Token:
    method parse (line 246) | def parse(self) -> SearchExpression | None:
    method parse_or_expression (line 263) | def parse_or_expression(self) -> SearchExpression:
    method parse_and_expression (line 274) | def parse_and_expression(self) -> SearchExpression:
    method parse_not_expression (line 294) | def parse_not_expression(self) -> SearchExpression:
    method parse_primary_expression (line 303) | def parse_primary_expression(self) -> SearchExpression:
  function parse_search_query (line 329) | def parse_search_query(query: str) -> SearchExpression | None:
  function _needs_parentheses (line 339) | def _needs_parentheses(expr: SearchExpression, parent_type: type) -> bool:
  function _is_simple_expression (line 348) | def _is_simple_expression(expr: SearchExpression) -> bool:
  function _expression_to_string (line 353) | def _expression_to_string(expr: SearchExpression, parent_type: type = No...
  function expression_to_string (line 413) | def expression_to_string(expr: SearchExpression | None) -> str:
  function _strip_tag_from_expression (line 419) | def _strip_tag_from_expression(
  function strip_tag_from_query (line 488) | def strip_tag_from_query(
  function _extract_tag_names_from_expression (line 511) | def _extract_tag_names_from_expression(
  function extract_tag_names_from_query (line 545) | def extract_tag_names_from_query(

FILE: bookmarks/services/singlefile.py
  class SingleFileError (line 10) | class SingleFileError(Exception):
  function create_snapshot (line 17) | def create_snapshot(url: str, filepath: str):

FILE: bookmarks/services/tags.py
  function get_or_create_tags (line 13) | def get_or_create_tags(tag_names: list[str], user: User):
  function get_or_create_tag (line 18) | def get_or_create_tag(name: str, user: User):

FILE: bookmarks/services/tasks.py
  function task (line 28) | def task(retries=5, retry_delay=15, retry_backoff=4):
  function is_web_archive_integration_active (line 50) | def is_web_archive_integration_active(user: User) -> bool:
  function create_web_archive_snapshot (line 60) | def create_web_archive_snapshot(user: User, bookmark: Bookmark, force_up...
  function _create_snapshot (line 65) | def _create_snapshot(bookmark: Bookmark):
  function _create_web_archive_snapshot_task (line 77) | def _create_web_archive_snapshot_task(bookmark_id: int, force_update: bo...
  function _load_web_archive_snapshot_task (line 103) | def _load_web_archive_snapshot_task(bookmark_id: int):
  function _schedule_bookmarks_without_snapshots_task (line 110) | def _schedule_bookmarks_without_snapshots_task(user_id: int):
  function is_favicon_feature_active (line 116) | def is_favicon_feature_active(user: User) -> bool:
  function is_preview_feature_active (line 122) | def is_preview_feature_active(user: User) -> bool:
  function load_favicon (line 128) | def load_favicon(user: User, bookmark: Bookmark):
  function _load_favicon_task (line 134) | def _load_favicon_task(bookmark_id: int):
  function schedule_bookmarks_without_favicons (line 152) | def schedule_bookmarks_without_favicons(user: User):
  function _schedule_bookmarks_without_favicons_task (line 158) | def _schedule_bookmarks_without_favicons_task(user_id: int):
  function schedule_refresh_favicons (line 168) | def schedule_refresh_favicons(user: User):
  function _schedule_refresh_favicons_task (line 174) | def _schedule_refresh_favicons_task(user_id: int):
  function load_preview_image (line 183) | def load_preview_image(user: User, bookmark: Bookmark):
  function _load_preview_image_task (line 189) | def _load_preview_image_task(bookmark_id: int):
  function schedule_bookmarks_without_previews (line 207) | def schedule_bookmarks_without_previews(user: User):
  function _schedule_bookmarks_without_previews_task (line 213) | def _schedule_bookmarks_without_previews_task(user_id: int):
  function refresh_metadata (line 228) | def refresh_metadata(bookmark: Bookmark):
  function _refresh_metadata_task (line 234) | def _refresh_metadata_task(bookmark_id: int):
  function is_html_snapshot_feature_active (line 253) | def is_html_snapshot_feature_active() -> bool:
  function create_html_snapshot (line 257) | def create_html_snapshot(bookmark: Bookmark):
  function create_html_snapshots (line 265) | def create_html_snapshots(bookmark_list: list[Bookmark]):
  function _schedule_html_snapshots_task (line 284) | def _schedule_html_snapshots_task():
  function _create_html_snapshot_task (line 294) | def _create_html_snapshot_task(asset_id: int):
  function create_missing_html_snapshots (line 315) | def create_missing_html_snapshots(user: User) -> int:

FILE: bookmarks/services/wayback.py
  function generate_fallback_webarchive_url (line 6) | def generate_fallback_webarchive_url(

FILE: bookmarks/services/website_loader.py
  class WebsiteMetadata (line 15) | class WebsiteMetadata:
    method to_dict (line 21) | def to_dict(self):
  function load_website_metadata (line 30) | def load_website_metadata(url: str, ignore_cache: bool = False):
  function _load_website_metadata_cached (line 39) | def _load_website_metadata_cached(url: str):
  function _load_website_metadata (line 43) | def _load_website_metadata(url: str):
  function load_page (line 96) | def load_page(url: str):
  function fake_request_headers (line 134) | def fake_request_headers():
  function detect_content_type (line 144) | def detect_content_type(url: str, timeout: int = 10) -> str | None:
  function is_pdf_content_type (line 176) | def is_pdf_content_type(content_type: str | None) -> bool:

FILE: bookmarks/signals.py
  function extend_sqlite (line 7) | def extend_sqlite(connection=None, **kwargs):

FILE: bookmarks/static/live-reload.js
  constant RELOAD_URL (line 1) | const RELOAD_URL = "/live_reload";
  function connect (line 6) | function connect() {

FILE: bookmarks/static/vendor/Readability.js
  function Readability (line 27) | function Readability(doc, options) {
  function toAbsoluteURI (line 368) | function toAbsoluteURI(uri) {
  function wordCount (line 477) | function wordCount(str) {

FILE: bookmarks/templatetags/bookmarks.py
  function bookmark_search (line 12) | def bookmark_search(context, search: BookmarkSearch, mode: str = ""):

FILE: bookmarks/templatetags/pagination.py
  function pagination (line 13) | def pagination(context, page: Page):
  function get_visible_page_numbers (line 59) | def get_visible_page_numbers(current_page_number: int, num_pages: int) -...
  function _generate_link (line 98) | def _generate_link(base_url: str, query_params: QueryDict, page_number: ...

FILE: bookmarks/templatetags/shared.py
  function update_query_string (line 18) | def update_query_string(context, **kwargs):
  function replace_query_param (line 29) | def replace_query_param(context, **kwargs):
  function first_char (line 41) | def first_char(text):
  function remaining_chars (line 46) | def remaining_chars(text, index):
  function humanize_absolute_date (line 51) | def humanize_absolute_date(value):
  function humanize_relative_date (line 58) | def humanize_relative_date(value):
  function model_to_dict_filter (line 65) | def model_to_dict_filter(value):
  function htmlmin (line 71) | def htmlmin(parser, token):
  class HtmlMinNode (line 77) | class HtmlMinNode(template.Node):
    method __init__ (line 78) | def __init__(self, nodelist):
    method render (line 81) | def render(self, context):
  function schemeless_urls_to_https (line 89) | def schemeless_urls_to_https(attrs, _new):
  function render_markdown (line 106) | def render_markdown(context, markdown_text):
  function append_attr (line 122) | def append_attr(widget, attr, value):
  function formlabel (line 131) | def formlabel(field, label_text):
  function formfield (line 138) | def formfield(field, **kwargs):
  function formhelp (line 166) | def formhelp(parser, token):
  class FormHelpNode (line 178) | class FormHelpNode(template.Node):
    method __init__ (line 179) | def __init__(self, nodelist, field_var):
    method render (line 183) | def render(self, context):

FILE: bookmarks/tests/helpers.py
  class BookmarkFactoryMixin (line 28) | class BookmarkFactoryMixin:
    method setup_temp_assets_dir (line 31) | def setup_temp_assets_dir(self):
    method cleanup_temp_assets_dir (line 37) | def cleanup_temp_assets_dir(self):
    method get_or_create_test_user (line 41) | def get_or_create_test_user(self):
    method setup_superuser (line 49) | def setup_superuser(self):
    method setup_bookmark (line 54) | def setup_bookmark(
    method setup_numbered_bookmarks (line 105) | def setup_numbered_bookmarks(
    method get_numbered_bookmark (line 171) | def get_numbered_bookmark(self, title: str):
    method setup_bundle (line 174) | def setup_bundle(
    method setup_asset (line 205) | def setup_asset(
    method setup_asset_file (line 237) | def setup_asset_file(self, asset: BookmarkAsset, file_content: str = "...
    method read_asset_file (line 246) | def read_asset_file(self, asset: BookmarkAsset):
    method get_asset_filesize (line 256) | def get_asset_filesize(self, asset: BookmarkAsset):
    method has_asset_file (line 260) | def has_asset_file(self, asset: BookmarkAsset):
    method setup_tag (line 264) | def setup_tag(self, user: User = None, name: str = ""):
    method setup_user (line 273) | def setup_user(
    method setup_api_token (line 287) | def setup_api_token(self, user: User = None, name: str = ""):
    method get_tags_from_bookmarks (line 296) | def get_tags_from_bookmarks(self, bookmarks: list[Bookmark]):
    method get_random_string (line 302) | def get_random_string(self, length: int = 32):
  class HtmlTestMixin (line 306) | class HtmlTestMixin:
    method make_soup (line 307) | def make_soup(self, html: str):
  class BookmarkListTestMixin (line 311) | class BookmarkListTestMixin(TestCase, HtmlTestMixin):
    method assertVisibleBookmarks (line 312) | def assertVisibleBookmarks(
    method assertInvisibleBookmarks (line 330) | def assertInvisibleBookmarks(
  class TagCloudTestMixin (line 342) | class TagCloudTestMixin(TestCase, HtmlTestMixin):
    method assertVisibleTags (line 343) | def assertVisibleTags(self, response, tags: list[Tag]):
    method assertInvisibleTags (line 356) | def assertInvisibleTags(self, response, tags: list[Tag]):
    method assertSelectedTags (line 365) | def assertSelectedTags(self, response, tags: list[Tag]):
  class LinkdingApiTestCase (line 380) | class LinkdingApiTestCase(APITestCase):
    method authenticate (line 381) | def authenticate(self):
    method get (line 387) | def get(self, url, expected_status_code=status.HTTP_200_OK):
    method post (line 392) | def post(self, url, data=None, expected_status_code=status.HTTP_200_OK):
    method put (line 397) | def put(self, url, data=None, expected_status_code=status.HTTP_200_OK):
    method patch (line 402) | def patch(self, url, data=None, expected_status_code=status.HTTP_200_OK):
    method delete (line 407) | def delete(self, url, expected_status_code=status.HTTP_200_OK):
  class BookmarkHtmlTag (line 413) | class BookmarkHtmlTag:
    method __init__ (line 414) | def __init__(
  class ImportTestMixin (line 435) | class ImportTestMixin:
    method render_tag (line 436) | def render_tag(self, tag: BookmarkHtmlTag):
    method render_html (line 450) | def render_html(self, tags: list[BookmarkHtmlTag] = None, tags_html: s...
  function random_sentence (line 489) | def random_sentence(num_words: int = None, including_word: str = ""):
  function disable_logging (line 500) | def disable_logging(f):
  function collapse_whitespace (line 511) | def collapse_whitespace(text: str):

FILE: bookmarks/tests/test_app_options.py
  class AppOptionsTestCase (line 8) | class AppOptionsTestCase(TestCase):
    method setUp (line 9) | def setUp(self) -> None:
    method test_empty_csrf_trusted_origins (line 12) | def test_empty_csrf_trusted_origins(self):
    method test_single_csrf_trusted_origin (line 20) | def test_single_csrf_trusted_origin(self):
    method test_multiple_csrf_trusted_origin (line 34) | def test_multiple_csrf_trusted_origin(self):

FILE: bookmarks/tests/test_assets_service.py
  class AssetServiceTestCase (line 17) | class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 18) | def setUp(self) -> None:
    method tearDown (line 49) | def tearDown(self) -> None:
    method get_saved_snapshot_file (line 54) | def get_saved_snapshot_file(self):
    method create_mock_pdf_response (line 60) | def create_mock_pdf_response(self, content=None, content_length=None):
    method test_create_snapshot_asset (line 74) | def test_create_snapshot_asset(self):
    method test_create_snapshot (line 89) | def test_create_snapshot(self):
    method test_create_snapshot_failure (line 134) | def test_create_snapshot_failure(self):
    method test_create_snapshot_truncates_asset_file_name (line 149) | def test_create_snapshot_truncates_asset_file_name(self):
    method test_create_pdf_snapshot (line 164) | def test_create_pdf_snapshot(self):
    method test_create_snapshot_falls_back_to_singlefile_when_detection_fails (line 203) | def test_create_snapshot_falls_back_to_singlefile_when_detection_fails...
    method test_create_pdf_snapshot_fails_when_content_length_exceeds_limit (line 218) | def test_create_pdf_snapshot_fails_when_content_length_exceeds_limit(s...
    method test_create_pdf_snapshot_fails_when_download_exceeds_limit (line 238) | def test_create_pdf_snapshot_fails_when_download_exceeds_limit(self):
    method test_create_pdf_snapshot_failure (line 258) | def test_create_pdf_snapshot_failure(self):
    method test_upload_snapshot (line 277) | def test_upload_snapshot(self):
    method test_upload_snapshot_failure (line 310) | def test_upload_snapshot_failure(self):
    method test_upload_snapshot_truncates_asset_file_name (line 323) | def test_upload_snapshot_truncates_asset_file_name(self):
    method test_upload_asset (line 337) | def test_upload_asset(self):
    method test_upload_gzip_asset (line 374) | def test_upload_gzip_asset(self):
    method test_upload_asset_truncates_asset_file_name (line 411) | def test_upload_asset_truncates_asset_file_name(self):
    method test_upload_asset_failure (line 430) | def test_upload_asset_failure(self):
    method test_create_snapshot_updates_bookmark_latest_snapshot (line 444) | def test_create_snapshot_updates_bookmark_latest_snapshot(self):
    method test_upload_snapshot_updates_bookmark_latest_snapshot (line 462) | def test_upload_snapshot_updates_bookmark_latest_snapshot(self):
    method test_create_snapshot_failure_does_not_update_latest_snapshot (line 476) | def test_create_snapshot_failure_does_not_update_latest_snapshot(self):
    method test_upload_snapshot_failure_does_not_update_latest_snapshot (line 500) | def test_upload_snapshot_failure_does_not_update_latest_snapshot(self):
    method test_remove_latest_snapshot_updates_bookmark (line 519) | def test_remove_latest_snapshot_updates_bookmark(self):
    method test_remove_non_latest_snapshot_does_not_affect_bookmark (line 579) | def test_remove_non_latest_snapshot_does_not_affect_bookmark(self):
    method test_remove_asset (line 617) | def test_remove_asset(self):

FILE: bookmarks/tests/test_auth_api.py
  class AuthApiTestCase (line 7) | class AuthApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
    method authenticate (line 8) | def authenticate(self, keyword):
    method test_auth_with_token_keyword (line 12) | def test_auth_with_token_keyword(self):
    method test_auth_with_bearer_keyword (line 18) | def test_auth_with_bearer_keyword(self):
    method test_auth_with_unknown_keyword (line 24) | def test_auth_with_unknown_keyword(self):

FILE: bookmarks/tests/test_auth_proxy_support.py
  class AuthProxySupportTest (line 10) | class AuthProxySupportTest(TestCase):
    method test_auth_proxy_authentication (line 19) | def test_auth_proxy_authentication(self):
    method test_auth_proxy_with_custom_header (line 37) | def test_auth_proxy_with_custom_header(self):
    method test_auth_proxy_is_disabled_by_default (line 51) | def test_auth_proxy_is_disabled_by_default(self):

FILE: bookmarks/tests/test_auto_tagging.py
  class AutoTaggingTestCase (line 6) | class AutoTaggingTestCase(TestCase):
    method test_auto_tag_by_domain (line 7) | def test_auto_tag_by_domain(self):
    method test_auto_tag_by_domain_handles_invalid_urls (line 18) | def test_auto_tag_by_domain_handles_invalid_urls(self):
    method test_auto_tag_by_domain_works_with_port (line 32) | def test_auto_tag_by_domain_works_with_port(self):
    method test_auto_tag_by_domain_ignores_case (line 43) | def test_auto_tag_by_domain_ignores_case(self):
    method test_auto_tag_by_domain_should_add_all_tags (line 53) | def test_auto_tag_by_domain_should_add_all_tags(self):
    method test_auto_tag_by_domain_work_with_idn_domains (line 63) | def test_auto_tag_by_domain_work_with_idn_domains(self):
    method test_auto_tag_by_domain_and_path (line 82) | def test_auto_tag_by_domain_and_path(self):
    method test_auto_tag_by_domain_and_path_ignores_case (line 94) | def test_auto_tag_by_domain_and_path_ignores_case(self):
    method test_auto_tag_by_domain_and_path_matches_path_ltr (line 104) | def test_auto_tag_by_domain_and_path_matches_path_ltr(self):
    method test_auto_tag_by_domain_ignores_domain_in_path (line 116) | def test_auto_tag_by_domain_ignores_domain_in_path(self):
    method test_auto_tag_by_domain_includes_subdomains (line 126) | def test_auto_tag_by_domain_includes_subdomains(self):
    method test_auto_tag_by_domain_matches_domain_rtl (line 138) | def test_auto_tag_by_domain_matches_domain_rtl(self):
    method test_auto_tag_by_domain_ignores_schema (line 148) | def test_auto_tag_by_domain_ignores_schema(self):
    method test_auto_tag_by_domain_ignores_lines_with_no_tags (line 159) | def test_auto_tag_by_domain_ignores_lines_with_no_tags(self):
    method test_auto_tag_by_domain_path_and_qs (line 169) | def test_auto_tag_by_domain_path_and_qs(self):
    method test_auto_tag_by_domain_path_and_qs_with_empty_value (line 185) | def test_auto_tag_by_domain_path_and_qs_with_empty_value(self):
    method test_auto_tag_by_domain_path_and_qs_works_with_encoded_url (line 196) | def test_auto_tag_by_domain_path_and_qs_works_with_encoded_url(self):
    method test_auto_tag_with_url_fragment (line 207) | def test_auto_tag_with_url_fragment(self):
    method test_auto_tag_with_url_fragment_partial_match (line 218) | def test_auto_tag_with_url_fragment_partial_match(self):
    method test_auto_tag_with_url_fragment_ignores_case (line 228) | def test_auto_tag_with_url_fragment_ignores_case(self):
    method test_auto_tag_with_url_fragment_and_comment (line 238) | def test_auto_tag_with_url_fragment_and_comment(self):

FILE: bookmarks/tests/test_bookmark_action_view.py
  class BookmarkActionViewTestCase (line 19) | class BookmarkActionViewTestCase(
    method setUp (line 22) | def setUp(self) -> None:
    method assertBookmarksAreUnmodified (line 26) | def assertBookmarksAreUnmodified(self, bookmarks: [Bookmark]):
    method test_archive_should_archive_bookmark (line 35) | def test_archive_should_archive_bookmark(self):
    method test_can_only_archive_own_bookmarks (line 49) | def test_can_only_archive_own_bookmarks(self):
    method test_unarchive_should_unarchive_bookmark (line 67) | def test_unarchive_should_unarchive_bookmark(self):
    method test_unarchive_can_only_archive_own_bookmarks (line 80) | def test_unarchive_can_only_archive_own_bookmarks(self):
    method test_delete_should_delete_bookmark (line 97) | def test_delete_should_delete_bookmark(self):
    method test_delete_can_only_delete_own_bookmarks (line 109) | def test_delete_can_only_delete_own_bookmarks(self):
    method test_mark_as_read (line 124) | def test_mark_as_read(self):
    method test_unshare_should_unshare_bookmark (line 137) | def test_unshare_should_unshare_bookmark(self):
    method test_can_only_unshare_own_bookmarks (line 151) | def test_can_only_unshare_own_bookmarks(self):
    method test_create_html_snapshot (line 170) | def test_create_html_snapshot(self):
    method test_can_only_create_html_snapshot_for_own_bookmarks (line 184) | def test_can_only_create_html_snapshot_for_own_bookmarks(self):
    method test_upload_asset (line 197) | def test_upload_asset(self):
    method test_can_only_upload_asset_for_own_bookmarks (line 217) | def test_can_only_upload_asset_for_own_bookmarks(self):
    method test_upload_asset_disabled (line 233) | def test_upload_asset_disabled(self):
    method test_upload_asset_without_file (line 244) | def test_upload_asset_without_file(self):
    method test_remove_asset (line 253) | def test_remove_asset(self):
    method test_can_only_remove_own_asset (line 263) | def test_can_only_remove_own_asset(self):
    method test_update_state (line 274) | def test_update_state(self):
    method test_can_only_update_own_bookmark_state (line 293) | def test_can_only_update_own_bookmark_state(self):
    method test_bulk_archive (line 313) | def test_bulk_archive(self):
    method test_can_only_bulk_archive_own_bookmarks (line 335) | def test_can_only_bulk_archive_own_bookmarks(self):
    method test_bulk_unarchive (line 360) | def test_bulk_unarchive(self):
    method test_can_only_bulk_unarchive_own_bookmarks (line 382) | def test_can_only_bulk_unarchive_own_bookmarks(self):
    method test_bulk_delete (line 407) | def test_bulk_delete(self):
    method test_can_only_bulk_delete_own_bookmarks (line 429) | def test_can_only_bulk_delete_own_bookmarks(self):
    method test_bulk_tag (line 454) | def test_bulk_tag(self):
    method test_can_only_bulk_tag_own_bookmarks (line 483) | def test_can_only_bulk_tag_own_bookmarks(self):
    method test_bulk_untag (line 515) | def test_bulk_untag(self):
    method test_can_only_bulk_untag_own_bookmarks (line 544) | def test_can_only_bulk_untag_own_bookmarks(self):
    method test_bulk_mark_as_read (line 576) | def test_bulk_mark_as_read(self):
    method test_can_only_bulk_mark_as_read_own_bookmarks (line 598) | def test_can_only_bulk_mark_as_read_own_bookmarks(self):
    method test_bulk_mark_as_unread (line 623) | def test_bulk_mark_as_unread(self):
    method test_can_only_bulk_mark_as_unread_own_bookmarks (line 645) | def test_can_only_bulk_mark_as_unread_own_bookmarks(self):
    method test_bulk_share (line 670) | def test_bulk_share(self):
    method test_can_only_bulk_share_own_bookmarks (line 692) | def test_can_only_bulk_share_own_bookmarks(self):
    method test_bulk_unshare (line 717) | def test_bulk_unshare(self):
    method test_can_only_bulk_unshare_own_bookmarks (line 739) | def test_can_only_bulk_unshare_own_bookmarks(self):
    method test_bulk_select_across (line 764) | def test_bulk_select_across(self):
    method test_bulk_select_across_ignores_page (line 782) | def test_bulk_select_across_ignores_page(self):
    method setup_bulk_edit_scope_test_data (line 796) | def setup_bulk_edit_scope_test_data(self):
    method test_index_action_bulk_select_across_only_affects_active_bookmarks (line 807) | def test_index_action_bulk_select_across_only_affects_active_bookmarks...
    method test_index_action_bulk_select_across_respects_query (line 828) | def test_index_action_bulk_select_across_respects_query(self):
    method test_index_action_bulk_select_across_respects_bundle (line 846) | def test_index_action_bulk_select_across_respects_bundle(self):
    method test_archived_action_bulk_select_across_only_affects_archived_bookmarks (line 866) | def test_archived_action_bulk_select_across_only_affects_archived_book...
    method test_archived_action_bulk_select_across_respects_query (line 893) | def test_archived_action_bulk_select_across_respects_query(self):
    method test_archived_action_bulk_select_across_respects_bundle (line 911) | def test_archived_action_bulk_select_across_respects_bundle(self):
    method test_shared_action_bulk_select_across_not_supported (line 931) | def test_shared_action_bulk_select_across_not_supported(self):
    method test_handles_empty_bookmark_id (line 944) | def test_handles_empty_bookmark_id(self):
    method test_empty_action_does_not_modify_bookmarks (line 970) | def test_empty_action_does_not_modify_bookmarks(self):
    method test_index_action_redirects_to_index_with_query_params (line 988) | def test_index_action_redirects_to_index_with_query_params(self):
    method test_archived_action_redirects_to_archived_with_query_params (line 995) | def test_archived_action_redirects_to_archived_with_query_params(self):
    method test_shared_action_redirects_to_shared_with_query_params (line 1002) | def test_shared_action_redirects_to_shared_with_query_params(self):
    method bookmark_update_fixture (line 1009) | def bookmark_update_fixture(self):
    method assertBookmarkUpdateResponse (line 1021) | def assertBookmarkUpdateResponse(self, response: HttpResponse):
    method test_index_action_with_turbo_returns_bookmark_update (line 1051) | def test_index_action_with_turbo_returns_bookmark_update(self):
    method test_archived_action_with_turbo_returns_bookmark_update (line 1069) | def test_archived_action_with_turbo_returns_bookmark_update(self):
    method test_shared_action_with_turbo_returns_bookmark_update (line 1087) | def test_shared_action_with_turbo_returns_bookmark_update(self):

FILE: bookmarks/tests/test_bookmark_archived_view.py
  class BookmarkArchivedViewTestCase (line 15) | class BookmarkArchivedViewTestCase(
    method setUp (line 18) | def setUp(self) -> None:
    method assertEditLink (line 22) | def assertEditLink(self, response, url):
    method assertBulkActionForm (line 31) | def assertBulkActionForm(self, response, url: str):
    method test_should_list_archived_and_user_owned_bookmarks (line 37) | def test_should_list_archived_and_user_owned_bookmarks(self):
    method test_should_list_bookmarks_matching_query (line 52) | def test_should_list_bookmarks_matching_query(self):
    method test_should_list_bookmarks_matching_bundle (line 65) | def test_should_list_bookmarks_matching_bundle(self):
    method test_should_list_tags_for_archived_and_user_owned_bookmarks (line 82) | def test_should_list_tags_for_archived_and_user_owned_bookmarks(self):
    method test_should_list_tags_for_bookmarks_matching_query (line 106) | def test_should_list_tags_for_bookmarks_matching_query(self):
    method test_should_list_tags_for_bookmarks_matching_bundle (line 122) | def test_should_list_tags_for_bookmarks_matching_bundle(self):
    method test_should_list_bookmarks_and_tags_for_search_preferences (line 142) | def test_should_list_bookmarks_and_tags_for_search_preferences(self):
    method test_should_display_selected_tags_from_query (line 175) | def test_should_display_selected_tags_from_query(self):
    method test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode (line 192) | def test_should_not_display_search_terms_from_query_as_selected_tags_i...
    method test_should_display_search_terms_from_query_as_selected_tags_in_lax_mode (line 211) | def test_should_display_search_terms_from_query_as_selected_tags_in_la...
    method test_should_open_bookmarks_in_new_page_by_default (line 231) | def test_should_open_bookmarks_in_new_page_by_default(self):
    method test_should_open_bookmarks_in_same_page_if_specified_in_user_profile (line 238) | def test_should_open_bookmarks_in_same_page_if_specified_in_user_profi...
    method test_edit_link_return_url_respects_search_options (line 249) | def test_edit_link_return_url_respects_search_options(self):
    method test_bulk_edit_respects_search_options (line 277) | def test_bulk_edit_respects_search_options(self):
    method test_allowed_bulk_actions (line 301) | def test_allowed_bulk_actions(self):
    method test_allowed_bulk_actions_with_html_snapshot_enabled (line 322) | def test_allowed_bulk_actions_with_html_snapshot_enabled(self):
    method test_allowed_bulk_actions_with_sharing_enabled (line 343) | def test_allowed_bulk_actions_with_sharing_enabled(self):
    method test_allowed_bulk_actions_with_sharing_and_html_snapshot_enabled (line 370) | def test_allowed_bulk_actions_with_sharing_and_html_snapshot_enabled(s...
    method test_apply_search_preferences (line 397) | def test_apply_search_preferences(self):
    method test_save_search_preferences (line 448) | def test_save_search_preferences(self):
    method test_url_encode_bookmark_actions_url (line 544) | def test_url_encode_bookmark_actions_url(self):
    method test_encode_search_params (line 556) | def test_encode_search_params(self):
    method test_turbo_frame_details_modal_renders_details_modal_update (line 584) | def test_turbo_frame_details_modal_renders_details_modal_update(self):
    method test_does_not_include_rss_feed (line 596) | def test_does_not_include_rss_feed(self):
    method test_hide_bundles_when_enabled_in_profile (line 603) | def test_hide_bundles_when_enabled_in_profile(self):

FILE: bookmarks/tests/test_bookmark_archived_view_performance.py
  class BookmarkArchivedViewPerformanceTestCase (line 11) | class BookmarkArchivedViewPerformanceTestCase(
    method setUp (line 14) | def setUp(self) -> None:
    method get_connection (line 18) | def get_connection(self):
    method test_should_not_increase_number_of_queries_per_bookmark (line 21) | def test_should_not_increase_number_of_queries_per_bookmark(self):

FILE: bookmarks/tests/test_bookmark_asset_view.py
  class BookmarkAssetViewTestCase (line 11) | class BookmarkAssetViewTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 12) | def setUp(self) -> None:
    method setup_asset_file (line 17) | def setup_asset_file(self, filename):
    method setup_asset_with_file (line 22) | def setup_asset_with_file(self, bookmark):
    method setup_asset_with_uploaded_file (line 30) | def setup_asset_with_uploaded_file(self, bookmark, content_type="image...
    method view_access_test (line 42) | def view_access_test(self, view_name: str):
    method view_access_guest_user_test (line 82) | def view_access_guest_user_test(self, view_name: str):
    method test_view_access (line 132) | def test_view_access(self):
    method test_view_access_guest_user (line 135) | def test_view_access_guest_user(self):
    method test_reader_view_access (line 138) | def test_reader_view_access(self):
    method test_reader_view_access_guest_user (line 141) | def test_reader_view_access_guest_user(self):
    method test_snapshot_download_headers (line 144) | def test_snapshot_download_headers(self):
    method test_uploaded_file_download_headers (line 156) | def test_uploaded_file_download_headers(self):
    method test_uploaded_video_download_headers (line 168) | def test_uploaded_video_download_headers(self):

FILE: bookmarks/tests/test_bookmark_assets.py
  class BookmarkAssetsTestCase (line 11) | class BookmarkAssetsTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 12) | def setUp(self):
    method setup_asset_file (line 15) | def setup_asset_file(self, filename):
    method setup_asset_with_file (line 20) | def setup_asset_with_file(self, bookmark):
    method test_delete_bookmark_deletes_asset_file (line 26) | def test_delete_bookmark_deletes_asset_file(self):
    method test_bulk_delete_bookmarks_deletes_asset_files (line 38) | def test_bulk_delete_bookmarks_deletes_asset_files(self):
    method test_save_updates_file_size (line 69) | def test_save_updates_file_size(self):
    method test_download_name_for_html_snapshot (line 84) | def test_download_name_for_html_snapshot(self):
    method test_download_name_for_pdf_snapshot (line 94) | def test_download_name_for_pdf_snapshot(self):
    method test_download_name_for_upload (line 104) | def test_download_name_for_upload(self):

FILE: bookmarks/tests/test_bookmark_assets_api.py
  class BookmarkAssetsApiTestCase (line 12) | class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
    method setUp (line 13) | def setUp(self):
    method assertAsset (line 16) | def assertAsset(self, asset: BookmarkAsset, data: dict):
    method test_asset_list (line 28) | def test_asset_list(self):
    method test_asset_list_only_returns_assets_for_own_bookmarks (line 63) | def test_asset_list_only_returns_assets_for_own_bookmarks(self):
    method test_asset_list_requires_authentication (line 75) | def test_asset_list_requires_authentication(self):
    method test_asset_detail (line 82) | def test_asset_detail(self):
    method test_asset_detail_only_returns_asset_for_own_bookmarks (line 103) | def test_asset_detail_only_returns_asset_for_own_bookmarks(self):
    method test_asset_detail_requires_authentication (line 116) | def test_asset_detail_requires_authentication(self):
    method test_asset_download_with_snapshot_asset (line 125) | def test_asset_download_with_snapshot_asset(self):
    method test_asset_download_with_uploaded_asset (line 161) | def test_asset_download_with_uploaded_asset(self):
    method test_asset_download_with_missing_file (line 189) | def test_asset_download_with_missing_file(self):
    method test_asset_download_only_returns_asset_for_own_bookmarks (line 207) | def test_asset_download_only_returns_asset_for_own_bookmarks(self):
    method test_asset_download_requires_authentication (line 220) | def test_asset_download_requires_authentication(self):
    method create_upload_body (line 229) | def create_upload_body(self):
    method test_upload_asset (line 237) | def test_upload_asset(self):
    method test_upload_asset_with_missing_file (line 262) | def test_upload_asset_with_missing_file(self):
    method test_upload_asset_only_works_for_own_bookmarks (line 273) | def test_upload_asset_only_works_for_own_bookmarks(self):
    method test_upload_asset_requires_authentication (line 285) | def test_upload_asset_requires_authentication(self):
    method test_upload_asset_disabled (line 295) | def test_upload_asset_disabled(self):
    method test_delete_asset (line 304) | def test_delete_asset(self):
    method test_delete_asset_only_works_for_own_bookmarks (line 320) | def test_delete_asset_only_works_for_own_bookmarks(self):
    method test_delete_asset_requires_authentication (line 333) | def test_delete_asset_requires_authentication(self):

FILE: bookmarks/tests/test_bookmark_details_modal.py
  class BookmarkDetailsModalTestCase (line 12) | class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlT...
    method setUp (line 13) | def setUp(self):
    method get_details_form (line 17) | def get_details_form(self, soup, bookmark):
    method get_index_details_modal (line 23) | def get_index_details_modal(self, bookmark):
    method get_shared_details_modal (line 29) | def get_shared_details_modal(self, bookmark):
    method has_details_modal (line 35) | def has_details_modal(self, response):
    method find_section_content (line 39) | def find_section_content(self, soup, section_name):
    method get_section_content (line 44) | def get_section_content(self, soup, section_name):
    method find_weblink (line 49) | def find_weblink(self, soup, url):
    method count_weblinks (line 52) | def count_weblinks(self, soup):
    method find_asset (line 55) | def find_asset(self, soup, asset):
    method test_access (line 58) | def test_access(self):
    method test_access_with_sharing (line 91) | def test_access_with_sharing(self):
    method test_displays_title (line 131) | def test_displays_title(self):
    method test_website_link (line 148) | def test_website_link(self):
    method test_reader_mode_link (line 183) | def test_reader_mode_link(self):
    method test_internet_archive_link_with_snapshot_url (line 225) | def test_internet_archive_link_with_snapshot_url(self):
    method test_internet_archive_link_with_fallback_url (line 264) | def test_internet_archive_link_with_fallback_url(self):
    method test_weblinks_respect_target_setting (line 277) | def test_weblinks_respect_target_setting(self):
    method test_preview_image (line 313) | def test_preview_image(self):
    method test_status (line 343) | def test_status(self):
    method test_status_visibility (line 404) | def test_status_visibility(self):
    method test_date_added (line 427) | def test_date_added(self):
    method test_tags (line 436) | def test_tags(self):
    method test_description (line 456) | def test_description(self):
    method test_notes (line 471) | def test_notes(self):
    method test_edit_link (line 486) | def test_edit_link(self):
    method test_delete_button (line 496) | def test_delete_button(self):
    method test_actions_visibility (line 510) | def test_actions_visibility(self):
    method test_asset_list_visibility (line 552) | def test_asset_list_visibility(self):
    method test_asset_list (line 570) | def test_asset_list(self):
    method test_asset_list_actions_visibility (line 598) | def test_asset_list_actions_visibility(self):
    method test_asset_list_actions_visibility_without_snapshots_enabled (line 645) | def test_asset_list_actions_visibility_without_snapshots_enabled(self):
    method test_asset_list_actions_visibility_with_uploads_disabled (line 657) | def test_asset_list_actions_visibility_with_uploads_disabled(self):
    method test_asset_without_file (line 668) | def test_asset_without_file(self):
    method test_asset_status (line 680) | def test_asset_status(self):
    method test_asset_file_size (line 695) | def test_asset_file_size(self):
    method test_asset_actions_visibility (line 715) | def test_asset_actions_visibility(self):
    method test_create_snapshot_is_disabled_when_having_pending_asset (line 770) | def test_create_snapshot_is_disabled_when_having_pending_asset(self):

FILE: bookmarks/tests/test_bookmark_edit_view.py
  class BookmarkEditViewTestCase (line 9) | class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 10) | def setUp(self) -> None:
    method create_form_data (line 14) | def create_form_data(self, overrides=None):
    method test_should_render_successfully (line 28) | def test_should_render_successfully(self):
    method test_should_edit_bookmark (line 35) | def test_should_edit_bookmark(self):
    method test_should_return_422_with_invalid_form (line 57) | def test_should_return_422_with_invalid_form(self):
    method test_should_edit_unread_state (line 65) | def test_should_edit_unread_state(self):
    method test_should_edit_shared_state (line 82) | def test_should_edit_shared_state(self):
    method test_should_prefill_bookmark_form_fields (line 99) | def test_should_prefill_bookmark_form_fields(self):
    method test_should_prevent_duplicate_urls (line 157) | def test_should_prevent_duplicate_urls(self):
    method test_should_prevent_duplicate_normalized_urls (line 191) | def test_should_prevent_duplicate_normalized_urls(self):
    method test_should_redirect_to_return_url (line 210) | def test_should_redirect_to_return_url(self):
    method test_should_redirect_to_bookmark_index_by_default (line 223) | def test_should_redirect_to_bookmark_index_by_default(self):
    method test_should_not_redirect_to_external_url (line 233) | def test_should_not_redirect_to_external_url(self):
    method test_can_only_edit_own_bookmarks (line 254) | def test_can_only_edit_own_bookmarks(self):
    method test_should_respect_share_profile_setting (line 268) | def test_should_respect_share_profile_setting(self):
    method test_should_hide_notes_if_there_are_no_notes (line 309) | def test_should_hide_notes_if_there_are_no_notes(self):
    method test_should_show_notes_if_there_are_notes (line 317) | def test_should_show_notes_if_there_are_notes(self):

FILE: bookmarks/tests/test_bookmark_index_view.py
  class BookmarkIndexViewTestCase (line 15) | class BookmarkIndexViewTestCase(
    method setUp (line 18) | def setUp(self) -> None:
    method assertEditLink (line 22) | def assertEditLink(self, response, url):
    method assertBulkActionForm (line 31) | def assertBulkActionForm(self, response, url: str):
    method assertVisibleBundles (line 37) | def assertVisibleBundles(self, soup, bundles):
    method test_should_list_unarchived_and_user_owned_bookmarks (line 52) | def test_should_list_unarchived_and_user_owned_bookmarks(self):
    method test_should_list_bookmarks_matching_query (line 67) | def test_should_list_bookmarks_matching_query(self):
    method test_should_list_bookmarks_matching_bundle (line 76) | def test_should_list_bookmarks_matching_bundle(self):
    method test_should_list_tags_for_unarchived_and_user_owned_bookmarks (line 89) | def test_should_list_tags_for_unarchived_and_user_owned_bookmarks(self):
    method test_should_list_tags_for_bookmarks_matching_query (line 111) | def test_should_list_tags_for_bookmarks_matching_query(self):
    method test_should_list_tags_for_bookmarks_matching_bundle (line 127) | def test_should_list_tags_for_bookmarks_matching_bundle(self):
    method test_should_list_bookmarks_and_tags_for_search_preferences (line 147) | def test_should_list_bookmarks_and_tags_for_search_preferences(self):
    method test_should_display_selected_tags_from_query (line 170) | def test_should_display_selected_tags_from_query(self):
    method test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode (line 187) | def test_should_not_display_search_terms_from_query_as_selected_tags_i...
    method test_should_display_search_terms_from_query_as_selected_tags_in_lax_mode (line 206) | def test_should_display_search_terms_from_query_as_selected_tags_in_la...
    method test_should_open_bookmarks_in_new_page_by_default (line 226) | def test_should_open_bookmarks_in_new_page_by_default(self):
    method test_should_open_bookmarks_in_same_page_if_specified_in_user_profile (line 233) | def test_should_open_bookmarks_in_same_page_if_specified_in_user_profi...
    method test_edit_link_return_url_respects_search_options (line 244) | def test_edit_link_return_url_respects_search_options(self):
    method test_bulk_edit_respects_search_options (line 272) | def test_bulk_edit_respects_search_options(self):
    method test_allowed_bulk_actions (line 296) | def test_allowed_bulk_actions(self):
    method test_allowed_bulk_actions_with_html_snapshot_enabled (line 317) | def test_allowed_bulk_actions_with_html_snapshot_enabled(self):
    method test_allowed_bulk_actions_with_sharing_enabled (line 338) | def test_allowed_bulk_actions_with_sharing_enabled(self):
    method test_allowed_bulk_actions_with_sharing_and_html_snapshot_enabled (line 365) | def test_allowed_bulk_actions_with_sharing_and_html_snapshot_enabled(s...
    method test_apply_search_preferences (line 392) | def test_apply_search_preferences(self):
    method test_save_search_preferences (line 441) | def test_save_search_preferences(self):
    method test_url_encode_bookmark_actions_url (line 537) | def test_url_encode_bookmark_actions_url(self):
    method test_encode_search_params (line 549) | def test_encode_search_params(self):
    method test_turbo_frame_details_modal_renders_details_modal_update (line 577) | def test_turbo_frame_details_modal_renders_details_modal_update(self):
    method test_does_not_include_rss_feed (line 589) | def test_does_not_include_rss_feed(self):
    method test_list_bundles (line 596) | def test_list_bundles(self):
    method test_list_bundles_only_shows_user_owned_bundles (line 606) | def test_list_bundles_only_shows_user_owned_bundles(self):
    method test_hide_bundles_when_enabled_in_profile (line 619) | def test_hide_bundles_when_enabled_in_profile(self):

FILE: bookmarks/tests/test_bookmark_index_view_performance.py
  class BookmarkIndexViewPerformanceTestCase (line 11) | class BookmarkIndexViewPerformanceTestCase(
    method setUp (line 14) | def setUp(self) -> None:
    method get_connection (line 18) | def get_connection(self):
    method test_should_not_increase_number_of_queries_per_bookmark (line 21) | def test_should_not_increase_number_of_queries_per_bookmark(self):

FILE: bookmarks/tests/test_bookmark_new_view.py
  class BookmarkNewViewTestCase (line 8) | class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 9) | def setUp(self) -> None:
    method create_form_data (line 13) | def create_form_data(self, overrides=None):
    method test_should_create_new_bookmark (line 28) | def test_should_create_new_bookmark(self):
    method test_should_return_422_with_invalid_form (line 48) | def test_should_return_422_with_invalid_form(self):
    method test_should_create_new_unread_bookmark (line 53) | def test_should_create_new_unread_bookmark(self):
    method test_should_create_new_shared_bookmark (line 63) | def test_should_create_new_shared_bookmark(self):
    method test_should_prefill_url_from_url_parameter (line 73) | def test_should_prefill_url_from_url_parameter(self):
    method test_should_prefill_title_from_url_parameter (line 86) | def test_should_prefill_title_from_url_parameter(self):
    method test_should_prefill_description_from_url_parameter (line 99) | def test_should_prefill_description_from_url_parameter(self):
    method test_should_prefill_tags_from_url_parameter (line 112) | def test_should_prefill_tags_from_url_parameter(self):
    method test_should_prefill_notes_from_url_parameter (line 127) | def test_should_prefill_notes_from_url_parameter(self):
    method test_should_enable_auto_close_when_specified_in_url_parameter (line 150) | def test_should_enable_auto_close_when_specified_in_url_parameter(self):
    method test_should_not_enable_auto_close_when_not_specified_in_url_parameter (line 159) | def test_should_not_enable_auto_close_when_not_specified_in_url_parame...
    method test_should_redirect_to_index_view (line 168) | def test_should_redirect_to_index_view(self):
    method test_should_not_redirect_to_external_url (line 175) | def test_should_not_redirect_to_external_url(self):
    method test_auto_close_should_redirect_to_close_view (line 185) | def test_auto_close_should_redirect_to_close_view(self):
    method test_should_respect_share_profile_setting (line 192) | def test_should_respect_share_profile_setting(self):
    method test_should_show_respective_share_hint (line 227) | def test_should_show_respective_share_hint(self):
    method test_should_hide_notes_if_there_are_no_notes (line 256) | def test_should_hide_notes_if_there_are_no_notes(self):
    method test_should_not_check_unread_by_default (line 264) | def test_should_not_check_unread_by_default(self):
    method test_should_check_unread_when_configured_in_profile (line 273) | def test_should_check_unread_when_configured_in_profile(self):
    method test_should_not_check_shared_by_default (line 285) | def test_should_not_check_shared_by_default(self):
    method test_should_check_shared_when_configured_in_profile (line 297) | def test_should_check_shared_when_configured_in_profile(self):

FILE: bookmarks/tests/test_bookmark_previews.py
  class BookmarkPreviewsTestCase (line 12) | class BookmarkPreviewsTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 13) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method setup_preview_file (line 22) | def setup_preview_file(self, filename):
    method setup_bookmark_with_preview (line 27) | def setup_bookmark_with_preview(self):
    method assertPreviewImageExists (line 34) | def assertPreviewImageExists(self, bookmark):
    method assertPreviewImageDoesNotExist (line 41) | def assertPreviewImageDoesNotExist(self, bookmark):
    method test_delete_bookmark_deletes_preview_image (line 48) | def test_delete_bookmark_deletes_preview_image(self):
    method test_bulk_delete_bookmarks_deletes_preview_images (line 55) | def test_bulk_delete_bookmarks_deletes_preview_images(self):

FILE: bookmarks/tests/test_bookmark_search_form.py
  class BookmarkSearchFormTest (line 8) | class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
    method test_initial_values (line 9) | def test_initial_values(self):
    method test_user_options (line 38) | def test_user_options(self):
    method test_hidden_fields (line 57) | def test_hidden_fields(self):

FILE: bookmarks/tests/test_bookmark_search_model.py
  class MockRequest (line 8) | class MockRequest:
    method __init__ (line 9) | def __init__(self, user):
  class BookmarkSearchModelTest (line 13) | class BookmarkSearchModelTest(TestCase, BookmarkFactoryMixin):
    method test_from_request (line 14) | def test_from_request(self):
    method test_from_request_ignores_invalid_bundle_param (line 80) | def test_from_request_ignores_invalid_bundle_param(self):
    method test_query_params (line 96) | def test_query_params(self):
    method test_modified_params (line 177) | def test_modified_params(self):
    method test_has_modifications (line 249) | def test_has_modifications(self):
    method test_preferences_dict (line 266) | def test_preferences_dict(self):

FILE: bookmarks/tests/test_bookmark_search_tag.py
  class BookmarkSearchTagTest (line 9) | class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
    method render_template (line 10) | def render_template(self, url: str, mode: str = ""):
    method assertHiddenInput (line 29) | def assertHiddenInput(self, form: BeautifulSoup, name: str, value: str...
    method assertNoHiddenInput (line 36) | def assertNoHiddenInput(self, form: BeautifulSoup, name: str):
    method assertSearchInput (line 40) | def assertSearchInput(self, form: BeautifulSoup, name: str, value: str...
    method assertSelect (line 47) | def assertSelect(self, form: BeautifulSoup, name: str, value: str = No...
    method assertRadioGroup (line 59) | def assertRadioGroup(self, form: BeautifulSoup, name: str, value: str ...
    method assertNoRadioGroup (line 70) | def assertNoRadioGroup(self, form: BeautifulSoup, name: str):
    method assertUnmodifiedLabel (line 74) | def assertUnmodifiedLabel(self, html: str, text: str):
    method assertModifiedLabel (line 79) | def assertModifiedLabel(self, html: str, text: str):
    method test_search_form_inputs (line 84) | def test_search_form_inputs(self):
    method test_preferences_form_inputs (line 111) | def test_preferences_form_inputs(self):
    method test_preferences_form_inputs_shared_mode (line 152) | def test_preferences_form_inputs_shared_mode(self):
    method test_modified_indicator (line 185) | def test_modified_indicator(self):
    method test_modified_labels (line 210) | def test_modified_labels(self):

FILE: bookmarks/tests/test_bookmark_shared_view.py
  class BookmarkSharedViewTestCase (line 15) | class BookmarkSharedViewTestCase(
    method authenticate (line 18) | def authenticate(self) -> None:
    method assertBookmarkCount (line 22) | def assertBookmarkCount(
    method assertVisibleUserOptions (line 31) | def assertVisibleUserOptions(self, response, users: list[User]):
    method assertEditLink (line 47) | def assertEditLink(self, response, url):
    method test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled (line 56) | def test_should_list_shared_bookmarks_from_all_users_that_have_sharing...
    method test_should_list_shared_bookmarks_from_selected_user (line 82) | def test_should_list_shared_bookmarks_from_selected_user(self):
    method test_should_list_bookmarks_matching_query (line 102) | def test_should_list_bookmarks_matching_query(self):
    method test_should_list_bookmarks_matching_bundle (line 116) | def test_should_list_bookmarks_matching_bundle(self):
    method test_should_list_only_publicly_shared_bookmarks_without_login (line 134) | def test_should_list_only_publicly_shared_bookmarks_without_login(self):
    method test_should_list_tags_for_shared_bookmarks_from_all_users_that_have_sharing_enabled (line 150) | def test_should_list_tags_for_shared_bookmarks_from_all_users_that_hav...
    method test_should_list_tags_for_shared_bookmarks_from_selected_user (line 184) | def test_should_list_tags_for_shared_bookmarks_from_selected_user(self):
    method test_should_list_tags_for_bookmarks_matching_query (line 207) | def test_should_list_tags_for_bookmarks_matching_query(self):
    method test_should_list_tags_for_bookmarks_matching_bundle (line 244) | def test_should_list_tags_for_bookmarks_matching_bundle(self):
    method test_should_list_only_tags_for_publicly_shared_bookmarks_without_login (line 283) | def test_should_list_only_tags_for_publicly_shared_bookmarks_without_l...
    method test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled (line 307) | def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled...
    method test_should_list_only_users_with_publicly_shared_bookmarks_without_login (line 322) | def test_should_list_only_users_with_publicly_shared_bookmarks_without...
    method test_should_list_bookmarks_and_tags_for_search_preferences (line 342) | def test_should_list_bookmarks_and_tags_for_search_preferences(self):
    method test_should_open_bookmarks_in_new_page_by_default (line 380) | def test_should_open_bookmarks_in_new_page_by_default(self):
    method test_should_open_bookmarks_in_same_page_if_specified_in_user_profile (line 395) | def test_should_open_bookmarks_in_same_page_if_specified_in_user_profi...
    method test_edit_link_return_url_respects_search_options (line 412) | def test_edit_link_return_url_respects_search_options(self):
    method test_apply_search_preferences (line 453) | def test_apply_search_preferences(self):
    method test_save_search_preferences (line 502) | def test_save_search_preferences(self):
    method test_url_encode_bookmark_actions_url (line 599) | def test_url_encode_bookmark_actions_url(self):
    method test_encode_search_params (line 611) | def test_encode_search_params(self):
    method test_turbo_frame_details_modal_renders_details_modal_update (line 643) | def test_turbo_frame_details_modal_renders_details_modal_update(self):
    method test_includes_public_shared_rss_feed (line 655) | def test_includes_public_shared_rss_feed(self):
    method test_tag_menu_visible_for_authenticated_user (line 663) | def test_tag_menu_visible_for_authenticated_user(self):
    method test_tag_menu_not_visible_for_unauthenticated_user (line 673) | def test_tag_menu_not_visible_for_unauthenticated_user(self):

FILE: bookmarks/tests/test_bookmark_shared_view_performance.py
  class BookmarkSharedViewPerformanceTestCase (line 11) | class BookmarkSharedViewPerformanceTestCase(
    method setUp (line 14) | def setUp(self) -> None:
    method get_connection (line 18) | def get_connection(self):
    method test_should_not_increase_number_of_queries_per_bookmark (line 21) | def test_should_not_increase_number_of_queries_per_bookmark(self):

FILE: bookmarks/tests/test_bookmark_validation.py
  class BookmarkValidationTestCase (line 32) | class BookmarkValidationTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 33) | def setUp(self) -> None:
    method test_bookmark_model_should_not_allow_missing_url (line 36) | def test_bookmark_model_should_not_allow_missing_url(self):
    method test_bookmark_model_should_not_allow_empty_url (line 46) | def test_bookmark_model_should_not_allow_empty_url(self):
    method test_bookmark_model_should_validate_url_if_not_disabled_in_settings (line 58) | def test_bookmark_model_should_validate_url_if_not_disabled_in_setting...
    method test_bookmark_model_should_not_validate_url_if_disabled_in_settings (line 62) | def test_bookmark_model_should_not_validate_url_if_disabled_in_setting...
    method test_bookmark_form_should_validate_required_fields (line 65) | def test_bookmark_form_should_validate_required_fields(self):
    method test_bookmark_form_should_validate_url_if_not_disabled_in_settings (line 80) | def test_bookmark_form_should_validate_url_if_not_disabled_in_settings...
    method test_bookmark_form_should_not_validate_url_if_disabled_in_settings (line 84) | def test_bookmark_form_should_not_validate_url_if_disabled_in_settings...
    method _run_bookmark_model_url_validity_checks (line 87) | def _run_bookmark_model_url_validity_checks(self, cases):
    method _run_bookmark_form_url_validity_checks (line 106) | def _run_bookmark_form_url_validity_checks(self, cases):

FILE: bookmarks/tests/test_bookmarks_api.py
  class BookmarksApiTestCase (line 23) | class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
    method setUp (line 24) | def setUp(self):
    method tearDown (line 32) | def tearDown(self):
    method authenticate (line 35) | def authenticate(self):
    method assertBookmarkListEqual (line 39) | def assertBookmarkListEqual(self, data_list, bookmarks):
    method test_list_bookmarks (line 83) | def test_list_bookmarks(self):
    method test_list_bookmarks_with_more_details (line 92) | def test_list_bookmarks_with_more_details(self):
    method test_list_bookmarks_returns_none_for_website_title_and_description (line 107) | def test_list_bookmarks_returns_none_for_website_title_and_description...
    method test_list_bookmarks_does_not_return_archived_bookmarks (line 120) | def test_list_bookmarks_does_not_return_archived_bookmarks(self):
    method test_list_bookmarks_should_filter_by_query (line 130) | def test_list_bookmarks_should_filter_by_query(self):
    method test_list_bookmarks_should_filter_by_bundle (line 142) | def test_list_bookmarks_should_filter_by_bundle(self):
    method test_list_bookmarks_filter_unread (line 155) | def test_list_bookmarks_filter_unread(self):
    method test_list_bookmarks_filter_shared (line 182) | def test_list_bookmarks_filter_shared(self):
    method test_list_bookmarks_should_respect_sort (line 209) | def test_list_bookmarks_should_respect_sort(self):
    method test_list_archived_bookmarks_does_not_return_unarchived_bookmarks (line 220) | def test_list_archived_bookmarks_does_not_return_unarchived_bookmarks(...
    method test_list_archived_bookmarks_with_more_details (line 231) | def test_list_archived_bookmarks_with_more_details(self):
    method test_list_archived_bookmarks_should_filter_by_query (line 248) | def test_list_archived_bookmarks_should_filter_by_query(self):
    method test_list_archived_bookmarks_should_filter_by_bundle (line 262) | def test_list_archived_bookmarks_should_filter_by_bundle(self):
    method test_list_archived_bookmarks_should_respect_sort (line 277) | def test_list_archived_bookmarks_should_respect_sort(self):
    method test_list_shared_bookmarks (line 288) | def test_list_shared_bookmarks(self):
    method test_list_shared_bookmarks_with_more_details (line 312) | def test_list_shared_bookmarks_with_more_details(self):
    method test_list_only_publicly_shared_bookmarks_when_not_logged_in (line 332) | def test_list_only_publicly_shared_bookmarks_when_not_logged_in(self):
    method test_list_shared_bookmarks_should_filter_by_query_and_user (line 349) | def test_list_shared_bookmarks_should_filter_by_query_and_user(self):
    method test_list_shared_bookmarks_should_respect_sort (line 405) | def test_list_shared_bookmarks_should_respect_sort(self):
    method test_create_bookmark (line 417) | def test_create_bookmark(self):
    method test_create_bookmark_enhances_with_metadata_by_default (line 443) | def test_create_bookmark_enhances_with_metadata_by_default(self):
    method test_create_bookmark_does_not_enhance_with_metadata_if_scraping_is_disabled (line 459) | def test_create_bookmark_does_not_enhance_with_metadata_if_scraping_is...
    method test_create_bookmark_creates_html_snapshot_by_default (line 481) | def test_create_bookmark_creates_html_snapshot_by_default(self):
    method test_create_bookmark_does_not_create_html_snapshot_if_disabled (line 496) | def test_create_bookmark_does_not_create_html_snapshot_if_disabled(self):
    method test_create_bookmark_with_same_url_updates_existing_bookmark (line 515) | def test_create_bookmark_with_same_url_updates_existing_bookmark(self):
    method test_create_bookmark_replaces_whitespace_in_tag_names (line 544) | def test_create_bookmark_replaces_whitespace_in_tag_names(self):
    method test_create_bookmark_minimal_payload (line 558) | def test_create_bookmark_minimal_payload(self):
    method test_create_archived_bookmark (line 578) | def test_create_archived_bookmark(self):
    method test_create_bookmark_is_not_archived_by_default (line 598) | def test_create_bookmark_is_not_archived_by_default(self):
    method test_create_unread_bookmark (line 606) | def test_create_unread_bookmark(self):
    method test_create_bookmark_is_not_unread_by_default (line 614) | def test_create_bookmark_is_not_unread_by_default(self):
    method test_create_shared_bookmark (line 622) | def test_create_shared_bookmark(self):
    method test_create_bookmark_is_not_shared_by_default (line 630) | def test_create_bookmark_is_not_shared_by_default(self):
    method test_create_bookmark_should_add_tags_from_auto_tagging (line 638) | def test_create_bookmark_should_add_tags_from_auto_tagging(self):
    method test_create_bookmark_should_set_default_dates (line 652) | def test_create_bookmark_should_set_default_dates(self):
    method test_create_bookmark_with_date_added (line 665) | def test_create_bookmark_with_date_added(self):
    method test_create_bookmark_with_date_modified (line 674) | def test_create_bookmark_with_date_modified(self):
    method test_get_bookmark (line 683) | def test_get_bookmark(self):
    method test_get_bookmark_with_more_details (line 691) | def test_get_bookmark_with_more_details(self):
    method test_get_bookmark_returns_fallback_webarchive_url (line 703) | def test_get_bookmark_returns_fallback_webarchive_url(self):
    method test_update_bookmark (line 718) | def test_update_bookmark(self):
    method test_update_bookmark_ignores_readonly_fields (line 728) | def test_update_bookmark_ignores_readonly_fields(self):
    method test_update_bookmark_fails_without_required_fields (line 750) | def test_update_bookmark_fails_without_required_fields(self):
    method test_update_bookmark_with_minimal_payload_does_not_modify_bookmark (line 758) | def test_update_bookmark_with_minimal_payload_does_not_modify_bookmark...
    method test_update_bookmark_unread_flag (line 777) | def test_update_bookmark_unread_flag(self):
    method test_update_bookmark_shared_flag (line 787) | def test_update_bookmark_shared_flag(self):
    method test_update_bookmark_adds_tags_from_auto_tagging (line 797) | def test_update_bookmark_adds_tags_from_auto_tagging(self):
    method test_update_bookmark_should_prevent_duplicate_urls (line 813) | def test_update_bookmark_should_prevent_duplicate_urls(self):
    method test_patch_bookmark (line 836) | def test_patch_bookmark(self):
    method test_patch_ignores_readonly_fields (line 895) | def test_patch_ignores_readonly_fields(self):
    method test_patch_with_empty_payload_does_not_modify_bookmark (line 915) | def test_patch_with_empty_payload_does_not_modify_bookmark(self):
    method test_patch_bookmark_adds_tags_from_auto_tagging (line 933) | def test_patch_bookmark_adds_tags_from_auto_tagging(self):
    method test_delete_bookmark (line 949) | def test_delete_bookmark(self):
    method test_archive (line 957) | def test_archive(self):
    method test_unarchive (line 966) | def test_unarchive(self):
    method test_check_returns_no_bookmark_if_url_is_not_bookmarked (line 975) | def test_check_returns_no_bookmark_if_url_is_not_bookmarked(self):
    method test_check_returns_scraped_metadata_if_url_is_not_bookmarked (line 987) | def test_check_returns_scraped_metadata_if_url_is_not_bookmarked(self):
    method test_check_returns_bookmark_if_url_is_bookmarked (line 1014) | def test_check_returns_bookmark_if_url_is_bookmarked(self):
    method test_check_returns_scraped_metadata_if_url_is_bookmarked (line 1044) | def test_check_returns_scraped_metadata_if_url_is_bookmarked(self):
    method test_check_returns_bookmark_using_normalized_url (line 1075) | def test_check_returns_bookmark_using_normalized_url(self):
    method test_check_returns_no_auto_tags_if_none_configured (line 1098) | def test_check_returns_no_auto_tags_if_none_configured(self):
    method test_check_returns_matching_auto_tags (line 1110) | def test_check_returns_matching_auto_tags(self):
    method test_check_ignore_cache (line 1126) | def test_check_ignore_cache(self):
    method test_can_only_access_own_bookmarks (line 1163) | def test_can_only_access_own_bookmarks(self):
    method assertUserProfile (line 1240) | def assertUserProfile(self, response: Response, profile: UserProfile):
    method test_user_profile (line 1264) | def test_user_profile(self):
    method create_singlefile_upload_body (line 1297) | def create_singlefile_upload_body(self):
    method test_singlefile_upload (line 1305) | def test_singlefile_upload(self):
    method test_singlefile_creates_bookmark_if_not_exists (line 1321) | def test_singlefile_creates_bookmark_if_not_exists(self):
    method test_singlefile_updates_own_bookmark_if_exists (line 1341) | def test_singlefile_updates_own_bookmark_if_exists(self):
    method test_singlefile_creates_bookmark_without_creating_snapshot (line 1358) | def test_singlefile_creates_bookmark_without_creating_snapshot(self):
    method test_singlefile_upload_missing_parameters (line 1375) | def test_singlefile_upload_missing_parameters(self):
    method test_singlefile_upload_disabled (line 1404) | def test_singlefile_upload_disabled(self):

FILE: bookmarks/tests/test_bookmarks_api_performance.py
  class BookmarksApiPerformanceTestCase (line 11) | class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFacto...
    method setUp (line 12) | def setUp(self) -> None:
    method get_connection (line 19) | def get_connection(self):
    method test_list_bookmarks_max_queries (line 22) | def test_list_bookmarks_max_queries(self):
    method test_list_archived_bookmarks_max_queries (line 40) | def test_list_archived_bookmarks_max_queries(self):
    method test_list_shared_bookmarks_max_queries (line 58) | def test_list_shared_bookmarks_max_queries(self):

FILE: bookmarks/tests/test_bookmarks_api_permissions.py
  class BookmarksApiPermissionsTestCase (line 9) | class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFacto...
    method authenticate (line 10) | def authenticate(self) -> None:
    method test_list_bookmarks_requires_authentication (line 14) | def test_list_bookmarks_requires_authentication(self):
    method test_list_archived_bookmarks_requires_authentication (line 25) | def test_list_archived_bookmarks_requires_authentication(self):
    method test_list_shared_bookmarks_does_not_require_authentication (line 37) | def test_list_shared_bookmarks_does_not_require_authentication(self):
    method test_create_bookmark_requires_authentication (line 49) | def test_create_bookmark_requires_authentication(self):
    method test_get_bookmark_requires_authentication (line 66) | def test_get_bookmark_requires_authentication(self):
    method test_update_bookmark_requires_authentication (line 75) | def test_update_bookmark_requires_authentication(self):
    method test_update_bookmark_only_updates_own_bookmarks (line 85) | def test_update_bookmark_only_updates_own_bookmarks(self):
    method test_patch_bookmark_requires_authentication (line 95) | def test_patch_bookmark_requires_authentication(self):
    method test_patch_bookmark_only_updates_own_bookmarks (line 105) | def test_patch_bookmark_only_updates_own_bookmarks(self):
    method test_delete_bookmark_requires_authentication (line 115) | def test_delete_bookmark_requires_authentication(self):
    method test_archive_requires_authentication (line 124) | def test_archive_requires_authentication(self):
    method test_unarchive_requires_authentication (line 133) | def test_unarchive_requires_authentication(self):
    method test_check_requires_authentication (line 142) | def test_check_requires_authentication(self):
    method test_user_profile_requires_authentication (line 153) | def test_user_profile_requires_authentication(self):
    method test_singlefile_upload_requires_authentication (line 161) | def test_singlefile_upload_requires_authentication(self):

FILE: bookmarks/tests/test_bookmarks_list_template.py
  class BookmarkListTemplateTest (line 17) | class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestM...
    method assertBookmarksLink (line 18) | def assertBookmarksLink(
    method assertWebArchiveLink (line 38) | def assertWebArchiveLink(
    method assertViewLink (line 51) | def assertViewLink(self, html: str, bookmark: Bookmark, base_url=None):
    method assertNoViewLink (line 54) | def assertNoViewLink(self, html: str, bookmark: Bookmark, base_url=None):
    method assertViewLinkCount (line 57) | def assertViewLinkCount(
    method assertEditLinkCount (line 75) | def assertEditLinkCount(self, html: str, bookmark: Bookmark, count=1):
    method assertArchiveLinkCount (line 85) | def assertArchiveLinkCount(self, html: str, bookmark: Bookmark, count=1):
    method assertDeleteLinkCount (line 95) | def assertDeleteLinkCount(self, html: str, bookmark: Bookmark, count=1):
    method assertBookmarkActions (line 105) | def assertBookmarkActions(self, html: str, bookmark: Bookmark):
    method assertNoBookmarkActions (line 108) | def assertNoBookmarkActions(self, html: str, bookmark: Bookmark):
    method assertBookmarkActionsCount (line 111) | def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, co...
    method assertShareInfo (line 116) | def assertShareInfo(self, html: str, bookmark: Bookmark):
    method assertNoShareInfo (line 119) | def assertNoShareInfo(self, html: str, bookmark: Bookmark):
    method assertShareInfoCount (line 122) | def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
    method assertFaviconVisible (line 134) | def assertFaviconVisible(self, html: str, bookmark: Bookmark):
    method assertFaviconHidden (line 137) | def assertFaviconHidden(self, html: str, bookmark: Bookmark):
    method assertFavicon (line 140) | def assertFavicon(self, html: str, bookmark: Bookmark, visible=True):
    method assertPreviewImageVisible (line 153) | def assertPreviewImageVisible(self, html: str, bookmark: Bookmark):
    method assertPreviewImageHidden (line 156) | def assertPreviewImageHidden(self, html: str, bookmark: Bookmark):
    method assertPreviewImage (line 159) | def assertPreviewImage(self, html: str, bookmark: Bookmark, visible=Tr...
    method assertPreviewImagePlaceholder (line 171) | def assertPreviewImagePlaceholder(self, html: str):
    method assertBookmarkURLCount (line 176) | def assertBookmarkURLCount(
    method assertBookmarkURLVisible (line 192) | def assertBookmarkURLVisible(self, html: str, bookmark: Bookmark):
    method assertBookmarkURLHidden (line 195) | def assertBookmarkURLHidden(
    method assertNotes (line 200) | def assertNotes(self, html: str, notes_html: str, count=1):
    method assertNotesToggle (line 213) | def assertNotesToggle(self, html: str, count=1):
    method assertUnshareButton (line 227) | def assertUnshareButton(self, html: str, bookmark: Bookmark, count=1):
    method assertMarkAsReadButton (line 243) | def assertMarkAsReadButton(self, html: str, bookmark: Bookmark, count=1):
    method render_template (line 259) | def render_template(
    method setup_date_format_test (line 283) | def setup_date_format_test(
    method inline_bookmark_description_test (line 295) | def inline_bookmark_description_test(self, bookmark):
    method test_inline_bookmark_description (line 344) | def test_inline_bookmark_description(self):
    method separate_bookmark_description_test (line 374) | def separate_bookmark_description_test(self, bookmark):
    method test_separate_bookmark_description (line 413) | def test_separate_bookmark_description(self):
    method test_bookmark_description_max_lines (line 443) | def test_bookmark_description_max_lines(self):
    method test_bookmark_tag_ordering (line 461) | def test_bookmark_tag_ordering(self):
    method test_bookmark_tag_query_string (line 477) | def test_bookmark_tag_query_string(self):
    method test_should_render_web_archive_link_with_absolute_date_setting (line 498) | def test_should_render_web_archive_link_with_absolute_date_setting(self):
    method test_should_render_web_archive_link_with_relative_date_setting (line 510) | def test_should_render_web_archive_link_with_relative_date_setting(self):
    method test_should_render_generated_web_archive_link_without_saved_snapshot_url (line 519) | def test_should_render_generated_web_archive_link_without_saved_snapsh...
    method test_bookmark_link_target_should_be_blank_by_default (line 538) | def test_bookmark_link_target_should_be_blank_by_default(self):
    method test_bookmark_link_target_should_respect_user_profile (line 544) | def test_bookmark_link_target_should_respect_user_profile(self):
    method test_web_archive_link_target_should_be_blank_by_default (line 554) | def test_web_archive_link_target_should_be_blank_by_default(self):
    method test_web_archive_link_target_should_respect_user_profile (line 566) | def test_web_archive_link_target_should_respect_user_profile(self):
    method test_should_render_latest_snapshot_link_if_one_exists (line 582) | def test_should_render_latest_snapshot_link_if_one_exists(self):
    method test_should_reflect_unread_state_as_css_class (line 607) | def test_should_reflect_unread_state_as_css_class(self):
    method test_should_reflect_shared_state_as_css_class (line 616) | def test_should_reflect_shared_state_as_css_class(self):
    method test_should_reflect_both_unread_and_shared_state_as_css_class (line 629) | def test_should_reflect_both_unread_and_shared_state_as_css_class(self):
    method test_show_bookmark_actions_for_owned_bookmarks (line 642) | def test_show_bookmark_actions_for_owned_bookmarks(self):
    method test_hide_view_link (line 650) | def test_hide_view_link(self):
    method test_hide_edit_link (line 662) | def test_hide_edit_link(self):
    method test_hide_archive_link (line 674) | def test_hide_archive_link(self):
    method test_hide_remove_link (line 686) | def test_hide_remove_link(self):
    method test_show_share_info_for_non_owned_bookmarks (line 698) | def test_show_share_info_for_non_owned_bookmarks(self):
    method test_share_info_user_link_keeps_query_params (line 714) | def test_share_info_user_link_keeps_query_params(self):
    method test_preview_image_should_be_visible_when_preview_images_enabled (line 735) | def test_preview_image_should_be_visible_when_preview_images_enabled(s...
    method test_preview_image_should_be_hidden_when_preview_images_disabled (line 745) | def test_preview_image_should_be_hidden_when_preview_images_disabled(s...
    method test_preview_image_shows_placeholder_when_there_is_no_preview_image (line 755) | def test_preview_image_shows_placeholder_when_there_is_no_preview_imag...
    method test_favicon_should_be_visible_when_favicons_enabled (line 765) | def test_favicon_should_be_visible_when_favicons_enabled(self):
    method test_favicon_should_be_hidden_when_there_is_no_icon (line 775) | def test_favicon_should_be_hidden_when_there_is_no_icon(self):
    method test_favicon_should_be_hidden_when_favicons_disabled (line 785) | def test_favicon_should_be_hidden_when_favicons_disabled(self):
    method test_bookmark_url_should_be_hidden_by_default (line 795) | def test_bookmark_url_should_be_hidden_by_default(self):
    method test_show_bookmark_url_when_enabled (line 804) | def test_show_bookmark_url_when_enabled(self):
    method test_hide_bookmark_url_when_disabled (line 814) | def test_hide_bookmark_url_when_disabled(self):
    method test_show_mark_as_read_when_unread (line 824) | def test_show_mark_as_read_when_unread(self):
    method test_hide_mark_as_read_when_read (line 830) | def test_hide_mark_as_read_when_read(self):
    method test_hide_mark_as_read_for_non_owned_bookmarks (line 836) | def test_hide_mark_as_read_for_non_owned_bookmarks(self):
    method test_show_unshare_button_when_shared (line 845) | def test_show_unshare_button_when_shared(self):
    method test_hide_unshare_button_when_not_shared (line 855) | def test_hide_unshare_button_when_not_shared(self):
    method test_hide_unshare_button_when_sharing_is_disabled (line 865) | def test_hide_unshare_button_when_sharing_is_disabled(self):
    method test_hide_unshare_for_non_owned_bookmarks (line 875) | def test_hide_unshare_for_non_owned_bookmarks(self):
    method test_without_notes (line 884) | def test_without_notes(self):
    method test_with_notes (line 891) | def test_with_notes(self):
    method test_note_renders_markdown (line 898) | def test_note_renders_markdown(self):
    method test_note_renders_markdown_with_linkify (line 907) | def test_note_renders_markdown_with_linkify(self):
    method test_note_linkify_converts_schemeless_urls_to_https (line 922) | def test_note_linkify_converts_schemeless_urls_to_https(self):
    method test_note_cleans_html (line 958) | def test_note_cleans_html(self):
    method test_notes_are_hidden_initially_by_default (line 971) | def test_notes_are_hidden_initially_by_default(self):
    method test_notes_are_hidden_initially_with_permanent_notes_disabled (line 979) | def test_notes_are_hidden_initially_with_permanent_notes_disabled(self):
    method test_notes_are_visible_initially_with_permanent_notes_enabled (line 991) | def test_notes_are_visible_initially_with_permanent_notes_enabled(self):
    method test_toggle_notes_is_visible_by_default (line 1003) | def test_toggle_notes_is_visible_by_default(self):
    method test_toggle_notes_is_visible_with_permanent_notes_disabled (line 1009) | def test_toggle_notes_is_visible_with_permanent_notes_disabled(self):
    method test_toggle_notes_is_hidden_with_permanent_notes_enabled (line 1019) | def test_toggle_notes_is_hidden_with_permanent_notes_enabled(self):
    method test_with_anonymous_user (line 1029) | def test_with_anonymous_user(self):
    method test_empty_state (line 1066) | def test_empty_state(self):
    method test_empty_state_with_valid_query_no_results (line 1073) | def test_empty_state_with_valid_query_no_results(self):
    method test_empty_state_with_invalid_query (line 1081) | def test_empty_state_with_invalid_query(self):
    method test_empty_state_with_legacy_search (line 1088) | def test_empty_state_with_legacy_search(self):
    method test_pagination_is_not_sticky_by_default (line 1101) | def test_pagination_is_not_sticky_by_default(self):
    method test_pagination_is_sticky_when_enabled_in_profile (line 1107) | def test_pagination_is_sticky_when_enabled_in_profile(self):
    method test_items_per_page_is_30_by_default (line 1116) | def test_items_per_page_is_30_by_default(self):
    method test_items_per_page_is_configurable (line 1124) | def test_items_per_page_is_configurable(self):
    method test_no_actions_rendered_when_is_preview (line 1135) | def test_no_actions_rendered_when_is_preview(self):

FILE: bookmarks/tests/test_bookmarks_model.py
  class BookmarkTestCase (line 6) | class BookmarkTestCase(TestCase):
    method test_bookmark_resolved_title (line 7) | def test_bookmark_resolved_title(self):

FILE: bookmarks/tests/test_bookmarks_service.py
  class BookmarkServiceTestCase (line 30) | class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 31) | def setUp(self) -> None:
    method tearDown (line 45) | def tearDown(self):
    method test_create_should_not_update_website_metadata (line 49) | def test_create_should_not_update_website_metadata(self):
    method test_create_should_update_existing_bookmark_with_same_url (line 70) | def test_create_should_update_existing_bookmark_with_same_url(self):
    method test_create_should_update_existing_bookmark_with_normalized_url (line 97) | def test_create_should_update_existing_bookmark_with_normalized_url(
    method test_create_should_update_existing_bookmark_when_normalized_url_is_empty (line 116) | def test_create_should_update_existing_bookmark_when_normalized_url_is...
    method test_create_should_update_first_existing_bookmark_for_multiple_duplicates (line 136) | def test_create_should_update_first_existing_bookmark_for_multiple_dup...
    method test_create_should_populate_url_normalized_field (line 157) | def test_create_should_populate_url_normalized_field(self):
    method test_create_should_create_web_archive_snapshot (line 173) | def test_create_should_create_web_archive_snapshot(self):
    method test_create_should_load_favicon (line 184) | def test_create_should_load_favicon(self):
    method test_create_should_load_html_snapshot (line 191) | def test_create_should_load_html_snapshot(self):
    method test_create_should_not_load_html_snapshot_when_disabled (line 198) | def test_create_should_not_load_html_snapshot_when_disabled(self):
    method test_create_should_not_load_html_snapshot_when_setting_is_disabled (line 207) | def test_create_should_not_load_html_snapshot_when_setting_is_disabled...
    method test_create_should_add_tags_from_auto_tagging (line 218) | def test_create_should_add_tags_from_auto_tagging(self):
    method test_create_should_set_default_dates (line 230) | def test_create_should_set_default_dates(self):
    method test_create_should_use_provided_date_added (line 242) | def test_create_should_use_provided_date_added(self):
    method test_create_should_use_provided_date_modified (line 250) | def test_create_should_use_provided_date_modified(self):
    method test_create_should_use_provided_dates (line 258) | def test_create_should_use_provided_dates(self):
    method test_update_should_create_web_archive_snapshot_if_url_did_change (line 272) | def test_update_should_create_web_archive_snapshot_if_url_did_change(s...
    method test_update_should_not_create_web_archive_snapshot_if_url_did_not_change (line 284) | def test_update_should_not_create_web_archive_snapshot_if_url_did_not_...
    method test_update_should_not_update_website_metadata (line 294) | def test_update_should_not_update_website_metadata(self):
    method test_update_should_not_update_website_metadata_if_url_did_change (line 306) | def test_update_should_not_update_website_metadata_if_url_did_change(s...
    method test_update_should_update_favicon (line 318) | def test_update_should_update_favicon(self):
    method test_update_should_not_create_html_snapshot (line 326) | def test_update_should_not_create_html_snapshot(self):
    method test_update_should_add_tags_from_auto_tagging (line 334) | def test_update_should_add_tags_from_auto_tagging(self):
    method test_archive_bookmark (line 346) | def test_archive_bookmark(self):
    method test_unarchive_bookmark (line 363) | def test_unarchive_bookmark(self):
    method test_archive_bookmarks (line 379) | def test_archive_bookmarks(self):
    method test_archive_bookmarks_should_only_archive_specified_bookmarks (line 392) | def test_archive_bookmarks_should_only_archive_specified_bookmarks(self):
    method test_archive_bookmarks_should_only_archive_user_owned_bookmarks (line 403) | def test_archive_bookmarks_should_only_archive_user_owned_bookmarks(se...
    method test_archive_bookmarks_should_accept_mix_of_int_and_string_ids (line 418) | def test_archive_bookmarks_should_accept_mix_of_int_and_string_ids(self):
    method test_unarchive_bookmarks (line 432) | def test_unarchive_bookmarks(self):
    method test_unarchive_bookmarks_should_only_unarchive_specified_bookmarks (line 445) | def test_unarchive_bookmarks_should_only_unarchive_specified_bookmarks...
    method test_unarchive_bookmarks_should_only_unarchive_user_owned_bookmarks (line 458) | def test_unarchive_bookmarks_should_only_unarchive_user_owned_bookmark...
    method test_unarchive_bookmarks_should_accept_mix_of_int_and_string_ids (line 473) | def test_unarchive_bookmarks_should_accept_mix_of_int_and_string_ids(s...
    method test_delete_bookmarks (line 487) | def test_delete_bookmarks(self):
    method test_delete_bookmarks_should_only_delete_specified_bookmarks (line 500) | def test_delete_bookmarks_should_only_delete_specified_bookmarks(self):
    method test_delete_bookmarks_should_only_delete_user_owned_bookmarks (line 511) | def test_delete_bookmarks_should_only_delete_user_owned_bookmarks(self):
    method test_delete_bookmarks_should_accept_mix_of_int_and_string_ids (line 528) | def test_delete_bookmarks_should_accept_mix_of_int_and_string_ids(self):
    method test_tag_bookmarks (line 541) | def test_tag_bookmarks(self):
    method test_tag_bookmarks_should_create_tags (line 562) | def test_tag_bookmarks_should_create_tags(self):
    method test_tag_bookmarks_should_handle_existing_relationships (line 589) | def test_tag_bookmarks_should_handle_existing_relationships(self):
    method test_tag_bookmarks_should_only_tag_specified_bookmarks (line 614) | def test_tag_bookmarks_should_only_tag_specified_bookmarks(self):
    method test_tag_bookmarks_should_only_tag_user_owned_bookmarks (line 635) | def test_tag_bookmarks_should_only_tag_user_owned_bookmarks(self):
    method test_tag_bookmarks_should_accept_mix_of_int_and_string_ids (line 657) | def test_tag_bookmarks_should_accept_mix_of_int_and_string_ids(self):
    method test_untag_bookmarks (line 674) | def test_untag_bookmarks(self):
    method test_untag_bookmarks_should_only_tag_specified_bookmarks (line 695) | def test_untag_bookmarks_should_only_tag_specified_bookmarks(self):
    method test_untag_bookmarks_should_only_tag_user_owned_bookmarks (line 716) | def test_untag_bookmarks_should_only_tag_user_owned_bookmarks(self):
    method test_untag_bookmarks_should_accept_mix_of_int_and_string_ids (line 738) | def test_untag_bookmarks_should_accept_mix_of_int_and_string_ids(self):
    method test_mark_bookmarks_as_read (line 755) | def test_mark_bookmarks_as_read(self):
    method test_mark_bookmarks_as_read_should_only_update_specified_bookmarks (line 768) | def test_mark_bookmarks_as_read_should_only_update_specified_bookmarks...
    method test_mark_bookmarks_as_read_should_only_update_user_owned_bookmarks (line 781) | def test_mark_bookmarks_as_read_should_only_update_user_owned_bookmark...
    method test_mark_bookmarks_as_read_should_accept_mix_of_int_and_string_ids (line 796) | def test_mark_bookmarks_as_read_should_accept_mix_of_int_and_string_id...
    method test_mark_bookmarks_as_unread (line 810) | def test_mark_bookmarks_as_unread(self):
    method test_mark_bookmarks_as_unread_should_only_update_specified_bookmarks (line 823) | def test_mark_bookmarks_as_unread_should_only_update_specified_bookmar...
    method test_mark_bookmarks_as_unread_should_only_update_user_owned_bookmarks (line 836) | def test_mark_bookmarks_as_unread_should_only_update_user_owned_bookma...
    method test_mark_bookmarks_as_unread_should_accept_mix_of_int_and_string_ids (line 851) | def test_mark_bookmarks_as_unread_should_accept_mix_of_int_and_string_...
    method test_share_bookmarks (line 865) | def test_share_bookmarks(self):
    method test_share_bookmarks_should_only_update_specified_bookmarks (line 878) | def test_share_bookmarks_should_only_update_specified_bookmarks(self):
    method test_share_bookmarks_should_only_update_user_owned_bookmarks (line 889) | def test_share_bookmarks_should_only_update_user_owned_bookmarks(self):
    method test_share_bookmarks_should_accept_mix_of_int_and_string_ids (line 904) | def test_share_bookmarks_should_accept_mix_of_int_and_string_ids(self):
    method test_unshare_bookmarks (line 918) | def test_unshare_bookmarks(self):
    method test_unshare_bookmarks_should_only_update_specified_bookmarks (line 931) | def test_unshare_bookmarks_should_only_update_specified_bookmarks(self):
    method test_unshare_bookmarks_should_only_update_user_owned_bookmarks (line 942) | def test_unshare_bookmarks_should_only_update_user_owned_bookmarks(self):
    method test_unshare_bookmarks_should_accept_mix_of_int_and_string_ids (line 957) | def test_unshare_bookmarks_should_accept_mix_of_int_and_string_ids(self):
    method test_enhance_with_website_metadata (line 971) | def test_enhance_with_website_metadata(self):
    method test_refresh_bookmarks_metadata (line 1029) | def test_refresh_bookmarks_metadata(self):
    method test_refresh_bookmarks_metadata_should_only_refresh_specified_bookmarks (line 1041) | def test_refresh_bookmarks_metadata_should_only_refresh_specified_book...
    method test_refresh_bookmarks_metadata_should_only_refresh_user_owned_bookmarks (line 1061) | def test_refresh_bookmarks_metadata_should_only_refresh_user_owned_boo...
    method test_refresh_bookmarks_metadata_should_accept_mix_of_int_and_string_ids (line 1083) | def test_refresh_bookmarks_metadata_should_accept_mix_of_int_and_strin...
    method test_create_html_snapshots (line 1096) | def test_create_html_snapshots(self):
    method test_create_html_snapshots_should_only_create_for_specified_bookmarks (line 1114) | def test_create_html_snapshots_should_only_create_for_specified_bookma...
    method test_create_html_snapshots_should_only_create_for_user_owned_bookmarks (line 1130) | def test_create_html_snapshots_should_only_create_for_user_owned_bookm...
    method test_create_html_snapshots_should_accept_mix_of_int_and_string_ids (line 1148) | def test_create_html_snapshots_should_accept_mix_of_int_and_string_ids...

FILE: bookmarks/tests/test_bookmarks_tasks.py
  function create_wayback_machine_save_api_mock (line 15) | def create_wayback_machine_save_api_mock(
  class BookmarkTasksTestCase (line 27) | class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 28) | def setUp(self):
    method tearDown (line 68) | def tearDown(self):
    method executed_count (line 76) | def executed_count(self):
    method test_create_web_archive_snapshot_should_update_snapshot_url (line 79) | def test_create_web_archive_snapshot_should_update_snapshot_url(self):
    method test_create_web_archive_snapshot_should_handle_missing_bookmark_id (line 94) | def test_create_web_archive_snapshot_should_handle_missing_bookmark_id...
    method test_create_web_archive_snapshot_should_skip_if_snapshot_exists (line 100) | def test_create_web_archive_snapshot_should_skip_if_snapshot_exists(se...
    method test_create_web_archive_snapshot_should_force_update_snapshot (line 110) | def test_create_web_archive_snapshot_should_force_update_snapshot(self):
    method test_create_web_archive_snapshot_should_not_save_stale_bookmark_data (line 121) | def test_create_web_archive_snapshot_should_not_save_stale_bookmark_da...
    method test_create_web_archive_snapshot_should_not_run_when_background_tasks_are_disabled (line 144) | def test_create_web_archive_snapshot_should_not_run_when_background_ta...
    method test_create_web_archive_snapshot_should_not_run_when_web_archive_integration_is_disabled (line 154) | def test_create_web_archive_snapshot_should_not_run_when_web_archive_i...
    method test_load_favicon_should_create_favicon_file (line 169) | def test_load_favicon_should_create_favicon_file(self):
    method test_load_favicon_should_update_favicon_file (line 178) | def test_load_favicon_should_update_favicon_file(self):
    method test_load_favicon_should_handle_missing_bookmark (line 189) | def test_load_favicon_should_handle_missing_bookmark(self):
    method test_load_favicon_should_not_save_stale_bookmark_data (line 194) | def test_load_favicon_should_not_save_stale_bookmark_data(self):
    method test_load_favicon_should_not_run_when_background_tasks_are_disabled (line 213) | def test_load_favicon_should_not_run_when_background_tasks_are_disable...
    method test_load_favicon_should_not_run_when_favicon_feature_is_disabled (line 219) | def test_load_favicon_should_not_run_when_favicon_feature_is_disabled(...
    method test_schedule_bookmarks_without_favicons_should_load_favicon_for_all_bookmarks_without_favicon (line 228) | def test_schedule_bookmarks_without_favicons_should_load_favicon_for_a...
    method test_schedule_bookmarks_without_favicons_should_only_update_user_owned_bookmarks (line 244) | def test_schedule_bookmarks_without_favicons_should_only_update_user_o...
    method test_schedule_bookmarks_without_favicons_should_not_run_when_background_tasks_are_disabled (line 263) | def test_schedule_bookmarks_without_favicons_should_not_run_when_backg...
    method test_schedule_bookmarks_without_favicons_should_not_run_when_favicon_feature_is_disabled (line 271) | def test_schedule_bookmarks_without_favicons_should_not_run_when_favic...
    method test_schedule_refresh_favicons_should_update_favicon_for_all_bookmarks (line 282) | def test_schedule_refresh_favicons_should_update_favicon_for_all_bookm...
    method test_schedule_refresh_favicons_should_only_update_user_owned_bookmarks (line 296) | def test_schedule_refresh_favicons_should_only_update_user_owned_bookm...
    method test_schedule_refresh_favicons_should_not_run_when_background_tasks_are_disabled (line 313) | def test_schedule_refresh_favicons_should_not_run_when_background_task...
    method test_schedule_refresh_favicons_should_not_run_when_refresh_is_disabled (line 322) | def test_schedule_refresh_favicons_should_not_run_when_refresh_is_disa...
    method test_schedule_refresh_favicons_should_not_run_when_favicon_feature_is_disabled (line 328) | def test_schedule_refresh_favicons_should_not_run_when_favicon_feature...
    method test_load_preview_image_should_create_preview_image_file (line 339) | def test_load_preview_image_should_create_preview_image_file(self):
    method test_load_preview_image_should_update_preview_image_file (line 348) | def test_load_preview_image_should_update_preview_image_file(self):
    method test_load_preview_image_should_set_blank_when_none_is_returned (line 361) | def test_load_preview_image_should_set_blank_when_none_is_returned(self):
    method test_load_preview_image_should_handle_missing_bookmark (line 374) | def test_load_preview_image_should_handle_missing_bookmark(self):
    method test_load_preview_image_should_not_save_stale_bookmark_data (line 379) | def test_load_preview_image_should_not_save_stale_bookmark_data(self):
    method test_load_preview_image_should_not_run_when_background_tasks_are_disabled (line 398) | def test_load_preview_image_should_not_run_when_background_tasks_are_d...
    method test_load_preview_image_should_not_run_when_preview_image_feature_is_disabled (line 404) | def test_load_preview_image_should_not_run_when_preview_image_feature_...
    method test_schedule_bookmarks_without_previews_should_load_preview_for_all_bookmarks_without_preview (line 415) | def test_schedule_bookmarks_without_previews_should_load_preview_for_a...
    method test_schedule_bookmarks_without_previews_should_only_update_user_owned_bookmarks (line 431) | def test_schedule_bookmarks_without_previews_should_only_update_user_o...
    method test_schedule_bookmarks_without_previews_should_not_run_when_background_tasks_are_disabled (line 450) | def test_schedule_bookmarks_without_previews_should_not_run_when_backg...
    method test_schedule_bookmarks_without_previews_should_not_run_when_preview_feature_is_disabled (line 458) | def test_schedule_bookmarks_without_previews_should_not_run_when_previ...
    method test_create_html_snapshot_should_create_pending_asset (line 470) | def test_create_html_snapshot_should_create_pending_asset(self):
    method test_schedule_html_snapshots_should_create_snapshots (line 492) | def test_schedule_html_snapshots_should_create_snapshots(self):
    method test_create_html_snapshot_should_handle_missing_asset (line 510) | def test_create_html_snapshot_should_handle_missing_asset(self):
    method test_create_html_snapshot_should_not_create_asset_when_single_file_is_disabled (line 516) | def test_create_html_snapshot_should_not_create_asset_when_single_file...
    method test_create_html_snapshot_should_not_create_asset_when_background_tasks_are_disabled (line 525) | def test_create_html_snapshot_should_not_create_asset_when_background_...
    method test_create_missing_html_snapshots (line 534) | def test_create_missing_html_snapshots(self):
    method test_create_missing_html_snapshots_respects_current_user (line 604) | def test_create_missing_html_snapshots_respects_current_user(self):
    method test_refresh_metadata_task_not_called_when_background_tasks_disabled (line 620) | def test_refresh_metadata_task_not_called_when_background_tasks_disabl...
    method test_refresh_metadata_task_called_when_background_tasks_enabled (line 629) | def test_refresh_metadata_task_called_when_background_tasks_enabled(se...
    method test_refresh_metadata_task_should_handle_missing_bookmark (line 637) | def test_refresh_metadata_task_should_handle_missing_bookmark(self):
    method test_refresh_metadata_updates_title_description (line 645) | def test_refresh_metadata_updates_title_description(self):

FILE: bookmarks/tests/test_bundles_api.py
  class BundlesApiTestCase (line 8) | class BundlesApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
    method assertBundle (line 9) | def assertBundle(self, bundle: BookmarkBundle, data: dict):
    method test_bundle_list (line 27) | def test_bundle_list(self):
    method test_bundle_list_only_returns_own_bundles (line 44) | def test_bundle_list_only_returns_own_bundles(self):
    method test_bundle_list_requires_authentication (line 63) | def test_bundle_list_requires_authentication(self):
    method test_bundle_detail (line 67) | def test_bundle_detail(self):
    method test_bundle_detail_only_returns_own_bundles (line 86) | def test_bundle_detail_only_returns_own_bundles(self):
    method test_bundle_detail_requires_authentication (line 95) | def test_bundle_detail_requires_authentication(self):
    method test_create_bundle (line 100) | def test_create_bundle(self):
    method test_create_bundle_auto_increments_order (line 131) | def test_create_bundle_auto_increments_order(self):
    method test_create_bundle_with_custom_order (line 146) | def test_create_bundle_with_custom_order(self):
    method test_create_bundle_requires_name (line 159) | def test_create_bundle_requires_name(self):
    method test_create_bundle_fields_can_be_empty (line 167) | def test_create_bundle_fields_can_be_empty(self):
    method test_create_bundle_requires_authentication (line 190) | def test_create_bundle_requires_authentication(self):
    method test_update_bundle_put (line 196) | def test_update_bundle_put(self):
    method test_update_bundle_patch (line 232) | def test_update_bundle_patch(self):
    method test_update_bundle_only_allows_own_bundles (line 256) | def test_update_bundle_only_allows_own_bundles(self):
    method test_update_bundle_requires_authentication (line 267) | def test_update_bundle_requires_authentication(self):
    method test_delete_bundle (line 274) | def test_delete_bundle(self):
    method test_delete_bundle_updates_order (line 284) | def test_delete_bundle_updates_order(self):
    method test_delete_bundle_only_allows_own_bundles (line 302) | def test_delete_bundle_only_allows_own_bundles(self):
    method test_delete_bundle_requires_authentication (line 313) | def test_delete_bundle_requires_authentication(self):
    method test_bundles_ordered_by_order_field (line 320) | def test_bundles_ordered_by_order_field(self):

FILE: bookmarks/tests/test_bundles_edit_view.py
  class BundleEditViewTestCase (line 8) | class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 9) | def setUp(self) -> None:
    method create_form_data (line 13) | def create_form_data(self, overrides=None):
    method test_should_edit_bundle (line 27) | def test_should_edit_bundle(self):
    method test_should_render_edit_form_with_prefilled_fields (line 47) | def test_should_render_edit_form_with_prefilled_fields(self):
    method test_should_return_422_with_invalid_form (line 129) | def test_should_return_422_with_invalid_form(self):
    method test_should_not_allow_editing_other_users_bundles (line 146) | def test_should_not_allow_editing_other_users_bundles(self):
    method test_should_show_correct_preview (line 161) | def test_should_show_correct_preview(self):
    method test_should_show_correct_preview_after_posting_invalid_data (line 174) | def test_should_show_correct_preview_after_posting_invalid_data(self):

FILE: bookmarks/tests/test_bundles_index_view.py
  class BundleIndexViewTestCase (line 9) | class BundleIndexViewTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 10) | def setUp(self) -> None:
    method test_render_bundle_list (line 14) | def test_render_bundle_list(self):
    method test_renders_user_owned_bundles_only (line 46) | def test_renders_user_owned_bundles_only(self):
    method test_empty_state (line 60) | def test_empty_state(self):
    method test_add_new_button (line 72) | def test_add_new_button(self):
    method test_remove_bundle (line 83) | def test_remove_bundle(self):
    method test_remove_bundle_updates_order (line 96) | def test_remove_bundle_updates_order(self):
    method test_remove_other_user_bundle (line 108) | def test_remove_other_user_bundle(self):
    method assertBundleOrder (line 120) | def assertBundleOrder(self, expected_bundles, user=None):
    method move_bundle (line 129) | def move_bundle(self, bundle: BookmarkBundle, position: int):
    method test_move_bundle (line 135) | def test_move_bundle(self):
    method test_move_bundle_response (line 152) | def test_move_bundle_response(self):
    method test_can_only_move_user_owned_bundles (line 161) | def test_can_only_move_user_owned_bundles(self):
    method test_move_bundle_only_affects_own_bundles (line 169) | def test_move_bundle_only_affects_own_bundles(self):
    method test_remove_non_existing_bundle (line 185) | def test_remove_non_existing_bundle(self):
    method test_post_without_action (line 195) | def test_post_without_action(self):

FILE: bookmarks/tests/test_bundles_new_view.py
  class BundleNewViewTestCase (line 10) | class BundleNewViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
    method setUp (line 11) | def setUp(self) -> None:
    method create_form_data (line 15) | def create_form_data(self, overrides=None):
    method test_should_create_new_bundle (line 29) | def test_should_create_new_bundle(self):
    method test_should_increment_order_for_subsequent_bundles (line 48) | def test_should_increment_order_for_subsequent_bundles(self):
    method test_incrementing_order_ignores_other_user_bookmark (line 70) | def test_incrementing_order_ignores_other_user_bookmark(self):
    method test_should_return_422_with_invalid_form (line 79) | def test_should_return_422_with_invalid_form(self):
    method test_should_prefill_form_from_search_query_parameters (line 84) | def test_should_prefill_form_from_search_query_parameters(self):
    method test_should_ignore_special_search_commands (line 96) | def test_should_ignore_special_search_commands(self):
    method test_should_not_prefill_when_no_query_parameter (line 108) | def test_should_not_prefill_when_no_query_parameter(self):
    method test_should_not_prefill_when_editing_existing_bundle (line 118) | def test_should_not_prefill_when_editing_existing_bundle(self):
    method test_should_show_correct_preview_with_prefilled_values (line 138) | def test_should_show_correct_preview_with_prefilled_values(self):

FILE: bookmarks/tests/test_bundles_preview_view.py
  class BundlePreviewViewTestCase (line 8) | class BundlePreviewViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTest...
    method setUp (line 9) | def setUp(self) -> None:
    method test_preview_empty_bundle (line 13) | def test_preview_empty_bundle(self):
    method test_preview_with_search_terms (line 25) | def test_preview_with_search_terms(self):
    method test_preview_no_matching_bookmarks (line 40) | def test_preview_no_matching_bookmarks(self):
    method test_preview_renders_bookmark (line 51) | def test_preview_renders_bookmark(self):
    method test_preview_renders_bookmark_in_preview_mode (line 68) | def test_preview_renders_bookmark_in_preview_mode(self):
    method test_preview_ignores_archived_bookmarks (line 84) | def test_preview_ignores_archived_bookmarks(self):
    method test_preview_with_filter_unread (line 97) | def test_preview_with_filter_unread(self):
    method test_preview_with_filter_shared (line 128) | def test_preview_with_filter_shared(self):
    method test_preview_requires_authentication (line 159) | def test_preview_requires_authentication(self):
    method test_preview_only_shows_user_bookmarks (line 168) | def test_preview_only_shows_user_bookmarks(self):

FILE: bookmarks/tests/test_context_path.py
  class MockUrlConf (line 7) | class MockUrlConf:
    method __init__ (line 8) | def __init__(self, module):
  class ContextPathTestCase (line 12) | class ContextPathTestCase(TestCase):
    method setUp (line 13) | def setUp(self):
    method tearDown (line 17) | def tearDown(self):
    method test_route_with_context_path (line 21) | def test_route_with_context_path(self):
    method test_route_without_context_path (line 41) | def test_route_without_context_path(self):

FILE: bookmarks/tests/test_create_initial_superuser_command.py
  class TestCreateInitialSuperuserCommand (line 10) | class TestCreateInitialSuperuserCommand(TestCase):
    method test_create_with_password (line 15) | def test_create_with_password(self):
    method test_create_without_password (line 26) | def test_create_without_password(self):
    method test_create_without_options (line 35) | def test_create_without_options(self):
    method test_create_multiple_times (line 44) | def test_create_multiple_times(self):

FILE: bookmarks/tests/test_custom_css_view.py
  class CustomCssViewTestCase (line 7) | class CustomCssViewTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 8) | def setUp(self) -> None:
    method test_with_empty_css (line 12) | def test_with_empty_css(self):
    method test_with_custom_css (line 19) | def test_with_custom_css(self):

FILE: bookmarks/tests/test_exporter.py
  class ExporterTestCase (line 9) | class ExporterTestCase(TestCase, BookmarkFactoryMixin):
    method test_export_bookmarks (line 10) | def test_export_bookmarks(self):
    method test_escape_html (line 94) | def test_escape_html(self):
    method test_handle_empty_values (line 109) | def test_handle_empty_values(self):

FILE: bookmarks/tests/test_exporter_performance.py
  class ExporterPerformanceTestCase (line 10) | class ExporterPerformanceTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 11) | def setUp(self) -> None:
    method get_connection (line 15) | def get_connection(self):
    method test_export_max_queries (line 18) | def test_export_max_queries(self):

FILE: bookmarks/tests/test_favicon_loader.py
  class MockStreamingResponse (line 16) | class MockStreamingResponse:
    method __init__ (line 17) | def __init__(self, data=mock_icon_data, content_type="image/png"):
    method iter_content (line 21) | def iter_content(self, **kwargs):
    method __enter__ (line 24) | def __enter__(self):
    method __exit__ (line 27) | def __exit__(self, exc_type, exc_value, traceback):
  class FaviconLoaderTestCase (line 31) | class FaviconLoaderTestCase(TestCase):
    method setUp (line 32) | def setUp(self) -> None:
    method tearDown (line 39) | def tearDown(self) -> None:
    method create_mock_response (line 43) | def create_mock_response(self, icon_data=mock_icon_data, content_type=...
    method clear_favicon_folder (line 48) | def clear_favicon_folder(self):
    method get_icon_path (line 53) | def get_icon_path(self, filename):
    method icon_exists (line 56) | def icon_exists(self, filename):
    method get_icon_data (line 59) | def get_icon_data(self, filename):
    method count_icons (line 62) | def count_icons(self):
    method test_load_favicon (line 66) | def test_load_favicon(self):
    method test_load_favicon_creates_folder_if_not_exists (line 79) | def test_load_favicon_creates_folder_if_not_exists(self):
    method test_load_favicon_creates_single_icon_for_same_base_url (line 92) | def test_load_favicon_creates_single_icon_for_same_base_url(self):
    method test_load_favicon_creates_multiple_icons_for_different_base_url (line 102) | def test_load_favicon_creates_multiple_icons_for_different_base_url(se...
    method test_load_favicon_caches_icons (line 114) | def test_load_favicon_caches_icons(self):
    method test_load_favicon_updates_stale_icon (line 127) | def test_load_favicon_updates_stale_icon(self):
    method test_custom_provider_with_url_param (line 158) | def test_custom_provider_with_url_param(self):
    method test_custom_provider_with_domain_param (line 168) | def test_custom_provider_with_domain_param(self):
    method test_guess_file_extension (line 177) | def test_guess_file_extension(self):

FILE: bookmarks/tests/test_feeds.py
  function rfc2822_date (line 15) | def rfc2822_date(date):
  class FeedsTestCase (line 21) | class FeedsTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 22) | def setUp(self) -> None:
    method assertFeedItems (line 27) | def assertFeedItems(self, response, bookmarks):
    method test_all_returns_404_for_unknown_feed_token (line 54) | def test_all_returns_404_for_unknown_feed_token(self):
    method test_all_metadata (line 59) | def test_all_metadata(self):
    method test_all_returns_all_unarchived_bookmarks (line 71) | def test_all_returns_all_unarchived_bookmarks(self):
    method test_all_returns_only_user_owned_bookmarks (line 85) | def test_all_returns_only_user_owned_bookmarks(self):
    method test_unread_returns_404_for_unknown_feed_token (line 98) | def test_unread_returns_404_for_unknown_feed_token(self):
    method test_unread_metadata (line 103) | def test_unread_metadata(self):
    method test_unread_returns_unread_and_unarchived_bookmarks (line 115) | def test_unread_returns_unread_and_unarchived_bookmarks(self):
    method test_unread_returns_only_user_owned_bookmarks (line 135) | def test_unread_returns_only_user_owned_bookmarks(self):
    method test_shared_returns_404_for_unknown_feed_token (line 150) | def test_shared_returns_404_for_unknown_feed_token(self):
    method test_shared_metadata (line 155) | def test_shared_metadata(self):
    method test_shared_returns_shared_bookmarks_only (line 167) | def test_shared_returns_shared_bookmarks_only(self):
    method test_public_shared_does_not_require_auth (line 187) | def test_public_shared_does_not_require_auth(self):
    method test_public_shared_metadata (line 192) | def test_public_shared_metadata(self):
    method test_public_shared_returns_publicly_shared_bookmarks_only (line 206) | def test_public_shared_returns_publicly_shared_bookmarks_only(self):
    method test_with_query (line 227) | def test_with_query(self):
    method test_unread_parameter (line 258) | def test_unread_parameter(self):
    method test_shared_parameter (line 285) | def test_shared_parameter(self):
    method test_with_tags (line 312) | def test_with_tags(self):
    method test_with_limit (line 325) | def test_with_limit(self):
    method test_strip_control_characters (line 351) | def test_strip_control_characters(self):
    method test_sanitize_with_none_text (line 363) | def test_sanitize_with_none_text(self):
    method test_with_bundle (line 366) | def test_with_bundle(self):
    method test_with_bundle_not_owned_by_user (line 385) | def test_with_bundle_not_owned_by_user(self):
    method test_with_invalid_bundle_id (line 397) | def test_with_invalid_bundle_id(self):
    method test_with_non_numeric_bundle_id (line 405) | def test_with_non_numeric_bundle_id(self):

FILE: bookmarks/tests/test_feeds_performance.py
  class FeedsPerformanceTestCase (line 11) | class FeedsPerformanceTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 12) | def setUp(self) -> None:
    method get_connection (line 20) | def get_connection(self):
    method test_all_max_queries (line 23) | def test_all_max_queries(self):

FILE: bookmarks/tests/test_health_view.py
  class HealthViewTestCase (line 9) | class HealthViewTestCase(TestCase):
    method test_health_healthy (line 10) | def test_health_healthy(self):
    method test_health_unhealhty (line 19) | def test_health_unhealhty(self):

FILE: bookmarks/tests/test_importer.py
  class ImporterTestCase (line 18) | class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
    method assertBookmarksImported (line 19) | def assertBookmarksImported(self, html_tags: list[BookmarkHtmlTag]):
    method test_import (line 42) | def test_import(self):
    method test_synchronize (line 90) | def test_synchronize(self):
    method test_import_with_some_invalid_bookmarks (line 210) | def test_import_with_some_invalid_bookmarks(self):
    method test_import_invalid_bookmark_does_not_associate_tags (line 231) | def test_import_invalid_bookmark_does_not_associate_tags(self):
    method test_import_tags (line 246) | def test_import_tags(self):
    method test_create_missing_tags (line 257) | def test_create_missing_tags(self):
    method test_create_missing_tags_does_not_duplicate_tags (line 272) | def test_create_missing_tags_does_not_duplicate_tags(self):
    method test_should_append_tags_to_bookmark_when_reimporting_with_different_tags (line 283) | def test_should_append_tags_to_bookmark_when_reimporting_with_differen...
    method test_use_current_date_when_no_add_date (line 298) | def test_use_current_date_when_no_add_date(self):
    method test_use_add_date_when_no_last_modified (line 314) | def test_use_add_date_when_no_last_modified(self):
    method test_keep_title_if_imported_bookmark_has_empty_title (line 327) | def test_keep_title_if_imported_bookmark_has_empty_title(self):
    method test_keep_description_if_imported_bookmark_has_empty_description (line 339) | def test_keep_description_if_imported_bookmark_has_empty_description(s...
    method test_replace_whitespace_in_tag_names (line 353) | def test_replace_whitespace_in_tag_names(self):
    method test_ignore_long_tag_names (line 367) | def test_ignore_long_tag_names(self):
    method test_validate_empty_or_missing_bookmark_url (line 394) | def test_validate_empty_or_missing_bookmark_url(self):
    method test_generate_normalized_url (line 410) | def test_generate_normalized_url(self):
    method test_private_flag (line 425) | def test_private_flag(self):
    method test_archived_state (line 458) | def test_archived_state(self):
    method test_notes (line 481) | def test_notes(self):
    method test_schedule_favicon_loading (line 521) | def test_schedule_favicon_loading(self):
    method test_schedule_preview_loading (line 532) | def test_schedule_preview_loading(self):

FILE: bookmarks/tests/test_layout.py
  class LayoutTestCase (line 8) | class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
    method setUp (line 9) | def setUp(self) -> None:
    method test_nav_menu_should_respect_share_profile_setting (line 13) | def test_nav_menu_should_respect_share_profile_setting(self):
    method test_metadata_should_respect_prefetch_links_setting (line 54) | def test_metadata_should_respect_prefetch_links_setting(self):
    method test_does_not_link_custom_css_when_empty (line 80) | def test_does_not_link_custom_css_when_empty(self):
    method test_does_link_custom_css_when_not_empty (line 88) | def test_does_link_custom_css_when_not_empty(self):
    method test_custom_css_link_href (line 100) | def test_custom_css_link_href(self):

FILE: bookmarks/tests/test_linkding_middleware.py
  class LinkdingMiddlewareTestCase (line 9) | class LinkdingMiddlewareTestCase(TestCase, BookmarkFactoryMixin):
    method test_unauthenticated_user_should_use_standard_profile_by_default (line 10) | def test_unauthenticated_user_should_use_standard_profile_by_default(s...
    method test_unauthenticated_user_should_use_custom_configured_profile (line 15) | def test_unauthenticated_user_should_use_custom_configured_profile(self):
    method test_authenticated_user_should_use_own_profile (line 29) | def test_authenticated_user_should_use_own_profile(self):

FILE: bookmarks/tests/test_login_view.py
  class LoginViewTestCase (line 12) | class LoginViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
    method test_failed_login_should_return_401 (line 13) | def test_failed_login_should_return_401(self):
    method test_successful_login_should_redirect (line 17) | def test_successful_login_should_redirect(self):
    method test_should_not_show_oidc_login_by_default (line 25) | def test_should_not_show_oidc_login_by_default(self):
    method test_should_show_oidc_login_when_enabled (line 34) | def test_should_show_oidc_login_when_enabled(self):
    method test_should_show_login_form_by_default (line 45) | def test_should_show_login_form_by_default(self):
    method test_should_hide_login_form_when_disabled (line 60) | def test_should_hide_login_form_when_disabled(self):
    method test_should_only_show_oidc_login_when_login_disabled_and_oidc_enabled (line 75) | def test_should_only_show_oidc_login_when_login_disabled_and_oidc_enab...

FILE: bookmarks/tests/test_metadata_view.py
  class MetadataViewTestCase (line 4) | class MetadataViewTestCase(TestCase):
    method test_default_manifest (line 5) | def test_default_manifest(self):
    method test_manifest_respects_context_path (line 102) | def test_manifest_respects_context_path(self):

FILE: bookmarks/tests/test_monolith_service.py
  class MonolithServiceTestCase (line 11) | class MonolithServiceTestCase(TestCase):
    method tearDown (line 16) | def tearDown(self):
    method create_test_file (line 22) | def create_test_file(self, *args, **kwargs):
    method test_create_snapshot (line 26) | def test_create_snapshot(self):
    method test_create_snapshot_failure (line 39) | def test_create_snapshot_failure(self):

FILE: bookmarks/tests/test_oidc_support.py
  class OidcSupportTest (line 10) | class OidcSupportTest(TestCase):
    method test_should_not_add_oidc_urls_by_default (line 11) | def test_should_not_add_oidc_urls_by_default(self):
    method test_should_add_oidc_urls_when_enabled (line 22) | def test_should_add_oidc_urls_when_enabled(self):
    method test_should_not_add_oidc_authentication_backend_by_default (line 32) | def test_should_not_add_oidc_authentication_backend_by_default(self):
    method test_should_add_oidc_authentication_backend_when_enabled (line 41) | def test_should_add_oidc_authentication_backend_when_enabled(self):
    method test_default_settings (line 55) | def test_default_settings(self):
    method test_username_should_use_email_by_default (line 67) | def test_username_should_use_email_by_default(self):
    method test_username_should_use_custom_claim (line 82) | def test_username_should_use_custom_claim(self):
    method test_username_should_fallback_to_email_for_non_existing_claim (line 97) | def test_username_should_fallback_to_email_for_non_existing_claim(self):
    method test_username_should_fallback_to_email_for_empty_claim (line 112) | def test_username_should_fallback_to_email_for_empty_claim(self):
    method test_username_should_be_normalized (line 127) | def test_username_should_be_normalized(self):

FILE: bookmarks/tests/test_opensearch_view.py
  class OpenSearchViewTestCase (line 5) | class OpenSearchViewTestCase(TestCase):
    method test_opensearch_configuration (line 6) | def test_opensearch_configuration(self):

FILE: bookmarks/tests/test_pagination_tag.py
  class PaginationTagTest (line 8) | class PaginationTagTest(TestCase, BookmarkFactoryMixin):
    method render_template (line 9) | def render_template(
    method assertPrevLinkDisabled (line 32) | def assertPrevLinkDisabled(self, html: str):
    method assertPrevLink (line 42) | def assertPrevLink(
    method assertNextLinkDisabled (line 55) | def assertNextLinkDisabled(self, html: str):
    method assertNextLink (line 65) | def assertNextLink(
    method assertPageLink (line 78) | def assertPageLink(
    method assertTruncationIndicators (line 99) | def assertTruncationIndicators(self, html: str, count: int):
    method test_previous_disabled_on_page_1 (line 110) | def test_previous_disabled_on_page_1(self):
    method test_previous_enabled_after_page_1 (line 114) | def test_previous_enabled_after_page_1(self):
    method test_next_disabled_on_last_page (line 119) | def test_next_disabled_on_last_page(self):
    method test_next_enabled_before_last_page (line 123) | def test_next_enabled_before_last_page(self):
    method test_truncate_pages_start (line 128) | def test_truncate_pages_start(self):
    method test_truncate_pages_middle (line 142) | def test_truncate_pages_middle(self):
    method test_truncate_pages_near_end (line 156) | def test_truncate_pages_near_end(self):
    method test_respects_search_parameters (line 170) | def test_respects_search_parameters(self):
    method test_removes_details_parameter (line 197) | def test_removes_details_parameter(self):
    method test_respects_pagination_frame (line 206) | def test_respects_pagination_frame(self):

FILE: bookmarks/tests/test_parser.py
  class ParserTestCase (line 8) | class ParserTestCase(TestCase, ImportTestMixin):
    method assertTagsEqual (line 9) | def assertTagsEqual(
    method test_parse_bookmarks (line 24) | def test_parse_bookmarks(self):
    method test_no_bookmarks (line 63) | def test_no_bookmarks(self):
    method test_reset_properties_after_adding_bookmark (line 69) | def test_reset_properties_after_adding_bookmark(self):
    method test_empty_title (line 93) | def test_empty_title(self):
    method test_with_closing_description_tag (line 113) | def test_with_closing_description_tag(self):
    method test_description_tag_before_anchor_tag (line 142) | def test_description_tag_before_anchor_tag(self):
    method test_with_folders (line 171) | def test_with_folders(self):
    method test_private_flag (line 207) | def test_private_flag(self):
    method test_notes (line 238) | def test_notes(self):
    method test_unescape_content (line 308) | def test_unescape_content(self):
    method test_unescape_href_attribute (line 325) | def test_unescape_href_attribute(self):

FILE: bookmarks/tests/test_password_change_view.py
  class PasswordChangeViewTestCase (line 8) | class PasswordChangeViewTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 9) | def setUp(self) -> None:
    method test_change_password (line 15) | def test_change_password(self):
    method test_change_password_done (line 26) | def test_change_password_done(self):
    method test_should_return_error_for_invalid_old_password (line 37) | def test_should_return_error_for_invalid_old_password(self):
    method test_should_return_error_for_mismatching_new_password (line 49) | def test_should_return_error_for_mismatching_new_password(self):

FILE: bookmarks/tests/test_preview_image_loader.py
  class MockStreamingResponse (line 15) | class MockStreamingResponse:
    method __init__ (line 16) | def __init__(
    method iter_content (line 31) | def iter_content(self, **kwargs):
    method __enter__ (line 34) | def __enter__(self):
    method __exit__ (line 37) | def __exit__(self, exc_type, exc_value, traceback):
  class PreviewImageLoaderTestCase (line 41) | class PreviewImageLoaderTestCase(TestCase):
    method setUp (line 42) | def setUp(self) -> None:
    method tearDown (line 56) | def tearDown(self) -> None:
    method create_mock_response (line 61) | def create_mock_response(
    method get_image_path (line 77) | def get_image_path(self, filename):
    method assertImageExists (line 80) | def assertImageExists(self, filename, data):
    method assertNoImageExists (line 84) | def assertNoImageExists(self):
    method test_load_preview_image (line 87) | def test_load_preview_image(self):
    method test_load_preview_image_returns_none_if_no_preview_image_detected (line 96) | def test_load_preview_image_returns_none_if_no_preview_image_detected(...
    method test_load_preview_image_returns_none_for_invalid_status_code (line 106) | def test_load_preview_image_returns_none_for_invalid_status_code(self):
    method test_load_preview_image_returns_none_if_content_length_exceeds_limit (line 120) | def test_load_preview_image_returns_none_if_content_length_exceeds_lim...
    method test_load_preview_image_returns_none_for_invalid_content_type (line 143) | def test_load_preview_image_returns_none_for_invalid_content_type(self):
    method test_load_preview_image_returns_none_if_download_exceeds_content_length (line 170) | def test_load_preview_image_returns_none_if_download_exceeds_content_l...
    method test_load_preview_image_creates_folder_if_not_exists (line 179) | def test_load_preview_image_creates_folder_if_not_exists(self):
    method test_guess_file_extension (line 192) | def test_guess_file_extension(self):

FILE: bookmarks/tests/test_queries.py
  class QueriesBasicTestCase (line 14) | class QueriesBasicTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 15) | def setUp(self):
    method setup_bookmark_search_data (line 18) | def setup_bookmark_search_data(self) -> None:
    method setup_tag_search_data (line 74) | def setup_tag_search_data(self):
    method assertQueryResult (line 156) | def assertQueryResult(self, query: QuerySet, item_lists: list[list]):
    method test_query_bookmarks_should_return_all_for_empty_query (line 165) | def test_query_bookmarks_should_return_all_for_empty_query(self):
    method test_query_bookmarks_should_search_single_term (line 183) | def test_query_bookmarks_should_search_single_term(self):
    method test_query_bookmarks_should_search_multiple_terms (line 198) | def test_query_bookmarks_should_search_multiple_terms(self):
    method test_query_bookmarks_should_search_single_tag (line 207) | def test_query_bookmarks_should_search_single_tag(self):
    method test_query_bookmarks_should_search_multiple_tags (line 219) | def test_query_bookmarks_should_search_multiple_tags(self):
    method test_query_bookmarks_should_search_multiple_tags_ignoring_casing (line 228) | def test_query_bookmarks_should_search_multiple_tags_ignoring_casing(s...
    method test_query_bookmarks_should_search_terms_and_tags_combined (line 237) | def test_query_bookmarks_should_search_terms_and_tags_combined(self):
    method test_query_bookmarks_in_strict_mode_should_not_search_tags_as_terms (line 246) | def test_query_bookmarks_in_strict_mode_should_not_search_tags_as_term...
    method test_query_bookmarks_in_lax_mode_should_search_tags_as_terms (line 257) | def test_query_bookmarks_in_lax_mode_should_search_tags_as_terms(self):
    method test_query_bookmarks_should_return_no_matches (line 306) | def test_query_bookmarks_should_return_no_matches(self):
    method test_query_bookmarks_should_not_return_archived_bookmarks (line 347) | def test_query_bookmarks_should_not_return_archived_bookmarks(self):
    method test_query_archived_bookmarks_should_not_return_unarchived_bookmarks (line 358) | def test_query_archived_bookmarks_should_not_return_unarchived_bookmar...
    method test_query_bookmarks_should_only_return_user_owned_bookmarks (line 371) | def test_query_bookmarks_should_only_return_user_owned_bookmarks(self):
    method test_query_archived_bookmarks_should_only_return_user_owned_bookmarks (line 386) | def test_query_archived_bookmarks_should_only_return_user_owned_bookma...
    method test_query_bookmarks_untagged_should_return_untagged_bookmarks_only (line 403) | def test_query_bookmarks_untagged_should_return_untagged_bookmarks_onl...
    method test_query_bookmarks_untagged_should_be_combinable_with_search_terms (line 414) | def test_query_bookmarks_untagged_should_be_combinable_with_search_ter...
    method test_query_bookmarks_untagged_should_not_be_combinable_with_tags (line 425) | def test_query_bookmarks_untagged_should_not_be_combinable_with_tags(s...
    method test_query_archived_bookmarks_untagged_should_return_untagged_bookmarks_only (line 436) | def test_query_archived_bookmarks_untagged_should_return_untagged_book...
    method test_query_archived_bookmarks_untagged_should_be_combinable_with_search_terms (line 449) | def test_query_archived_bookmarks_untagged_should_be_combinable_with_s...
    method test_query_archived_bookmarks_untagged_should_not_be_combinable_with_tags (line 462) | def test_query_archived_bookmarks_untagged_should_not_be_combinable_wi...
    method test_query_bookmarks_unread_should_return_unread_bookmarks_only (line 473) | def test_query_bookmarks_unread_should_return_unread_bookmarks_only(se...
    method test_query_archived_bookmarks_unread_should_return_unread_bookmarks_only (line 507) | def test_query_archived_bookmarks_unread_should_return_unread_bookmark...
    method test_query_bookmarks_filter_shared (line 541) | def test_query_bookmarks_filter_shared(self):
    method test_query_bookmark_tags_should_return_all_tags_for_empty_query (line 560) | def test_query_bookmark_tags_should_return_all_tags_for_empty_query(se...
    method test_query_bookmark_tags_should_search_single_term (line 580) | def test_query_bookmark_tags_should_search_single_term(self):
    method test_query_bookmark_tags_should_search_multiple_terms (line 596) | def test_query_bookmark_tags_should_search_multiple_terms(self):
    method test_query_bookmark_tags_should_search_single_tag (line 610) | def test_query_bookmark_tags_should_search_single_tag(self):
    method test_query_bookmark_tags_should_search_multiple_tags (line 626) | def test_query_bookmark_tags_should_search_multiple_tags(self):
    method test_query_bookmark_tags_should_search_multiple_tags_ignoring_casing (line 640) | def test_query_bookmark_tags_should_search_multiple_tags_ignoring_casi...
    method test_query_bookmark_tags_should_search_term_and_tag_combined (line 654) | def test_query_bookmark_tags_should_search_term_and_tag_combined(self):
    method test_query_bookmark_tags_in_strict_mode_should_not_search_tags_as_terms (line 668) | def test_query_bookmark_tags_in_strict_mode_should_not_search_tags_as_...
    method test_query_bookmark_tags_in_lax_mode_should_search_tags_as_terms (line 681) | def test_query_bookmark_tags_in_lax_mode_should_search_tags_as_terms(s...
    method test_query_bookmark_tags_should_return_no_matches (line 730) | def test_query_bookmark_tags_should_return_no_matches(self):
    method test_query_bookmark_tags_should_return_tags_for_unarchived_bookmarks_only (line 771) | def test_query_bookmark_tags_should_return_tags_for_unarchived_bookmar...
    method test_query_bookmark_tags_should_return_distinct_tags (line 784) | def test_query_bookmark_tags_should_return_distinct_tags(self):
    method test_query_archived_bookmark_tags_should_return_tags_for_archived_bookmarks_only (line 796) | def test_query_archived_bookmark_tags_should_return_tags_for_archived_...
    method test_query_archived_bookmark_tags_should_return_distinct_tags (line 811) | def test_query_archived_bookmark_tags_should_return_distinct_tags(self):
    method test_query_bookmark_tags_should_only_return_user_owned_tags (line 823) | def test_query_bookmark_tags_should_only_return_user_owned_tags(self):
    method test_query_archived_bookmark_tags_should_only_return_user_owned_tags (line 840) | def test_query_archived_bookmark_tags_should_only_return_user_owned_ta...
    method test_query_bookmark_tags_untagged_should_never_return_any_tags (line 863) | def test_query_bookmark_tags_untagged_should_never_return_any_tags(self):
    method test_query_archived_bookmark_tags_untagged_should_never_return_any_tags (line 885) | def test_query_archived_bookmark_tags_untagged_should_never_return_any...
    method test_query_bookmark_tags_filter_unread (line 907) | def test_query_bookmark_tags_filter_unread(self):
    method test_query_bookmark_tags_filter_shared (line 943) | def test_query_bookmark_tags_filter_shared(self):
    method test_query_shared_bookmarks (line 966) | def test_query_shared_bookmarks(self):
    method test_query_publicly_shared_bookmarks (line 1002) | def test_query_publicly_shared_bookmarks(self):
    method test_query_shared_bookmark_tags (line 1014) | def test_query_shared_bookmark_tags(self):
    method test_query_publicly_shared_bookmark_tags (line 1041) | def test_query_publicly_shared_bookmark_tags(self):
    method test_query_shared_bookmark_users (line 1057) | def test_query_shared_bookmark_users(self):
    method test_query_publicly_shared_bookmark_users (line 1093) | def test_query_publicly_shared_bookmark_users(self):
    method test_sorty_by_date_added_asc (line 1105) | def test_sorty_by_date_added_asc(self):
    method test_sorty_by_date_added_desc (line 1136) | def test_sorty_by_date_added_desc(self):
    method setup_title_sort_data (line 1167) | def setup_title_sort_data(self):
    method test_sort_by_title_asc (line 1189) | def test_sort_by_title_asc(self):
    method test_sort_by_title_desc (line 1202) | def test_sort_by_title_desc(self):
    method test_query_bookmarks_filter_modified_since (line 1217) | def test_query_bookmarks_filter_modified_since(self):
    method test_query_bookmarks_filter_added_since (line 1257) | def test_query_bookmarks_filter_added_since(self):
    method test_query_bookmarks_with_bundle_search_terms (line 1293) | def test_query_bookmarks_with_bundle_search_terms(self):
    method test_query_bookmarks_with_search_and_bundle_search_terms (line 1313) | def test_query_bookmarks_with_search_and_bundle_search_terms(self):
    method test_query_bookmarks_with_bundle_any_tags (line 1331) | def test_query_bookmarks_with_bundle_any_tags(self):
    method test_query_bookmarks_with_search_tags_and_bundle_any_tags (line 1353) | def test_query_bookmarks_with_search_tags_and_bundle_any_tags(self):
    method test_query_bookmarks_with_bundle_all_tags (line 1385) | def test_query_bookmarks_with_bundle_all_tags(self):
    method test_query_bookmarks_with_search_tags_and_bundle_all_tags (line 1406) | def test_query_bookmarks_with_search_tags_and_bundle_all_tags(self):
    method test_query_bookmarks_with_bundle_excluded_tags (line 1434) | def test_query_bookmarks_with_bundle_excluded_tags(self):
    method test_query_bookmarks_with_bundle_combined_tags (line 1462) | def test_query_bookmarks_with_bundle_combined_tags(self):
    method test_query_bookmarks_with_bundle_filter_unread (line 1504) | def test_query_bookmarks_with_bundle_filter_unread(self):
    method test_query_bookmarks_with_bundle_filter_shared (line 1535) | def test_query_bookmarks_with_bundle_filter_shared(self):
    method test_query_bookmarks_with_bundle_unread_shared_filters_combined (line 1566) | def test_query_bookmarks_with_bundle_unread_shared_filters_combined(se...
    method test_query_archived_bookmarks_with_bundle (line 1587) | def test_query_archived_bookmarks_with_bundle(self):
    method test_query_shared_bookmarks_with_bundle (line 1612) | def test_query_shared_bookmarks_with_bundle(self):
  class QueriesLegacySearchTestCase (line 1643) | class QueriesLegacySearchTestCase(QueriesBasicTestCase):
    method setUp (line 1644) | def setUp(self):
  class QueriesAdvancedSearchTestCase (line 1650) | class QueriesAdvancedSearchTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 1651) | def setUp(self):
    method test_explicit_and_operator (line 1677) | def test_explicit_and_operator(self):
    method test_or_operator (line 1682) | def test_or_operator(self):
    method test_not_operator (line 1690) | def test_not_operator(self):
    method test_implicit_and_between_terms (line 1695) | def test_implicit_and_between_terms(self):
    method test_implicit_and_between_tags (line 1704) | def test_implicit_and_between_tags(self):
    method test_nested_and_expression (line 1709) | def test_nested_and_expression(self):
    method test_mixed_terms_and_tags_with_operators (line 1722) | def test_mixed_terms_and_tags_with_operators(self):
    method test_parentheses (line 1733) | def test_parentheses(self):
    method test_complex_query_with_all_operators (line 1752) | def test_complex_query_with_all_operators(self):
    method test_quoted_strings_with_operators (line 1765) | def test_quoted_strings_with_operators(self):
    method test_implicit_and_with_quoted_strings (line 1777) | def test_implicit_and_with_quoted_strings(self):
    method test_empty_query (line 1782) | def test_empty_query(self):
    method test_unparseable_query_returns_no_results (line 1795) | def test_unparseable_query_returns_no_results(self):
  class GetTagsForQueryTestCase (line 1802) | class GetTagsForQueryTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 1803) | def setUp(self):
    method test_returns_tags_matching_query (line 1807) | def test_returns_tags_matching_query(self):
    method test_case_insensitive_matching (line 1817) | def test_case_insensitive_matching(self):
    method test_lax_mode_includes_terms (line 1829) | def test_lax_mode_includes_terms(self):
    method test_strict_mode_excludes_terms (line 1841) | def test_strict_mode_excludes_terms(self):
    method test_only_returns_user_tags (line 1850) | def test_only_returns_user_tags(self):
    method test_empty_query_returns_no_tags (line 1864) | def test_empty_query_returns_no_tags(self):
    method test_query_with_no_tags_returns_empty (line 1870) | def test_query_with_no_tags_returns_empty(self):
    method test_nonexistent_tag_returns_empty (line 1876) | def test_nonexistent_tag_returns_empty(self):
  class GetSharedTagsForQueryTestCase (line 1883) | class GetSharedTagsForQueryTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 1884) | def setUp(self):
    method test_returns_tags_from_shared_bookmarks (line 1890) | def test_returns_tags_from_shared_bookmarks(self):
    method test_excludes_tags_from_non_shared_bookmarks (line 1900) | def test_excludes_tags_from_non_shared_bookmarks(self):
    method test_respects_sharing_enabled_setting (line 1910) | def test_respects_sharing_enabled_setting(self):
    method test_public_only_flag (line 1923) | def test_public_only_flag(self):
    method test_filters_by_user (line 1943) | def test_filters_by_user(self):

FILE: bookmarks/tests/test_root_view.py
  class RootViewTestCase (line 8) | class RootViewTestCase(TestCase, BookmarkFactoryMixin):
    method test_unauthenticated_user_redirect_to_login_by_default (line 9) | def test_unauthenticated_user_redirect_to_login_by_default(self):
    method test_unauthenticated_redirect_to_shared_bookmarks_if_configured_in_global_settings (line 13) | def test_unauthenticated_redirect_to_shared_bookmarks_if_configured_in...
    method test_authenticated_user_always_redirected_to_bookmarks (line 23) | def test_authenticated_user_always_redirected_to_bookmarks(self):

FILE: bookmarks/tests/test_search_query_parser.py
  function _term (line 22) | def _term(term: str) -> TermExpression:
  function _tag (line 26) | def _tag(tag: str) -> TagExpression:
  function _and (line 30) | def _and(left: SearchExpression, right: SearchExpression) -> AndExpression:
  function _or (line 34) | def _or(left: SearchExpression, right: SearchExpression) -> OrExpression:
  function _not (line 38) | def _not(operand: SearchExpression) -> NotExpression:
  function _keyword (line 42) | def _keyword(keyword: str) -> SpecialKeywordExpression:
  class SearchQueryTokenizerTest (line 46) | class SearchQueryTokenizerTest(TestCase):
    method test_empty_query (line 47) | def test_empty_query(self):
    method test_whitespace_only_query (line 53) | def test_whitespace_only_query(self):
    method test_single_term (line 59) | def test_single_term(self):
    method test_multiple_terms (line 67) | def test_multiple_terms(self):
    method test_hyphenated_term (line 79) | def test_hyphenated_term(self):
    method test_and_operator (line 87) | def test_and_operator(self):
    method test_or_operator (line 99) | def test_or_operator(self):
    method test_not_operator (line 111) | def test_not_operator(self):
    method test_case_insensitive_operators (line 123) | def test_case_insensitive_operators(self):
    method test_parentheses (line 133) | def test_parentheses(self):
    method test_operator_as_part_of_term (line 149) | def test_operator_as_part_of_term(self):
    method test_extra_whitespace (line 160) | def test_extra_whitespace(self):
    method test_quoted_strings (line 171) | def test_quoted_strings(self):
    method test_quoted_strings_with_operators (line 188) | def test_quoted_strings_with_operators(self):
    method test_escaped_quotes (line 199) | def test_escaped_quotes(self):
    method test_unclosed_quotes (line 216) | def test_unclosed_quotes(self):
    method test_tags (line 225) | def test_tags(self):
    method test_tags_with_operators (line 242) | def test_tags_with_operators(self):
    method test_tags_mixed_with_terms (line 253) | def test_tags_mixed_with_terms(self):
    method test_empty_tag (line 267) | def test_empty_tag(self):
    method test_special_keywords (line 291) | def test_special_keywords(self):
    method test_special_keywords_with_operators (line 306) | def test_special_keywords_with_operators(self):
    method test_special_keywords_mixed_with_terms_and_tags (line 317) | def test_special_keywords_mixed_with_terms_and_tags(self):
    method test_empty_special_keyword (line 331) | def test_empty_special_keyword(self):
  class SearchQueryParserTest (line 345) | class SearchQueryParserTest(TestCase):
    method test_empty_query (line 348) | def test_empty_query(self):
    method test_whitespace_only_query (line 352) | def test_whitespace_only_query(self):
    method test_single_term (line 356) | def test_single_term(self):
    method test_and_expression (line 361) | def test_and_expression(self):
    method test_or_expression (line 366) | def test_or_expression(self):
    method test_not_expression (line 371) | def test_not_expression(self):
    method test_operator_precedence_and_over_or (line 376) | def test_operator_precedence_and_over_or(self):
    method test_operator_precedence_not_over_and (line 382) | def test_operator_precedence_not_over_and(self):
    method test_multiple_and_operators (line 388) | def test_multiple_and_operators(self):
    method test_multiple_or_operators (line 394) | def test_multiple_or_operators(self):
    method test_multiple_not_operators (line 400) | def test_multiple_not_operators(self):
    method test_parentheses_basic (line 405) | def test_parentheses_basic(self):
    method test_parentheses_change_precedence (line 410) | def test_parentheses_change_precedence(self):
    method test_nested_parentheses (line 416) | def test_nested_parentheses(self):
    method test_complex_expression (line 421) | def test_complex_expression(self):
    method test_hyphenated_terms (line 432) | def test_hyphenated_terms(self):
    method test_case_insensitive_operators (line 437) | def test_case_insensitive_operators(self):
    method test_case_insensitive_operators_with_explicit_operators (line 450) | def test_case_insensitive_operators_with_explicit_operators(self):
    method test_single_character_terms (line 459) | def test_single_character_terms(self):
    method test_numeric_terms (line 464) | def test_numeric_terms(self):
    method test_special_characters_in_terms (line 469) | def test_special_characters_in_terms(self):
    method test_url_terms (line 474) | def test_url_terms(self):
    method test_url_with_operators (line 479) | def test_url_with_operators(self):
    method test_quoted_strings (line 484) | def test_quoted_strings(self):
    method test_quoted_strings_with_operators (line 495) | def test_quoted_strings_with_operators(self):
    method test_multiple_quoted_strings (line 511) | def test_multiple_quoted_strings(self):
    method test_quoted_strings_with_parentheses (line 516) | def test_quoted_strings_with_parentheses(self):
    method test_escaped_quotes_in_terms (line 523) | def test_escaped_quotes_in_terms(self):
    method test_tags (line 528) | def test_tags(self):
    method test_tags_with_operators (line 539) | def test_tags_with_operators(self):
    method test_tags_mixed_with_terms (line 555) | def test_tags_mixed_with_terms(self):
    method test_tags_with_quoted_strings (line 560) | def test_tags_with_quoted_strings(self):
    method test_tags_with_parentheses (line 565) | def test_tags_with_parentheses(self):
    method test_empty_tags_ignored (line 570) | def test_empty_tags_ignored(self):
    method test_special_keywords (line 581) | def test_special_keywords(self):
    method test_special_keywords_with_operators (line 590) | def test_special_keywords_with_operators(self):
    method test_special_keywords_mixed_with_terms_and_tags (line 606) | def test_special_keywords_mixed_with_terms_and_tags(self):
    method test_special_keywords_with_quoted_strings (line 611) | def test_special_keywords_with_quoted_strings(self):
    method test_special_keywords_with_parentheses (line 616) | def test_special_keywords_with_parentheses(self):
    method test_special_keywords_within_quoted_string (line 623) | def test_special_keywords_within_quoted_string(self):
    method test_implicit_and_basic (line 628) | def test_implicit_and_basic(self):
    method test_implicit_and_with_tags (line 639) | def test_implicit_and_with_tags(self):
    method test_implicit_and_with_quoted_strings (line 655) | def test_implicit_and_with_quoted_strings(self):
    method test_implicit_and_with_explicit_operators (line 666) | def test_implicit_and_with_explicit_operators(self):
    method test_implicit_and_with_not (line 680) | def test_implicit_and_with_not(self):
    method test_implicit_and_with_parentheses (line 693) | def test_implicit_and_with_parentheses(self):
    method test_complex_precedence_with_implicit_and (line 709) | def test_complex_precedence_with_implicit_and(self):
    method test_operator_words_as_substrings (line 732) | def test_operator_words_as_substrings(self):
    method test_complex_queries (line 738) | def test_complex_queries(self):
  class SearchQueryParserErrorTest (line 813) | class SearchQueryParserErrorTest(TestCase):
    method test_unmatched_left_parenthesis (line 814) | def test_unmatched_left_parenthesis(self):
    method test_unmatched_right_parenthesis (line 819) | def test_unmatched_right_parenthesis(self):
    method test_empty_parentheses (line 824) | def test_empty_parentheses(self):
    method test_operator_without_operand (line 829) | def test_operator_without_operand(self):
    method test_trailing_operator (line 834) | def test_trailing_operator(self):
    method test_consecutive_operators (line 839) | def test_consecutive_operators(self):
    method test_not_without_operand (line 844) | def test_not_without_operand(self):
  class ExpressionToStringTest (line 850) | class ExpressionToStringTest(TestCase):
    method test_simple_term (line 851) | def test_simple_term(self):
    method test_simple_tag (line 855) | def test_simple_tag(self):
    method test_simple_keyword (line 859) | def test_simple_keyword(self):
    method test_term_with_spaces (line 863) | def test_term_with_spaces(self):
    method test_term_with_quotes (line 867) | def test_term_with_quotes(self):
    method test_and_expression_implicit (line 871) | def test_and_expression_implicit(self):
    method test_and_expression_with_tags (line 875) | def test_and_expression_with_tags(self):
    method test_and_expression_complex (line 879) | def test_and_expression_complex(self):
    method test_or_expression (line 883) | def test_or_expression(self):
    method test_or_expression_with_and (line 887) | def test_or_expression_with_and(self):
    method test_not_expression (line 891) | def test_not_expression(self):
    method test_not_with_tag (line 895) | def test_not_with_tag(self):
    method test_not_with_and (line 899) | def test_not_with_and(self):
    method test_complex_nested_expression (line 903) | def test_complex_nested_expression(self):
    method test_implicit_and_chain (line 911) | def test_implicit_and_chain(self):
    method test_none_expression (line 915) | def test_none_expression(self):
    method test_round_trip (line 918) | def test_round_trip(self):
  class StripTagFromQueryTest (line 939) | class StripTagFromQueryTest(TestCase):
    method test_single_tag (line 940) | def test_single_tag(self):
    method test_tag_with_and (line 944) | def test_tag_with_and(self):
    method test_tag_with_and_not (line 948) | def test_tag_with_and_not(self):
    method test_implicit_and_with_term_and_tags (line 952) | def test_implicit_and_with_term_and_tags(self):
    method test_tag_in_or_expression (line 956) | def test_tag_in_or_expression(self):
    method test_complex_or_with_and (line 960) | def test_complex_or_with_and(self):
    method test_case_insensitive (line 966) | def test_case_insensitive(self):
    method test_tag_not_present (line 970) | def test_tag_not_present(self):
    method test_multiple_same_tags (line 974) | def test_multiple_same_tags(self):
    method test_nested_parentheses (line 978) | def test_nested_parentheses(self):
    method test_not_expression_with_tag (line 982) | def test_not_expression_with_tag(self):
    method test_only_not_tag (line 986) | def test_only_not_tag(self):
    method test_complex_query (line 990) | def test_complex_query(self):
    method test_empty_query (line 996) | def test_empty_query(self):
    method test_whitespace_only (line 1000) | def test_whitespace_only(self):
    method test_special_keywords_preserved (line 1004) | def test_special_keywords_preserved(self):
    method test_quoted_terms_preserved (line 1008) | def test_quoted_terms_preserved(self):
    method test_all_tags_in_and_chain (line 1012) | def test_all_tags_in_and_chain(self):
    method test_tag_similar_name (line 1016) | def test_tag_similar_name(self):
    method test_invalid_query_returns_original (line 1021) | def test_invalid_query_returns_original(self):
    method test_implicit_and_in_output (line 1026) | def test_implicit_and_in_output(self):
    method test_nested_or_simplify_parenthesis (line 1030) | def test_nested_or_simplify_parenthesis(self):
    method test_nested_or_preserve_parenthesis (line 1036) | def test_nested_or_preserve_parenthesis(self):
    method test_left_side_removed (line 1042) | def test_left_side_removed(self):
    method test_right_side_removed (line 1046) | def test_right_side_removed(self):
  class StripTagFromQueryLaxSearchTest (line 1051) | class StripTagFromQueryLaxSearchTest(TestCase):
    method setUp (line 1052) | def setUp(self):
    method test_lax_search_removes_matching_term (line 1060) | def test_lax_search_removes_matching_term(self):
    method test_lax_search_removes_term_case_insensitive (line 1064) | def test_lax_search_removes_term_case_insensitive(self):
    method test_lax_search_multiple_terms (line 1071) | def test_lax_search_multiple_terms(self):
    method test_lax_search_preserves_non_matching_terms (line 1075) | def test_lax_search_preserves_non_matching_terms(self):
    method test_lax_search_removes_both_tag_and_term (line 1079) | def test_lax_search_removes_both_tag_and_term(self):
    method test_lax_search_mixed_tag_and_term (line 1083) | def test_lax_search_mixed_tag_and_term(self):
    method test_lax_search_term_in_or_expression (line 1089) | def test_lax_search_term_in_or_expression(self):
    method test_lax_search_term_in_not_expression (line 1095) | def test_lax_search_term_in_not_expression(self):
    method test_lax_search_only_not_term (line 1101) | def test_lax_search_only_not_term(self):
    method test_lax_search_complex_query (line 1105) | def test_lax_search_complex_query(self):
    method test_lax_search_quoted_term_with_same_name (line 1111) | def test_lax_search_quoted_term_with_same_name(self):
    method test_lax_search_partial_match_not_removed (line 1115) | def test_lax_search_partial_match_not_removed(self):
    method test_lax_search_multiple_occurrences (line 1119) | def test_lax_search_multiple_occurrences(self):
    method test_lax_search_nested_expressions (line 1125) | def test_lax_search_nested_expressions(self):
    method test_strict_search_preserves_terms (line 1131) | def test_strict_search_preserves_terms(self):
    method test_strict_search_preserves_terms_with_tags (line 1135) | def test_strict_search_preserves_terms_with_tags(self):
    method test_no_profile_defaults_to_strict (line 1139) | def test_no_profile_defaults_to_strict(self):
  class ExtractTagNamesFromQueryTest (line 1144) | class ExtractTagNamesFromQueryTest(TestCase):
    method test_empty_query (line 1145) | def test_empty_query(self):
    method test_whitespace_query (line 1149) | def test_whitespace_query(self):
    method test_single_tag (line 1153) | def test_single_tag(self):
    method test_multiple_tags (line 1157) | def test_multiple_tags(self):
    method test_tags_with_or (line 1161) | def test_tags_with_or(self):
    method test_tags_with_not (line 1165) | def test_tags_with_not(self):
    method test_tags_in_complex_query (line 1169) | def test_tags_in_complex_query(self):
    method test_duplicate_tags (line 1175) | def test_duplicate_tags(self):
    method test_case_insensitive_deduplication (line 1179) | def test_case_insensitive_deduplication(self):
    method test_mixed_tags_and_terms (line 1183) | def test_mixed_tags_and_terms(self):
    method test_only_terms_no_tags (line 1187) | def test_only_terms_no_tags(self):
    method test_special_keywords_not_extracted (line 1191) | def test_special_keywords_not_extracted(self):
    method test_tags_in_nested_parentheses (line 1195) | def test_tags_in_nested_parentheses(self):
    method test_invalid_query_returns_empty (line 1199) | def test_invalid_query_returns_empty(self):
    method test_tags_with_hyphens (line 1203) | def test_tags_with_hyphens(self):
  class ExtractTagNamesFromQueryLaxSearchTest (line 1208) | class ExtractTagNamesFromQueryLaxSearchTest(TestCase):
    method setUp (line 1209) | def setUp(self):
    method test_lax_search_extracts_terms (line 1217) | def test_lax_search_extracts_terms(self):
    method test_lax_search_mixed_tags_and_terms (line 1221) | def test_lax_search_mixed_tags_and_terms(self):
    method test_lax_search_deduplicates_tags_and_terms (line 1227) | def test_lax_search_deduplicates_tags_and_terms(self):
    method test_lax_search_case_insensitive_dedup (line 1231) | def test_lax_search_case_insensitive_dedup(self):
    method test_lax_search_terms_in_or_expression (line 1235) | def test_lax_search_terms_in_or_expression(self):
    method test_lax_search_terms_in_not_expression (line 1241) | def test_lax_search_terms_in_not_expression(self):
    method test_lax_search_quoted_terms (line 1247) | def test_lax_search_quoted_terms(self):
    method test_lax_search_complex_query (line 1253) | def test_lax_search_complex_query(self):
    method test_lax_search_special_keywords_not_extracted (line 1259) | def test_lax_search_special_keywords_not_extracted(self):
    method test_strict_search_ignores_terms (line 1265) | def test_strict_search_ignores_terms(self):
    method test_strict_search_only_tags (line 1269) | def test_strict_search_only_tags(self):
    method test_no_profile_defaults_to_strict (line 1275) | def test_no_profile_defaults_to_strict(self):

FILE: bookmarks/tests/test_settings_export_view.py
  class SettingsExportViewTestCase (line 11) | class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
    method setUp (line 12) | def setUp(self) -> None:
    method assertFormErrorHint (line 16) | def assertFormErrorHint(self, response, text: str):
    method test_should_export_successfully (line 20) | def test_should_export_successfully(self):
    method test_should_on
Condensed preview — 405 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,934K chars).
[
  {
    "path": ".coveragerc",
    "chars": 50,
    "preview": "[run]\nsource = bookmarks\nomit = bookmarks/tests/*\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 890,
    "preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.co"
  },
  {
    "path": ".dockerignore",
    "chars": 353,
    "preview": "# Ignore everything\n*\n\n# Include files required for build or at runtime\n!/bookmarks\n\n!/bootstrap.sh\n!/LICENSE.txt\n!/mana"
  },
  {
    "path": ".gitattributes",
    "chars": 28,
    "preview": "* text=auto\n*.sh text eol=lf"
  },
  {
    "path": ".github/workflows/build-test.yaml",
    "chars": 2737,
    "preview": "name: build-test\n\non: workflow_dispatch\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n   "
  },
  {
    "path": ".github/workflows/build.yaml",
    "chars": 3555,
    "preview": "name: build\n\non: workflow_dispatch\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        "
  },
  {
    "path": ".github/workflows/main.yaml",
    "chars": 1703,
    "preview": "name: linkding CI\n\non:\n  pull_request:\n  push:\n    branches:\n      - master\n\njobs:\n  unit_tests:\n    name: Unit Tests\n  "
  },
  {
    "path": ".gitignore",
    "chars": 2929,
    "preview": "# Created by .ignore support plugin (hsz.mobi)\n### JetBrains template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpSt"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 66329,
    "preview": "# Changelog\n\n## v1.45.0 (06/01/2026)\n\n### What's Changed\r\n* API token management by @sissbruecker in https://github.com/"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1082,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2019 Sascha Ißbrücker\n\nPermission is hereby granted, free of charge, to any person "
  },
  {
    "path": "Makefile",
    "chars": 699,
    "preview": ".PHONY: serve\n\ninit:\n\tuv sync\n\t[ -d data ] || mkdir data data/assets data/favicons data/previews\n\tuv run manage.py migra"
  },
  {
    "path": "README.md",
    "chars": 4558,
    "preview": "<div align=\"center\">\n    <br>\n    <a href=\"https://github.com/sissbruecker/linkding\">\n        <img src=\"assets/header.sv"
  },
  {
    "path": "SECURITY.md",
    "chars": 295,
    "preview": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 1.10.x   |"
  },
  {
    "path": "bookmarks/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "bookmarks/admin.py",
    "chars": 10425,
    "preview": "import os\n\nfrom django import forms\nfrom django.contrib import admin, messages\nfrom django.contrib.admin import AdminSit"
  },
  {
    "path": "bookmarks/api/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "bookmarks/api/auth.py",
    "chars": 1327,
    "preview": "from django.utils.translation import gettext_lazy as _\nfrom rest_framework import exceptions\nfrom rest_framework.authent"
  },
  {
    "path": "bookmarks/api/routes.py",
    "chars": 10750,
    "preview": "import gzip\nimport logging\nimport os\n\nfrom django.conf import settings\nfrom django.http import Http404, StreamingHttpRes"
  },
  {
    "path": "bookmarks/api/serializers.py",
    "chars": 7473,
    "preview": "from django.db.models import prefetch_related_objects\nfrom django.templatetags.static import static\nfrom rest_framework "
  },
  {
    "path": "bookmarks/apps.py",
    "chars": 236,
    "preview": "from django.apps import AppConfig\n\n\nclass BookmarksConfig(AppConfig):\n    name = \"bookmarks\"\n\n    def ready(self):\n     "
  },
  {
    "path": "bookmarks/context_processors.py",
    "chars": 460,
    "preview": "from bookmarks import utils\nfrom bookmarks.models import Toast\n\n\ndef toasts(request):\n    user = request.user\n    toast_"
  },
  {
    "path": "bookmarks/feeds.py",
    "chars": 3926,
    "preview": "import unicodedata\nfrom dataclasses import dataclass\n\nfrom django.contrib.syndication.views import Feed\nfrom django.db.m"
  },
  {
    "path": "bookmarks/forms.py",
    "chars": 13686,
    "preview": "from django import forms\nfrom django.contrib.auth.models import User\nfrom django.db import models\nfrom django.utils impo"
  },
  {
    "path": "bookmarks/frontend/api.js",
    "chars": 975,
    "preview": "export class Api {\n  constructor(baseUrl) {\n    this.baseUrl = baseUrl;\n  }\n\n  listBookmarks(search, options = { limit: "
  },
  {
    "path": "bookmarks/frontend/components/bookmark-page.js",
    "chars": 4931,
    "preview": "import { HeadlessElement } from \"../utils/element.js\";\n\nclass BookmarkPage extends HeadlessElement {\n  init() {\n    this"
  },
  {
    "path": "bookmarks/frontend/components/clear-button.js",
    "chars": 777,
    "preview": "import { HeadlessElement } from \"../utils/element\";\n\nclass ClearButton extends HeadlessElement {\n  init() {\n    this.fie"
  },
  {
    "path": "bookmarks/frontend/components/confirm-dropdown.js",
    "chars": 2755,
    "preview": "import { html, LitElement } from \"lit\";\nimport { FocusTrapController, isKeyboardActive } from \"../utils/focus.js\";\nimpor"
  },
  {
    "path": "bookmarks/frontend/components/details-modal.js",
    "chars": 487,
    "preview": "import { setAfterPageLoadFocusTarget } from \"../utils/focus.js\";\nimport { Modal } from \"./modal.js\";\n\nclass DetailsModal"
  },
  {
    "path": "bookmarks/frontend/components/dev-tool.js",
    "chars": 7169,
    "preview": "import { LitElement, html, css } from \"lit\";\n\nclass DevTool extends LitElement {\n  static properties = {\n    profile: { "
  },
  {
    "path": "bookmarks/frontend/components/dropdown.js",
    "chars": 1778,
    "preview": "import { HeadlessElement } from \"../utils/element.js\";\n\nclass Dropdown extends HeadlessElement {\n  constructor() {\n    s"
  },
  {
    "path": "bookmarks/frontend/components/filter-drawer.js",
    "chars": 3469,
    "preview": "import { html, render } from \"lit\";\nimport { Modal } from \"./modal.js\";\nimport { HeadlessElement } from \"../utils/elemen"
  },
  {
    "path": "bookmarks/frontend/components/form.js",
    "chars": 1959,
    "preview": "import { HeadlessElement } from \"../utils/element.js\";\n\nclass Form extends HeadlessElement {\n  constructor() {\n    super"
  },
  {
    "path": "bookmarks/frontend/components/modal.js",
    "chars": 1873,
    "preview": "import { FocusTrapController } from \"../utils/focus.js\";\nimport { HeadlessElement } from \"../utils/element.js\";\n\nexport "
  },
  {
    "path": "bookmarks/frontend/components/search-autocomplete.js",
    "chars": 8261,
    "preview": "import { html } from \"lit\";\nimport { api } from \"../api.js\";\nimport { TurboLitElement } from \"../utils/element.js\";\nimpo"
  },
  {
    "path": "bookmarks/frontend/components/tag-autocomplete.js",
    "chars": 5568,
    "preview": "import { html, nothing } from \"lit\";\nimport { TurboLitElement } from \"../utils/element.js\";\nimport { getCurrentWord, get"
  },
  {
    "path": "bookmarks/frontend/components/upload-button.js",
    "chars": 870,
    "preview": "import { HeadlessElement } from \"../utils/element.js\";\n\nclass UploadButton extends HeadlessElement {\n  init() {\n    this"
  },
  {
    "path": "bookmarks/frontend/index.js",
    "chars": 515,
    "preview": "import \"@hotwired/turbo\";\nimport \"./components/bookmark-page.js\";\nimport \"./components/clear-button.js\";\nimport \"./compo"
  },
  {
    "path": "bookmarks/frontend/shortcuts.js",
    "chars": 1674,
    "preview": "document.addEventListener(\"keydown\", (event) => {\n  // Skip if event occurred within an input element\n  const targetNode"
  },
  {
    "path": "bookmarks/frontend/utils/element.js",
    "chars": 2280,
    "preview": "import { LitElement } from \"lit\";\n\n/**\n * Base class for custom elements that wrap existing server-rendered DOM.\n *\n * H"
  },
  {
    "path": "bookmarks/frontend/utils/focus.js",
    "chars": 3610,
    "preview": "let keyboardActive = false;\n\nwindow.addEventListener(\n  \"keydown\",\n  () => {\n    keyboardActive = true;\n  },\n  { capture"
  },
  {
    "path": "bookmarks/frontend/utils/input.js",
    "chars": 840,
    "preview": "export function debounce(callback, delay = 250) {\n  let timeoutId;\n  return (...args) => {\n    clearTimeout(timeoutId);\n"
  },
  {
    "path": "bookmarks/frontend/utils/position-controller.js",
    "chars": 1729,
    "preview": "import {\n  arrow,\n  autoUpdate,\n  computePosition,\n  flip,\n  offset,\n  shift,\n} from \"@floating-ui/dom\";\n\nexport class P"
  },
  {
    "path": "bookmarks/frontend/utils/search-history.js",
    "chars": 1275,
    "preview": "const SEARCH_HISTORY_KEY = \"searchHistory\";\nconst MAX_ENTRIES = 30;\n\nexport class SearchHistory {\n  getHistory() {\n    c"
  },
  {
    "path": "bookmarks/frontend/utils/tag-cache.js",
    "chars": 752,
    "preview": "import { api } from \"../api.js\";\n\nclass TagCache {\n  constructor(api) {\n    this.api = api;\n\n    // Reset cached tags af"
  },
  {
    "path": "bookmarks/management/commands/backup.py",
    "chars": 1132,
    "preview": "import os\nimport sqlite3\n\nfrom django.core.management.base import BaseCommand\n\n\nclass Command(BaseCommand):\n    help = \""
  },
  {
    "path": "bookmarks/management/commands/create_initial_superuser.py",
    "chars": 1221,
    "preview": "import logging\nimport os\n\nfrom django.contrib.auth import get_user_model\nfrom django.core.management.base import BaseCom"
  },
  {
    "path": "bookmarks/management/commands/enable_wal.py",
    "chars": 775,
    "preview": "import logging\n\nfrom django.conf import settings\nfrom django.core.management.base import BaseCommand\nfrom django.db impo"
  },
  {
    "path": "bookmarks/management/commands/ensure_superuser.py",
    "chars": 772,
    "preview": "from django.contrib.auth import get_user_model\nfrom django.core.management.base import BaseCommand\n\n\nclass Command(BaseC"
  },
  {
    "path": "bookmarks/management/commands/full_backup.py",
    "chars": 3263,
    "preview": "import os\nimport sqlite3\nimport tempfile\nimport zipfile\n\nfrom django.core.management.base import BaseCommand\n\n\nclass Com"
  },
  {
    "path": "bookmarks/management/commands/generate_secret_key.py",
    "chars": 668,
    "preview": "import logging\nimport os\n\nfrom django.core.management.base import BaseCommand\nfrom django.core.management.utils import g"
  },
  {
    "path": "bookmarks/management/commands/import_netscape.py",
    "chars": 733,
    "preview": "from django.contrib.auth.models import User\nfrom django.core.management.base import BaseCommand\n\nfrom bookmarks.services"
  },
  {
    "path": "bookmarks/management/commands/migrate_tasks.py",
    "chars": 2475,
    "preview": "import importlib\nimport json\nimport os\nimport sqlite3\n\nfrom django.core.management.base import BaseCommand\n\n\nclass Comma"
  },
  {
    "path": "bookmarks/middlewares.py",
    "chars": 1280,
    "preview": "from django.conf import settings\nfrom django.contrib.auth.middleware import RemoteUserMiddleware\n\nfrom bookmarks.models "
  },
  {
    "path": "bookmarks/migrations/0001_initial.py",
    "chars": 1637,
    "preview": "# Generated by Django 2.2.2 on 2019-06-28 23:49\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "bookmarks/migrations/0002_auto_20190629_2303.py",
    "chars": 1279,
    "preview": "# Generated by Django 2.2.2 on 2019-06-29 23:03\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "bookmarks/migrations/0003_auto_20200913_0656.py",
    "chars": 388,
    "preview": "# Generated by Django 2.2.13 on 2020-09-13 06:56\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "bookmarks/migrations/0004_auto_20200926_1028.py",
    "chars": 559,
    "preview": "# Generated by Django 2.2.13 on 2020-09-26 10:28\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "bookmarks/migrations/0005_auto_20210103_1212.py",
    "chars": 523,
    "preview": "# Generated by Django 2.2.13 on 2021-01-03 12:12\n\nfrom django.db import migrations, models\n\nimport bookmarks.validators\n"
  },
  {
    "path": "bookmarks/migrations/0006_bookmark_is_archived.py",
    "chars": 396,
    "preview": "# Generated by Django 2.2.13 on 2021-02-14 09:08\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "bookmarks/migrations/0007_userprofile.py",
    "chars": 1931,
    "preview": "# Generated by Django 2.2.18 on 2021-03-26 22:39\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom"
  },
  {
    "path": "bookmarks/migrations/0008_userprofile_bookmark_date_display.py",
    "chars": 645,
    "preview": "# Generated by Django 2.2.18 on 2021-03-30 10:40\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "bookmarks/migrations/0009_bookmark_web_archive_snapshot_url.py",
    "chars": 435,
    "preview": "# Generated by Django 2.2.20 on 2021-05-16 14:35\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "bookmarks/migrations/0010_userprofile_bookmark_link_target.py",
    "chars": 558,
    "preview": "# Generated by Django 3.2.6 on 2021-10-03 06:35\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0011_userprofile_web_archive_integration.py",
    "chars": 564,
    "preview": "# Generated by Django 3.2.6 on 2022-01-08 12:39\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0012_toast.py",
    "chars": 1209,
    "preview": "# Generated by Django 3.2.6 on 2022-01-08 19:24\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "bookmarks/migrations/0013_web_archive_optin_toast.py",
    "chars": 803,
    "preview": "# Generated by Django 3.2.6 on 2022-01-08 19:27\n\nfrom django.contrib.auth import get_user_model\nfrom django.db import mi"
  },
  {
    "path": "bookmarks/migrations/0014_alter_bookmark_unread.py",
    "chars": 626,
    "preview": "# Generated by Django 3.2.13 on 2022-07-23 12:30\n\nfrom django.db import migrations, models\n\n\ndef forwards(apps, schema_e"
  },
  {
    "path": "bookmarks/migrations/0015_feedtoken.py",
    "chars": 1005,
    "preview": "# Generated by Django 3.2.13 on 2022-07-23 20:35\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom"
  },
  {
    "path": "bookmarks/migrations/0016_bookmark_shared.py",
    "chars": 382,
    "preview": "# Generated by Django 3.2.14 on 2022-08-02 18:42\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "bookmarks/migrations/0017_userprofile_enable_sharing.py",
    "chars": 399,
    "preview": "# Generated by Django 3.2.14 on 2022-08-04 09:08\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations."
  },
  {
    "path": "bookmarks/migrations/0018_bookmark_favicon_file.py",
    "chars": 412,
    "preview": "# Generated by Django 4.1 on 2023-01-07 23:42\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "bookmarks/migrations/0019_userprofile_enable_favicons.py",
    "chars": 403,
    "preview": "# Generated by Django 4.1 on 2023-01-09 21:16\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "bookmarks/migrations/0020_userprofile_tag_search.py",
    "chars": 532,
    "preview": "# Generated by Django 4.1.7 on 2023-04-10 01:55\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0021_userprofile_display_url.py",
    "chars": 402,
    "preview": "# Generated by Django 4.1.7 on 2023-05-18 07:58\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0022_bookmark_notes.py",
    "chars": 388,
    "preview": "# Generated by Django 4.1.7 on 2023-05-19 10:52\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0023_userprofile_permanent_notes.py",
    "chars": 398,
    "preview": "# Generated by Django 4.1.9 on 2023-05-20 08:00\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0024_userprofile_enable_public_sharing.py",
    "chars": 417,
    "preview": "# Generated by Django 4.1.9 on 2023-08-14 07:08\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0025_userprofile_search_preferences.py",
    "chars": 416,
    "preview": "# Generated by Django 4.1.9 on 2023-09-30 10:44\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0026_userprofile_custom_css.py",
    "chars": 403,
    "preview": "# Generated by Django 5.0.2 on 2024-03-16 23:05\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0027_userprofile_bookmark_description_display_and_more.py",
    "chars": 734,
    "preview": "# Generated by Django 5.0.2 on 2024-03-23 21:48\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0028_userprofile_display_archive_bookmark_action_and_more.py",
    "chars": 990,
    "preview": "# Generated by Django 5.0.2 on 2024-03-29 20:05\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0029_bookmark_list_actions_toast.py",
    "chars": 905,
    "preview": "# Generated by Django 5.0.2 on 2024-03-29 21:25\n\nfrom django.contrib.auth import get_user_model\nfrom django.db import mi"
  },
  {
    "path": "bookmarks/migrations/0030_bookmarkasset.py",
    "chars": 1479,
    "preview": "# Generated by Django 5.0.2 on 2024-03-31 08:21\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "bookmarks/migrations/0031_userprofile_enable_automatic_html_snapshots.py",
    "chars": 412,
    "preview": "# Generated by Django 5.0.2 on 2024-04-01 10:29\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0032_html_snapshots_hint_toast.py",
    "chars": 918,
    "preview": "# Generated by Django 5.0.2 on 2024-04-01 12:17\n\nfrom django.contrib.auth import get_user_model\nfrom django.db import mi"
  },
  {
    "path": "bookmarks/migrations/0033_userprofile_default_mark_unread.py",
    "chars": 413,
    "preview": "# Generated by Django 5.0.3 on 2024-04-17 19:27\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0034_bookmark_preview_image_file_and_more.py",
    "chars": 599,
    "preview": "# Generated by Django 5.0.3 on 2024-05-10 07:01\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0035_userprofile_tag_grouping.py",
    "chars": 571,
    "preview": "# Generated by Django 5.0.3 on 2024-05-14 08:28\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0036_userprofile_auto_tagging_rules.py",
    "chars": 405,
    "preview": "# Generated by Django 5.0.3 on 2024-05-17 07:09\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0037_globalsettings.py",
    "chars": 1046,
    "preview": "# Generated by Django 5.0.8 on 2024-08-31 12:39\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0038_globalsettings_guest_profile_user.py",
    "chars": 697,
    "preview": "# Generated by Django 5.0.8 on 2024-08-31 17:54\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "bookmarks/migrations/0039_globalsettings_enable_link_prefetch.py",
    "chars": 425,
    "preview": "# Generated by Django 5.0.8 on 2024-09-14 07:48\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0040_userprofile_items_per_page_and_more.py",
    "chars": 704,
    "preview": "# Generated by Django 5.0.8 on 2024-09-18 20:11\n\nimport django.core.validators\nfrom django.db import migrations, models\n"
  },
  {
    "path": "bookmarks/migrations/0041_merge_metadata.py",
    "chars": 895,
    "preview": "# Generated by Django 5.1.1 on 2024-09-21 08:13\n\nfrom django.db import migrations\nfrom django.db.models import Q\nfrom dj"
  },
  {
    "path": "bookmarks/migrations/0042_userprofile_custom_css_hash.py",
    "chars": 407,
    "preview": "# Generated by Django 5.1.1 on 2024-09-28 08:03\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0043_userprofile_collapse_side_panel.py",
    "chars": 415,
    "preview": "# Generated by Django 5.1.5 on 2025-02-02 09:35\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0044_bookmark_latest_snapshot.py",
    "chars": 1298,
    "preview": "# Generated by Django 5.1.7 on 2025-03-22 12:28\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "bookmarks/migrations/0045_userprofile_hide_bundles_bookmarkbundle.py",
    "chars": 1771,
    "preview": "# Generated by Django 5.1.9 on 2025-06-19 08:48\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "bookmarks/migrations/0046_add_url_normalized_field.py",
    "chars": 445,
    "preview": "# Generated by Django 5.2.3 on 2025-08-22 08:26\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0047_populate_url_normalized_field.py",
    "chars": 1113,
    "preview": "# Generated by Django 5.2.3 on 2025-08-22 08:28\n\nfrom django.db import migrations, transaction\n\nfrom bookmarks.utils imp"
  },
  {
    "path": "bookmarks/migrations/0048_userprofile_default_mark_shared.py",
    "chars": 417,
    "preview": "# Generated by Django 5.2.3 on 2025-08-22 17:38\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0049_userprofile_legacy_search.py",
    "chars": 413,
    "preview": "# Generated by Django 5.2.5 on 2025-10-05 09:10\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "bookmarks/migrations/0050_new_search_toast.py",
    "chars": 934,
    "preview": "# Generated by Django 5.2.5 on 2025-10-05 10:01\n\nfrom django.contrib.auth import get_user_model\nfrom django.db import mi"
  },
  {
    "path": "bookmarks/migrations/0051_fix_normalized_url.py",
    "chars": 891,
    "preview": "# Generated by Django 5.2.5 on 2025-10-11 08:46\n\nfrom django.db import migrations\n\nfrom bookmarks.utils import normalize"
  },
  {
    "path": "bookmarks/migrations/0052_apitoken.py",
    "chars": 1269,
    "preview": "# Generated by Django 5.2.5 on 2025-12-14 16:33\n\nimport django.db.models.deletion\nfrom django.conf import settings\nfrom "
  },
  {
    "path": "bookmarks/migrations/0053_migrate_api_tokens.py",
    "chars": 892,
    "preview": "# Generated by Django 5.2.5 on 2025-12-14 16:34\nfrom django.db import migrations\n\n\ndef migrate_tokens_forward(apps, sche"
  },
  {
    "path": "bookmarks/migrations/0054_bookmarkbundle_filter_shared_and_more.py",
    "chars": 844,
    "preview": "# Generated by Django 6.0 on 2026-02-28 09:05\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Mig"
  },
  {
    "path": "bookmarks/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "bookmarks/models.py",
    "chars": 19059,
    "preview": "import binascii\nimport hashlib\nimport logging\nimport os\n\nfrom django.conf import settings\nfrom django.contrib.auth.model"
  },
  {
    "path": "bookmarks/queries.py",
    "chars": 13479,
    "preview": "import contextlib\n\nfrom django.conf import settings\nfrom django.contrib.auth.models import User\nfrom django.core.excepti"
  },
  {
    "path": "bookmarks/services/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "bookmarks/services/assets.py",
    "chars": 8754,
    "preview": "import gzip\nimport logging\nimport os\nimport shutil\n\nimport requests\nfrom django.conf import settings\nfrom django.core.fi"
  },
  {
    "path": "bookmarks/services/auto_tagging.py",
    "chars": 2439,
    "preview": "import re\nfrom urllib.parse import parse_qs, urlparse\n\nimport idna\n\n\ndef get_tags(script: str, url: str):\n    parsed_url"
  },
  {
    "path": "bookmarks/services/bookmarks.py",
    "chars": 8577,
    "preview": "import logging\n\nfrom django.utils import timezone\n\nfrom bookmarks.models import Bookmark, User, parse_tag_string\nfrom bo"
  },
  {
    "path": "bookmarks/services/bundles.py",
    "chars": 1253,
    "preview": "from django.db.models import Max\n\nfrom bookmarks.models import BookmarkBundle, User\n\n\ndef create_bundle(bundle: Bookmark"
  },
  {
    "path": "bookmarks/services/exporter.py",
    "chars": 1594,
    "preview": "import html\n\nfrom bookmarks.models import Bookmark\n\nBookmarkDocument = list[str]\n\n\ndef export_netscape_html(bookmarks: l"
  },
  {
    "path": "bookmarks/services/favicon_loader.py",
    "chars": 2747,
    "preview": "import logging\nimport mimetypes\nimport os.path\nimport re\nimport time\nfrom pathlib import Path\nfrom urllib.parse import u"
  },
  {
    "path": "bookmarks/services/importer.py",
    "chars": 8632,
    "preview": "import logging\nfrom dataclasses import dataclass\n\nfrom django.contrib.auth.models import User\nfrom django.utils import t"
  },
  {
    "path": "bookmarks/services/monolith.py",
    "chars": 1004,
    "preview": "import gzip\nimport os\nimport shutil\nimport subprocess\n\nfrom django.conf import settings\n\n\nclass MonolithError(Exception)"
  },
  {
    "path": "bookmarks/services/parser.py",
    "chars": 3200,
    "preview": "import contextlib\nfrom dataclasses import dataclass\nfrom html.parser import HTMLParser\n\nfrom bookmarks.models import par"
  },
  {
    "path": "bookmarks/services/preview_image_loader.py",
    "chars": 3069,
    "preview": "import hashlib\nimport logging\nimport mimetypes\nimport os.path\nfrom pathlib import Path\n\nimport requests\nfrom django.conf"
  },
  {
    "path": "bookmarks/services/search_query_parser.py",
    "chars": 18841,
    "preview": "from dataclasses import dataclass\nfrom enum import Enum\n\nfrom bookmarks.models import UserProfile\n\n\nclass TokenType(Enum"
  },
  {
    "path": "bookmarks/services/singlefile.py",
    "chars": 1830,
    "preview": "import logging\nimport os\nimport shlex\nimport signal\nimport subprocess\n\nfrom django.conf import settings\n\n\nclass SingleFi"
  },
  {
    "path": "bookmarks/services/tags.py",
    "chars": 1305,
    "preview": "import logging\nimport operator\n\nfrom django.contrib.auth.models import User\nfrom django.utils import timezone\n\nfrom book"
  },
  {
    "path": "bookmarks/services/tasks.py",
    "chars": 10574,
    "preview": "import functools\nimport logging\n\nimport waybackpy\nfrom django.conf import settings\nfrom django.contrib.auth.models impor"
  },
  {
    "path": "bookmarks/services/wayback.py",
    "chars": 625,
    "preview": "import datetime\n\nfrom django.utils import timezone\n\n\ndef generate_fallback_webarchive_url(\n    url: str, timestamp: date"
  },
  {
    "path": "bookmarks/services/website_loader.py",
    "chars": 5892,
    "preview": "import logging\nfrom dataclasses import dataclass\nfrom functools import lru_cache\nfrom urllib.parse import urljoin\n\nimpor"
  },
  {
    "path": "bookmarks/settings/__init__.py",
    "chars": 148,
    "preview": "# Use dev settings as default, use production if dev settings do not exist\n# ruff: noqa\ntry:\n    from .dev import *\nexce"
  },
  {
    "path": "bookmarks/settings/base.py",
    "chars": 10793,
    "preview": "\"\"\"\nDjango settings for linkding webapp.\n\nGenerated by 'django-admin startproject' using Django 2.2.2.\n\nFor more informa"
  },
  {
    "path": "bookmarks/settings/custom.py",
    "chars": 75,
    "preview": "# Placeholder, can be mounted in a Docker container with a custom settings\n"
  },
  {
    "path": "bookmarks/settings/dev.py",
    "chars": 1688,
    "preview": "\"\"\"\nDevelopment settings for linkding webapp\n\"\"\"\n\n# ruff: noqa\n\n# Start from development settings\n# noinspection PyUnres"
  },
  {
    "path": "bookmarks/settings/prod.py",
    "chars": 1454,
    "preview": "\"\"\"\nProduction settings for linkding webapp\n\"\"\"\n\n# ruff: noqa\n\n# Start from development settings\n# noinspection PyUnreso"
  },
  {
    "path": "bookmarks/signals.py",
    "chars": 966,
    "preview": "from django.conf import settings\nfrom django.db.backends.signals import connection_created\nfrom django.dispatch import r"
  },
  {
    "path": "bookmarks/static/live-reload.js",
    "chars": 1197,
    "preview": "const RELOAD_URL = \"/live_reload\";\n\nlet eventSource = null;\nlet serverId = null;\n\nfunction connect() {\n  console.debug(\""
  },
  {
    "path": "bookmarks/static/robots.txt",
    "chars": 26,
    "preview": "User-agent: *\nDisallow: /\n"
  },
  {
    "path": "bookmarks/static/vendor/Readability.js",
    "chars": 84022,
    "preview": "/*\n * Copyright (c) 2010 Arc90 Inc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not "
  },
  {
    "path": "bookmarks/styles/auth.css",
    "chars": 53,
    "preview": ".auth-page {\n  margin: 0 auto;\n  max-width: 350px;\n}\n"
  },
  {
    "path": "bookmarks/styles/bookmark-details.css",
    "chars": 1563,
    "preview": ".bookmark-details {\n  .modal-container {\n    width: 100%;\n  }\n\n  .title {\n    word-break: break-word;\n    display: -webk"
  },
  {
    "path": "bookmarks/styles/bookmark-form.css",
    "chars": 828,
    "preview": ".bookmarks-form-page {\n  main {\n    max-width: 550px;\n    margin: 0 auto;\n  }\n}\n\n.bookmarks-form {\n  & .has-icon-right >"
  },
  {
    "path": "bookmarks/styles/bookmark-page.css",
    "chars": 10416,
    "preview": ":root {\n  --bookmark-title-color: var(--primary-text-color);\n  --bookmark-title-weight: 500;\n  --bookmark-description-co"
  },
  {
    "path": "bookmarks/styles/bundles.css",
    "chars": 439,
    "preview": ".bundles-page {\n  .crud-table {\n    svg {\n      cursor: grab;\n    }\n\n    tr.drag-start {\n      --secondary-border-color:"
  },
  {
    "path": "bookmarks/styles/components.css",
    "chars": 2010,
    "preview": "/* Shared components */\n\n/* Section header component */\n.section-header {\n  border-bottom: solid 1px var(--secondary-bor"
  },
  {
    "path": "bookmarks/styles/crud.css",
    "chars": 1059,
    "preview": ".crud-page {\n  .crud-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin"
  },
  {
    "path": "bookmarks/styles/layout.css",
    "chars": 471,
    "preview": "/* Main layout */\nbody {\n  margin: 20px 10px;\n\n  @media (min-width: 600px) {\n    /* Horizontal offset accounts for check"
  },
  {
    "path": "bookmarks/styles/markdown.css",
    "chars": 644,
    "preview": ".markdown {\n  & p,\n  & ul,\n  & ol,\n  & pre,\n  & blockquote {\n    margin: 0 0 var(--unit-2) 0;\n  }\n\n  & > *:first-child {"
  },
  {
    "path": "bookmarks/styles/reader-mode.css",
    "chars": 303,
    "preview": "html.reader-mode {\n  --font-size: 1rem;\n  line-height: 1.6;\n\n  body {\n    margin: 3rem 2rem;\n  }\n\n  .container {\n    max"
  },
  {
    "path": "bookmarks/styles/responsive.css",
    "chars": 1416,
    "preview": ".show-sm,\n.show-md {\n  display: none !important;\n}\n\n.width-25 {\n  width: 25%;\n}\n\n.width-50 {\n  width: 50%;\n}\n\n.width-75 "
  },
  {
    "path": "bookmarks/styles/settings.css",
    "chars": 497,
    "preview": ".settings-page {\n  h1 {\n    font-size: var(--font-size-xl);\n    margin-bottom: var(--unit-6);\n  }\n\n  section {\n    margi"
  },
  {
    "path": "bookmarks/styles/tags.css",
    "chars": 145,
    "preview": ".tags-editor-page {\n  main {\n    max-width: 550px;\n    margin: 0 auto;\n  }\n}\n\n.tag-edit-modal {\n  .modal-container {\n   "
  },
  {
    "path": "bookmarks/styles/theme/LICENSE",
    "chars": 1081,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 - 2020 Yan Zhu\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "bookmarks/styles/theme/_normalize.css",
    "chars": 7824,
    "preview": "/* Manually forked from Normalize.css */\n/* normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */\n\n/*"
  },
  {
    "path": "bookmarks/styles/theme/animations.css",
    "chars": 451,
    "preview": "/* Animations */\n@keyframes loading {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  "
  },
  {
    "path": "bookmarks/styles/theme/asian.css",
    "chars": 640,
    "preview": "/* Optimized for East Asian CJK */\nhtml:lang(zh),\nhtml:lang(zh-Hans),\n.lang-zh,\n.lang-zh-hans {\n  font-family: var(--cjk"
  },
  {
    "path": "bookmarks/styles/theme/autocomplete.css",
    "chars": 1835,
    "preview": "/* Autocomplete */\n.form-autocomplete {\n  & .form-autocomplete-input {\n    box-sizing: border-box;\n    align-content: fl"
  },
  {
    "path": "bookmarks/styles/theme/badges.css",
    "chars": 1182,
    "preview": "/* Badges */\n.badge {\n  position: relative;\n  white-space: nowrap;\n\n  &[data-badge],\n  &:not([data-badge]) {\n    &::afte"
  },
  {
    "path": "bookmarks/styles/theme/base.css",
    "chars": 1011,
    "preview": "/* Base */\n*,\n*::before,\n*::after {\n  box-sizing: inherit;\n}\n\nhtml {\n  box-sizing: border-box;\n  font-size: var(--html-f"
  },
  {
    "path": "bookmarks/styles/theme/buttons.css",
    "chars": 5650,
    "preview": "/* Buttons */\n:root {\n  --btn-bg-color: var(--body-color);\n  --btn-hover-bg-color: var(--gray-50);\n  --btn-border-color:"
  },
  {
    "path": "bookmarks/styles/theme/code.css",
    "chars": 562,
    "preview": "/* Code */\n:root {\n  --code-bg-color: var(--body-color-contrast);\n  --code-color: var(--text-color);\n}\n\ncode {\n  border-"
  },
  {
    "path": "bookmarks/styles/theme/dropdowns.css",
    "chars": 852,
    "preview": "/* Dropdown */\n.dropdown {\n  --dropdown-focus-display: block;\n\n  display: inline-block;\n  position: relative;\n\n  .menu {"
  },
  {
    "path": "bookmarks/styles/theme/empty.css",
    "chars": 441,
    "preview": "/* Empty states (or Blank slates) */\n.empty {\n  background: var(--body-color-contrast);\n  border-radius: var(--border-ra"
  },
  {
    "path": "bookmarks/styles/theme/forms.css",
    "chars": 12466,
    "preview": "/* Forms */\n:root {\n  --input-bg-color: var(--body-color);\n  --input-disabled-bg-color: var(--gray-100);\n  --input-text-"
  },
  {
    "path": "bookmarks/styles/theme/menus.css",
    "chars": 2491,
    "preview": ":root {\n  --menu-bg-color: var(--body-color);\n  --menu-border-color: var(--gray-200);\n  --menu-border-radius: var(--bord"
  },
  {
    "path": "bookmarks/styles/theme/modals.css",
    "chars": 2624,
    "preview": "/* Modals */\n:root {\n  --modal-overlay-bg-color: rgba(243, 244, 246, 0.6);\n  --modal-container-bg-color: var(--body-colo"
  },
  {
    "path": "bookmarks/styles/theme/pagination.css",
    "chars": 1118,
    "preview": "/* Pagination */\n.pagination {\n  display: flex;\n  list-style: none;\n  margin: var(--unit-1) 0;\n  padding: var(--unit-1) "
  },
  {
    "path": "bookmarks/styles/theme/tables.css",
    "chars": 388,
    "preview": "/* Tables */\n.table {\n  border-collapse: collapse;\n  border-spacing: 0;\n  width: 100%;\n  text-align: left;\n\n  td,\n  th {"
  },
  {
    "path": "bookmarks/styles/theme/tabs.css",
    "chars": 1525,
    "preview": "/* Tabs */\n:root {\n  --tab-color: var(--text-color);\n  --tab-hover-color: var(--primary-text-color);\n  --tab-active-colo"
  },
  {
    "path": "bookmarks/styles/theme/toasts.css",
    "chars": 553,
    "preview": "/* Toasts */\n.toast {\n  background: var(--gray-600);\n  border-radius: var(--border-radius);\n  color: var(--contrast-text"
  },
  {
    "path": "bookmarks/styles/theme/typography.css",
    "chars": 1330,
    "preview": "/* Typography */\n/* Headings */\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  color: inherit;\n  font-weight: 500;\n  line-height: 1.2;\n  mar"
  },
  {
    "path": "bookmarks/styles/theme/utilities.css",
    "chars": 4782,
    "preview": "/* Colors */\n.text-primary {\n  color: var(--primary-text-color);\n}\n\n.text-secondary {\n  color: var(--secondary-text-colo"
  },
  {
    "path": "bookmarks/styles/theme/variables.css",
    "chars": 4548,
    "preview": ":root {\n  /* Color palette */\n  --gray-50: rgb(249, 250, 251);\n  --gray-100: rgb(243, 244, 246);\n  --gray-200: rgb(229, "
  },
  {
    "path": "bookmarks/styles/theme-dark.css",
    "chars": 4829,
    "preview": "@import \"theme-light.css\";\n\n:root {\n  /* Color palette */\n  --contrast-5: hsla(241, 65%, 85%, 0.06);\n  --contrast-10: hs"
  },
  {
    "path": "bookmarks/styles/theme-light.css",
    "chars": 905,
    "preview": "@import \"theme/variables.css\";\n@import \"theme/_normalize.css\";\n@import \"theme/base.css\";\n@import \"theme/typography.css\";"
  },
  {
    "path": "bookmarks/tasks.py",
    "chars": 124,
    "preview": "# Expose task modules to Huey Django extension\n# noinspection PyUnusedImports\nimport bookmarks.services.tasks  # noqa: F"
  },
  {
    "path": "bookmarks/templates/admin/background_tasks.html",
    "chars": 960,
    "preview": "{% extends \"admin/base_site.html\" %}\n{% block content %}\n  <table style=\"width: 100%\">\n    <thead>\n      <tr>\n        <t"
  },
  {
    "path": "bookmarks/templates/bookmarks/bookmark_list.html",
    "chars": 8521,
    "preview": "{% load static shared pagination %}\n{% if bookmark_list.is_empty %}\n  {% include 'bookmarks/empty_bookmarks.html' %}\n{% "
  },
  {
    "path": "bookmarks/templates/bookmarks/bookmark_page.html",
    "chars": 2026,
    "preview": "{% extends \"shared/layout.html\" %}\n{% load static shared bookmarks %}\n{% block content %}\n  <ld-bookmark-page {% if not "
  },
  {
    "path": "bookmarks/templates/bookmarks/bulk_edit_bar.html",
    "chars": 1734,
    "preview": "{% load shared %}\n{% htmlmin %}\n<div class=\"bulk-edit-bar\">\n  <label class=\"form-checkbox bulk-edit-checkbox all\">\n    <"
  },
  {
    "path": "bookmarks/templates/bookmarks/bundle_section.html",
    "chars": 1341,
    "preview": "{% load static %}\n{% if not request.user_profile.hide_bundles %}\n  <section aria-labelledby=\"bundles-heading\">\n    <div "
  },
  {
    "path": "bookmarks/templates/bookmarks/close.html",
    "chars": 182,
    "preview": "{% extends \"shared/layout.html\" %}\n{% block content %}\n  <script type=\"application/javascript\">\n    window.close()\n  </s"
  },
  {
    "path": "bookmarks/templates/bookmarks/details/asset_icon.html",
    "chars": 2381,
    "preview": "{% if asset.content_type == 'text/html' %}\n  <svg xmlns=\"http://www.w3.org/2000/svg\"\n       width=\"24\"\n       height=\"24"
  },
  {
    "path": "bookmarks/templates/bookmarks/details/assets.html",
    "chars": 2204,
    "preview": "<div>\n  {% if details.assets %}\n    <div class=\"item-list assets\">\n      {% for asset in details.assets %}\n        <div "
  },
  {
    "path": "bookmarks/templates/bookmarks/details/form.html",
    "chars": 5806,
    "preview": "{% load static %}\n{% load shared %}\n<ld-form>\n  <form action=\"{{ details.action_url }}\"\n        method=\"post\"\n        en"
  },
  {
    "path": "bookmarks/templates/bookmarks/details/modal.html",
    "chars": 1606,
    "preview": "<turbo-frame id=\"details-modal\" target=\"_top\">\n{% if details %}\n  <ld-details-modal class=\"modal active bookmark-details"
  },
  {
    "path": "bookmarks/templates/bookmarks/edit.html",
    "chars": 663,
    "preview": "{% extends 'shared/layout.html' %}\n{% block head %}\n  {% with page_title=\"Edit bookmark - Linkding\" %}{{ block.super }}{"
  },
  {
    "path": "bookmarks/templates/bookmarks/empty_bookmarks.html",
    "chars": 894,
    "preview": "<div class=\"empty mt-4\">\n  {% if not bookmark_list.query_is_valid %}\n    <p class=\"empty-title h5\">Invalid search query<"
  },
  {
    "path": "bookmarks/templates/bookmarks/form.html",
    "chars": 9173,
    "preview": "{% load static %}\n{% load shared %}\n<div class=\"bookmarks-form\">\n  {% csrf_token %}\n  {{ form.auto_close }}\n  <div class"
  },
  {
    "path": "bookmarks/templates/bookmarks/new.html",
    "chars": 582,
    "preview": "{% extends 'shared/layout.html' %}\n{% block head %}\n  {% with page_title=\"New bookmark - Linkding\" %}{{ block.super }}{%"
  },
  {
    "path": "bookmarks/templates/bookmarks/read.html",
    "chars": 3966,
    "preview": "{% load static %}\n<!DOCTYPE html>\n<html lang=\"en\" class=\"reader-mode\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>Re"
  },
  {
    "path": "bookmarks/templates/bookmarks/search.html",
    "chars": 3570,
    "preview": "{% load static shared %}\n<div class=\"search-container\">\n  <form id=\"search\" action=\"\" method=\"get\" role=\"search\">\n    <l"
  },
  {
    "path": "bookmarks/templates/bookmarks/tag_cloud.html",
    "chars": 1132,
    "preview": "{% load shared %}\n{% htmlmin %}\n<div class=\"tag-cloud\">\n  {% if tag_cloud.has_selected_tags %}\n    <p class=\"selected-ta"
  },
  {
    "path": "bookmarks/templates/bookmarks/tag_section.html",
    "chars": 804,
    "preview": "{% load static %}\n<section aria-labelledby=\"tags-heading\">\n  <div class=\"section-header no-wrap\">\n    <h2 id=\"tags-headi"
  },
  {
    "path": "bookmarks/templates/bookmarks/user_section.html",
    "chars": 670,
    "preview": "{% load shared %}\n<section aria-labelledby=\"user-heading\">\n  <div class=\"section-header\">\n    <h2 id=\"user-heading\">User"
  },
  {
    "path": "bookmarks/templates/bundles/edit.html",
    "chars": 871,
    "preview": "{% extends 'shared/layout.html' %}\n{% block head %}\n  {% with page_title=\"Edit bundle - Linkding\" %}{{ block.super }}{% "
  },
  {
    "path": "bookmarks/templates/bundles/form.html",
    "chars": 3080,
    "preview": "{% load shared %}\n<div class=\"form-group\">\n  {% formlabel form.name \"Name\" %}\n  {% formfield form.name %}\n  {{ form.name"
  },
  {
    "path": "bookmarks/templates/bundles/index.html",
    "chars": 4244,
    "preview": "{% extends \"shared/layout.html\" %}\n{% load static %}\n{% block head %}\n  {% with page_title=\"Bundles - Linkding\" %}{{ blo"
  },
  {
    "path": "bookmarks/templates/bundles/new.html",
    "chars": 858,
    "preview": "{% extends 'shared/layout.html' %}\n{% block head %}\n  {% with page_title=\"New bundle - Linkding\" %}{{ block.super }}{% e"
  },
  {
    "path": "bookmarks/templates/bundles/preview.html",
    "chars": 354,
    "preview": "<turbo-frame id=\"preview\">\n{% if bookmark_list.is_empty %}\n  <div>No bookmarks match the current bundle.</div>\n{% else %"
  },
  {
    "path": "bookmarks/templates/opensearch.xml",
    "chars": 457,
    "preview": "<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\" xmlns:moz=\"http://www.mozilla.org/2006/browser/searc"
  },
  {
    "path": "bookmarks/templates/registration/login.html",
    "chars": 1284,
    "preview": "{% extends 'shared/layout.html' %}\n{% load shared %}\n{% block head %}\n  {% with page_title=\"Login - Linkding\" %}{{ block"
  },
  {
    "path": "bookmarks/templates/registration/password_change_done.html",
    "chars": 420,
    "preview": "{% extends 'shared/layout.html' %}\n{% block head %}\n  {% with page_title=\"Password changed - Linkding\" %}{{ block.super "
  }
]

// ... and 205 more files (download for full content)

About this extraction

This page contains the full source code of the sissbruecker/linkding GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 405 files (1.7 MB), approximately 424.5k tokens, and a symbol index with 2534 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!