Closes#5644. Part of #3317.
22 `aqt` modules already type-check cleanly under `strict_optional`, so
this opts them in. Config only — no code changes.
Verified clean with `mypy --platform` for `darwin`, `linux` and `win32`.
`aqt.mpv` is left out: it sets `ignore_errors = True`, so a stanza there
would be a no-op.
## Linked issue (required)
Fixes#5700
## Summary / motivation (required)
Since #5637, `setupGL()` runs after `AnkiApp` is constructed. Some of
what it sets is only read by Qt at startup (`AA_UseSoftwareOpenGL` on
macOS, `QT_OPENGL` on Windows), so safe mode and the Software driver are
no longer fully applied.
This calls `setupGL()` before creating the app again, where it was
before #5637. Safe mode is decided at that point, from `--safemode` or
from Shift. Since Qt can only detect Shift once the app exists,
`shift_held_before_app()` checks it with platform APIs:
`CGEventSourceFlagsState()` on macOS and `GetAsyncKeyState(VK_SHIFT)` on
Windows.
If Qt still detects Shift after the app is created but the check above
didn't (e.g. on Linux, where it isn't implemented), `setupGL()` is
called again with the Software driver. On Linux the settings it makes
still take effect at that point, so that path keeps working as it does
today. Happy to drop that fallback if you'd prefer.
## Steps to reproduce (required, use N/A if not applicable)
1. On macOS, run `/Applications/Anki.app/Contents/MacOS/anki --safemode`
from a terminal
2. The console shows `Qt warning: Attribute Qt::AA_UseSoftwareOpenGL
must be set before QCoreApplication is created.`
## How to test (required)
### Checklist (minimum)
- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
- `qt/tests/test_startup_video_driver.py` runs `_run()` with a stand-in
for `AnkiApp` and checks that `setupGL()` is called with the expected
driver (profile driver, or Software with `--safemode` or Shift held)
before the app is constructed. Fails on main, passes with this change.
- Launched the source tree on macOS with a temporary base folder, with
`--safemode`: the warning above appears on main and is gone with this
change. With Shift held, `shift_held_before_app()` returns True and the
warning doesn't appear either.
- On Windows 11, checked the `GetAsyncKeyState(VK_SHIFT)` call used
here: it returns False before Shift is pressed and True while it's held.
I haven't tested a full Anki launch on Windows.
- ruff check/format pass on the changed files. I haven't been able to
run the full `just check` locally yet, so I'm relying on CI for the
rest.
## Before / after behavior (optional)
Before: `--safemode` and the Software driver don't set
`AA_UseSoftwareOpenGL` (macOS) / `QT_OPENGL` (Windows) in time. After:
they're applied before the app is created.
## Risk / compatibility / migration (optional)
This touches the same startup block as #5686, so whichever lands second
will need a small rebase. I'll take care of it if this one goes second.
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Closes#5627
## Summary
This improves graphics error detection so that the "Failed to create RHI
for backend" error returned by Qt is matched. The error message
apparently changed somewhere around Qt 6.11. We have at least one report
from 26.05b1: https://forums.ankiweb.net/t/anki-26-05-beta-1/69707/27
## How to test
Try:
```
echo opengl > ~/.local/share/Anki2/gldriver6 # or delete the file
QT_QPA_PLATFORM=xcb QT_XCB_GL_INTEGRATION=none ./run
```
According to my tests on a Ubuntu 26.04 VM, both "Failed to create RHI"
and "Failed to create QRhi" got printed, so I couldn't reproduce the
case where the error message is not shown. This is hardware-dependent.
## Linked issue (required)
Fixes#5716
## Summary / motivation (required)
During a full download, Anki loads the whole collection into memory
before saving it to disk. With a large collection, iOS can kill the app
partway through the sync because of OOM. See #5716 for details.
This PR streams the download straight into the temporary file instead,
so memory usage no longer grows with the collection size.
## Steps to reproduce (required, use N/A if not applicable)
<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
- Mainly for bugfixes;
- For bugs: numbered steps before the fix. For non-bugs: write N/A.
- use N/A for features, refactors, docs, chore, etc.
-->
1. Set up a self-hosted `anki-sync-server`, and put a large enough
collection database(My collection is ~3GiB)
2. On AnkiMobile or Amgi, with an empty local collection, point the
client to the self-hosted server
3. Start full sync, and then it get killed because of OOM
## How to test (required)
<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->
- Pointed [Amgi](https://github.com/antigluten/amgi)'s `anki-upstream`
submodule at this branch, then ran a full download of a ~3 GiB
collection from a self-hosted sync server on iOS. The app no longer gets
killed for running out of memory, and the sync completes. AnkiMobile is
close-source so I can not test it.
- Built and ran Anki desktop from this branch. It starts and works
normally.
- Ran the `cargo test -p anki --lib sync::`
### Checklist (minimum)
- [X] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
## Before / after behavior (optional)
Before: A full download loads the whole collection into memory before
saving it, so on iOS, syncing a large collection (e.g. ~3 GiB) can get
the app killed for running out of memory.
After: The collection is saved to disk as it downloads
Measured peak memory while downloading a 512 MiB collection from a LAN
server: it dropped from about 534 MB to about 22 MB with Amgi.
## Scope
- [X] This PR is focused on one change (no unrelated edits).
## Linked issue (required)
Fixes#5676
## Summary / motivation (required)
#5637 moved the `QT_SCALE_FACTOR` assignment to after `AnkiApp` is
created, but Qt only reads it when the app is constructed, so the "User
interface size" setting stopped doing anything. this sets it before the
app is created again; `setupGL()` stays where it is (smaller version
suggested in review)
## Steps to reproduce (required, use N/A if not applicable)
set "User interface size" to 200% in preferences, restart, the UI is
still the default size
## How to test (required)
### Checklist (minimum)
- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
qt/tests/test_startup.py runs `_run()` with a stand-in for AnkiApp and
checks QT_SCALE_FACTOR is already set when it's constructed, fails on
main and passes with this. also checked with the 26.9.3 wheels on
Windows at 150%: device pixel ratio 1.5 before, 3.0 after
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue (required)
Fixes#5691
## Summary / motivation (required)
Updates the **Get Shared** link to use the current `/shared/decks` URL
directly instead of `/shared/decks/`, which redirects to it.
## Steps to reproduce (required, use N/A if not applicable)
1. Open Anki's deck list.
2. Click **Get Shared**.
3. Observe that `/shared/decks/` is opened and redirects to
`/shared/decks`.
## How to test (required)
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
Ran `./ninja check:pytest:aqt` successfully.
Manually verified that `/shared/decks/` redirects to `/shared/decks`,
and
that `/shared/decks` loads directly. No automated test was added because
this is a trivial one-line URL correction.
## Before / after behavior (optional)
Before: **Get Shared** opens `/shared/decks/` and relies on a redirect.
After: **Get Shared** opens `/shared/decks` directly.
## Risk / compatibility / migration (optional)
N/A. This is a one-line URL change.
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue (required)
Fixes https://github.com/ankitects/anki/issues/5285
- Follow up to https://github.com/ankitects/anki/pull/5277
Also allows forgetting curve to appear when the memory states are
absent.
## Summary / motivation (required)
The frontend was inferring FSRS state from indirect signals such as
memoryState or desiredRetention. This can be incorrect and it's better
to rely on the actual bool from the Rust backend instead.
## Steps to reproduce (required, use N/A if not applicable)
1. Have a card with no memory states and no desired retention but FSRS
enabled (e.g. by setting "Ignore reviews before" to a date later than
its latest review)
2. Open card info
3. Ease is shown and forgetting curve is not shown.
## How to test (required)
On repeating the above steps,
- ease is not shown
- forgetting curve is shown (to show the forgetting curve, you need to
follow the steps with a memory-state-less card from an earlier Anki
version by changing deck. If you set "ignore reviews before" to a later
date, there will be no forgetting curve)
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
One test is removed because it's not possible to have memory states with
FSRS disabled.
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue (required)
Closes#5681
## Summary / motivation (required)
When reusing existing media files instead of readding, we currently pass
back the normalised name given to us when it should be the name of the
file on disk instead, which this pr fixes
## Steps to reproduce (required, use N/A if not applicable)
See linked issue
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue
Closes#5282
## Summary
This adds a new workflow that submits a PR to merge a release/* branch
back to main once a stable release is published.
## How to test
I tested the workflow in my personal fork and confirmed it's working:
- PR: https://github.com/abdnh/anki/pull/12
- Workflow run: https://github.com/abdnh/anki/actions/runs/35910375389
### Notes
- An admin needs to enable the "Allow GitHub Actions to create and
approve pull requests" setting under
https://github.com/ankitects/anki/settings/actions
- This workflow can be later extended to submit a PR to update the
download links in the landing page: #5672
## Linked issue (required)
Closes#5679
## Summary / motivation (required)
The "pre-built Python wheels" section in both `docs/development.md` and
`docs-site/developers/development.mdx` says you need Python 3.9 or later
and recommends 3.9 because 3.10+ had minimal testing. The packages now
declare `requires-python = ">=3.10"` (`pylib/pyproject.toml`,
`qt/pyproject.toml`), so pip on 3.9 won't install them at all. This
updates both copies to 3.10 or later and drops the outdated 3.9
recommendation.
This is my first PR, so it also adds my name to `CONTRIBUTORS`.
## Steps to reproduce (required, use N/A if not applicable)
N/A (docs).
## How to test (required)
### Checklist (minimum)
- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
Docs-only; compared against `requires-python` in every `pyproject.toml`
in the repo (the root dev environment is `>=3.12`, the published
packages are `>=3.10`).
## Scope
- [x] This PR is focused on one change (no unrelated edits).
AI assistance: found and prepared with Claude Code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Linked issue
Closes#5385
## Summary
This adds tests for rslib/src/deckconfig/service.rs and some related
modules.
## How to test
`cargo test -p anki deckconfig`
The cloze tokenizer in `rslib/src/cloze.rs` previously treated any
occurrence of `}}` as a cloze terminator, even when it appeared inside
MathJax expressions.
Example:
{{c1:: \( \frac{1}{ \sqrt{ \pi }} \) }}
The `}}` from `\sqrt{\pi}}` would cause the cloze to terminate early,
resulting in incorrect parsing and rendering.
<img width="725" height="257" alt="Screenshot 2026-03-14 at 11 17 21 AM"
src="https://github.com/user-attachments/assets/76e475ed-b3ab-4bb5-9538-2e65d3a136fe"
/>
<img width="610" height="253" alt="Screenshot 2026-03-14 at 11 17 46 AM"
src="https://github.com/user-attachments/assets/ecefe3ad-960d-4fd8-a2d9-501edef11679"
/>
`normal_text()` now tracks when the scanner is inside a MathJax region
(`\(...\)` or `\[...\]`). While inside MathJax, `}}` is ignored as a
cloze terminator.
Cloze openings are still recognized inside MathJax.
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Closes#5391
## Summary / motivation
Adds inline coverage for the previously under-tested sync upload
(`rslib/src/sync/collection/upload.rs`, ~51%) and login
(`rslib/src/sync/login.rs`, 0%) paths.
**upload.rs**
- `check_upload_limit`: below-limit, the inclusive `>=` boundary
(`UploadTooLarge`), and the `"X.XX MB > Y.YY MB"` user-facing message.
- `gzipped_data_from_vec`: gzip round-trip + empty input.
- `handle_received_upload`: valid data replaces the collection; corrupt
bytes return `CORRUPT_MESSAGE` and leave the existing collection intact
(verified in memory and after reopening); a closed collection yields a
500.
- `full_upload_with_server`: a server rejection is mapped to
`SyncErrorKind::ServerMessage` (wiremock + zstd-encoded body).
**login.rs**
- `sync_login` success: returns the server-issued host key and sends the
request serialized with the legacy `{"u","p"}` field names.
- Error mapping: 403 → `AuthFailed`, 500 → `ServerError`, and a closed
connection → `NetworkError`.
- `HostKeyRequest` serde renames.
Tests use `wiremock` (already a dev-dependency) so they are
self-contained and deterministic, with no dependency on AnkiWeb.
No production code was changed; existing test seams were reused.
## How to test
### Details
```
cargo test -p anki sync::login sync::collection::upload
just check
```
All new tests pass; `just check` is green.
## Linked issue
Closes#5390
Refs #5407 (stale retrievability fallback)
## Summary / motivation
Adds unit-test coverage for three under-tested storage areas called out
in #5390, and fixes a bug found while writing it:
- **storage/card/filtered.rs**: tests for `order_and_limit_for_search`
across every `FilteredSearchOrder` variant, plus the
Due/RelativeOverdueness and FSRS retrievability fragments.
- **storage/revlog/mod.rs** (previously no tests): `add_revlog_entry`
id-uniquify logic, get lookups, unknown-review-kind fallback,
`time_of_last_review`, and the `studied_today` / `studied_today_by_deck`
filters (window boundary, distinct-card counting, kind/factor rules).
- **storage/sqlite.rs**: schema-version migration gating in
`open_or_create` (FileTooOld / FileTooNew / the 12–13 special case) and
the invariant that a rejected file is left untouched.
- (Plus) **storage/upgrades/mod.rs**: a schema-11 downgrade→upgrade
roundtrip proving migrated notetypes, decks, presets, config, tags and
graves survive intact.
**Bug fix (filtered.rs):** when a filtered deck kept a saved
retrievability order after FSRS was disabled,
`build_retrievability_query` returned an empty string, so
`order_and_limit_for_search` produced SQL with a leading comma (`",
fnvhash(...) limit N"`) that fails at runtime. It now falls back to
`random()`, matching the filtered-deck dialog's behavior.
## Steps to reproduce
1. Create a filtered deck whose order is "Retrievability".
2. Enable FSRS, then later disable it (leaving the saved order in
place).
3. Rebuild the filtered deck → the generated ordering SQL starts with a
comma and errors instead of falling back to a valid order.
## How to test
### Details
- `just test-rust` (new tests under `storage::card::filtered`,
`storage::revlog`, `storage::sqlite`, `storage::upgrades`).
- `just check` for the full format/lint/build/test gate.
- The retrievability fix has a regression test
(`retrievability_orders_fall_back_to_random_without_fsrs`) that builds a
collection and runs the generated clause through `search_cards`, which
would fail on the old leading-comma SQL.
## Linked issue (required)
Fixes#5510
## Summary / motivation (required)
Expands the manual translation instructions as requested in #5510:
- `docs-site/translators/anki/manual.mdx` now contains detailed
instructions for translating the documentation: forking the repository,
working in a language subdirectory (existing or new), keeping Markdown
formatting intact while translating the MDX pages, registering
translated pages in `docs.json` (with an example following the shape of
the existing language entries), and submitting a PR. It also notes that
partial translations are welcome, that documentation changes are
licensed under CC BY-SA 4.0, and where to ask questions.
- The Translations section of `docs-site/README.md` now links to that
page instead of holding the details itself, reversing the previous
direction per the issue.
- The Translations sections of `docs-site/developers/contributing.mdx`
and the cog-generated `docs/contributing.md` briefly mention
documentation translations, which were previously only covered for
interface strings.
Also adds myself to the CONTRIBUTORS file in a separate commit, as
requested for first pull requests.
## Steps to reproduce (required, use N/A if not applicable)
N/A
## How to test (required)
Documentation-only change. Verified that:
- All paths referenced in the new instructions exist in the repository
(e.g. `docs-site/ru/manual/`, `docs-site/manual/`, and the other
language subdirectories).
- The `docs.json` example matches the structure of the existing language
entries under `navigation.languages`.
- Both internal links resolve to pages registered in
`docs-site/docs.json` (`/translators/anki/manual`,
`/translators/anki/developers`).
- No MDX-special characters (`{`, `<`) appear outside fenced code blocks
in the new content, so `mintlify validate` should pass.
- `docs/contributing.md` stays in sync with
`docs-site/developers/contributing.mdx` under the transformation in
`docs/cogdocs.py`.
### Checklist (minimum)
- [ ] I ran `./ninja check` or an equivalent relevant check locally.
*Not applicable for a documentation-only change; the relevant check is
the Docs Site workflow (`mintlify validate` / `mintlify a11y`), which
runs in CI.*
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
*N/A - documentation-only changes are exempt from tests per
`contributing.md`.*
## Before / after behavior (optional)
N/A
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Fixes#5657
## Summary / motivation
SonarCloud PR analysis derives the changed-file set from git and
requires the target branch to be present in the local repository. The
scan checks out only the fork's head history (`fetch-depth: 0` on the
head ref), so the base branch was never fetched. Without a base to diff
against, the scanner attributed every analysed file to the PR as new
code, e.g. 977 files on the single-file change in #5641
(`qt/aqt/preferences.py`), and recurring noise in `tools/`.
This adds a step that fetches the target branch from the trusted
upstream repository (`github.repository`, never the fork) and pins it
locally under the name passed as `sonar.pullrequest.base`, so new code
is measured against the real PR diff. It does not disable any rule or
exclude any file.
The isolation boundary is preserved: the upstream branch is read as data
only, nothing is built or executed, the trusted config and
no-fork-code-execution constraints are unchanged, and the Quality Gate
stays informative.
## Steps to reproduce
1. Open a fork PR that changes a single file (e.g. #5641, base `main`).
2. Let CI complete and the SonarCloud workflow run.
3. Observe SonarCloud reporting new-code issues across hundreds of
unchanged files (~977) instead of only the changed file.
## How to test
### Details
Reproduced the scanner's git-based new-code computation locally against
the exact commits from #5641:
- head = `1e7914a`
- base branch `main` fetched from upstream → `merge-base` = `bdeec0877`,
which matches the PR's `baseRefOid` reported by the GitHub API.
- `git diff --name-only <merge-base> <head>` → **only**
`qt/aqt/preferences.py`.
The new step also logs the analysed commit, base repo/branch, base
commit and merge-base, and fails closed if the base reference cannot be
resolved (no silent fallback to a missing base). YAML parse and the
cog-generated `docs/sonarcloud.md` sync were verified.
## Before / after behavior
- Before: fork/PR scan treats all indexed files as new code (977 files
for a
one-file PR).
- After: only the files actually changed vs the upstream base are
considered
new code.
closes#5652
`col.fsrs_enabled` should be usable in any place where we have access to
the collection (And that doesn't involve building the queue or modifying
the config).
c.c. @user1823
Unfortunately the change in #5626 to skip the scope dialog in the MSI
installer depending on the detected installation does not work as
expected in some cases. To reproduce:
- Install 26.09 on all-users scope.
- Install 26.09.2 on all-users scope.
- Bump .version (26.09.3) and build a new installer with #5626
- Install 26.09.3 and notice the default installation path is
`C:\Users\%USERNAME%\AppData\Local\Programs\Anki`, despite there being
two all-users installations.
Reverting for now. We can revisit it for the next major release.
fixes#5635
We could fix specifically the SM2 part of this bug with just the
"fsrs_enabled" part; but then if we encounter a card with the memory
state actually missing while reviewing I think we would get the same
error. So now any calculated memory state is only shown in the card info
and not writ to the database after being calculated.
## What was happening
When I set up the Sonar PoC, I pinned `sonar.projectVersion` to `0.1`.
The New Code period is "previous version", so with the version never
changing, the baseline stayed frozen at that first analysis. Everything
merged since then kept accumulating as "new code" instead of rolling off
at each release, which is why the Quality Gate on `main` has been red
since August (e.g., coverage for "new code" is at 62%, below the 80%
minimum).
## The fix
Now the Sonar version follows the project version instead of being
pinned, so every release bump gives us a fresh baseline. Pulled from the
trusted default-branch checkout so a fork can't tamper with it.
## How it works from now on
PRs won't change: Sonar already checks only your diff, so the 80%
coverage gate applies just to the lines you touched, not the old debt.
For `main`: the baseline now resets on each release. Just note it moves
on the next version bump, not on this merge, so `main` stays red until
then, and after that "new code" is only what changed since the last
release.
Closes#5629
I decided to do this by moving safe mode to "app" rather than by moving
the rendering code to the main window
(2db70434ea1412b4513479236cd74464de986b6a)
If 2db70434ea1412b4513479236cd74464de986b6a is preferred then I'll
switch back to it
## Linked issue
Closes#5037
## Summary
The MSI installer fails to detect existing installations and results in
multiple versions being installed. This was fixed in #5038 but
apparently later template changes reintroduced the issue (Windows
Installer is full of footguns and is very fussy about ordering...).
## Steps to reproduce (before)
- Install 26.09 in all-users scope.
- Install 26.09.2 in all-users scope and confirm it results in two
entries in _Settings > Apps > Installed Apps_.
## How to test (after)
Repeat the same process. All-users scope should now by selected by
default if an existing installation is detected and replaces the
existing one correctly.
AnkiWebPage.acceptNavigationRequest() did not check the full host and
port before accepting navigation requests, which made it possible for
external URLs to be opened in a trusted webview with access to pycmd().
This also adds a port check to AuthInterceptor so the API key is not
sent to a localhost server on a different port.
_This fix is already included in 26.09.1_.
## Linked issue
Closes#5387
## Summary / motivation
Add tests for the `TagsService` protobuf layer, covering all service
endpoints, request conversions, persisted changes, and representative
error paths.
This completes the remaining tags service coverage requested in #5387.
The media service portion was previously covered by #5588.
## How to test
### Details
Ran:
- `just test-rust`
- `just test-rust --coverage --html`
All Rust tests pass. Production code in `rslib/src/tags/service.rs` has
80/80 lines covered (100%).
<img width="1143" height="24" alt="image"
src="https://github.com/user-attachments/assets/fc8dd1d7-4903-4528-a92d-f985d7fafe7f"
/>
## Before / after behavior (optional)
Before: `rslib/src/tags/service.rs` had no direct test coverage.
After: all `TagsService` endpoints are covered, including persisted
mutations, protobuf request handling, empty-ID behavior, tag-name
canonification, and explicit error variants.
## Linked issue
Closes#5384
## Summary / motivation
`rslib/src/card_rendering/service.rs` had 0% test coverage, whereas the
issue asks for at least 70%.
This PR adds golden unit tests that drive the `CardRenderingService`
trait implementation directly through qualified trait syntax, so that
the service layer itself gets exercised (its proto conversion,
validation and mode/flag dispatch) rather than the inherent `Collection`
render methods it delegates to.
Coverage, grouped by the issue's areas:
- **Question/answer rendering**: `render_uncommitted_card` /
`render_existing_card` produce the expected nodes, css and `is_empty`.
- **Template filters**: a known filter is applied; an unknown filter is
ignored when `partial_render` is false and emitted as a replacement node
when true, including when preceded by literal text.
- **Edge cases**: empty fields report `is_empty`; cloze rendering; the
empty-card report for both a note kept alive and a note marked for
deletion.
- **Error handling**: missing template/note and unknown card id return
the concrete `InvalidInput` variant and message; invalid legacy template
bytes return `JsonError`, so that none of these paths panic.
- **Delegating methods**: av-tag/latex extraction, markdown
(with/without sanitize), IRI path round-trip, strip-html modes,
html-to-text, compare-answer, cloze-for-typing, and the legacy render
path.
No production code was changed, since this PR adds tests only.
## How to test
### Details
- `cargo test -p anki card_rendering::service` → 27 passed.
- `just test-rust` → full Rust suite green; `cargo fmt --check` and
`clippy` clean on the changed file.
- `just test-rust --coverage` → `service.rs` now at 100% function and
~99% line coverage; the only uncovered lines are test-only guard arms
(`_ => None` fallbacks and `other => panic!(...)` in error matches),
which execute only on test failure and are intentionally left uncovered
rather than padded.
- Note: the repository-wide `just check` also runs the web/Python
stacks; this change is Rust-test-only, so the relevant Rust checks above
are the ones that apply.
<img width="994" height="28" alt="image"
src="https://github.com/user-attachments/assets/67479d89-66bc-4a2e-bdbd-fe16a0da3114"
/>
closes#5598
I tried to use a glob but it didn't like redirecting based on it ending
in .html 😢
```json
{
"source": "/:glob.html",
"destination": "/manual/:glob"
}
```
```json
{
"source": "/:glob*.html",
"destination": "/manual/:glob*"
}
```
Does not work. So I brute forced it.
## Linked issue
Closes#5603
## Summary
Briefcase recently started to adding the "lib" subfolder to PYTHONPATH
(https://github.com/beeware/briefcase-windows-VisualStudio-template/pull/105).
This unfortunately caused unexpected issues with users trying to upgrade
to 26.09 who happened to have leftover lib/anki/_rsbridge.pyd libraries
(probably resulting from failed uninstalls) from PyOxidizer-based
pre-25.07 builds.
The fix suggested here is to revert the upstream change. This involves
switching from the pre-built
[briefcase-windows-app-template](https://github.com/ankitects/briefcase-windows-app-template)
template to
[briefcase-windows-VisualStudio-template](https://github.com/ankitects/briefcase-windows-VisualStudio-template).
I made sure to port all Anki-specific changes we made to the Wix
template (such as uninstall handling for the old NSIS-based installer).
## Steps to reproduce (before)
- Install 26.09
- Download and extract a lib folder from a PyOxidizer-build such as
[25.02.7](https://github.com/ankitects/anki/releases/tag/25.02.7) (You
can unzip the .exe using [7-zip](https://www.7-zip.org/)).
- Move the lib folder to the installation folder of 26.09
- Run Anki and confirm you get an error.
## How to test (after)
Repeat the same process but with this PR (`./tools/ninja installer`) and
confirm the lib subfolder has no effect.
The deprecation warnings added in #5547 incorrectly mention
_LegacyAnkiPackageExporter/_LegacyAnkiPackageImporter as the replacement
APIs instead of
Collection.export_anki_package/Collection.import_anki_package.
_This fix is already included in 26.09_
## Linked issue
Closes#5592
## Summary
Our Briefcase build didn't respect locked dependencies and dependency
cooldowns and used to resolve newer versions of some packages such as
`click` and `protobuf`.
This PR fixes the issue by exporting a list of pinned dependencies to a
file and passing it to uv using the `--constraints` option.
## Steps to reproduce (before)
- Build the bundle: `./ninja installer:build`.
- Inspect `out/installer/build/anki/*/app_packages` and notice
`protobuf-7.36.1.dist-info` is included despite it being locked to
6.33.5 in uv.lock
## How to test (after)
Build the bundle again and confirm there's no mismatch between locked
and installed dependency versions.
## Linked issue
Follow-up to #5498, which was closed in favor of a smaller and more
maintainable implementation.
## Summary / motivation
Run CI-based SonarCloud analysis for internal and fork pull requests
while keeping the Quality Gate informational.
The analysis matrix is:
| Event | Static analysis | Coverage in CI | Coverage in SonarCloud |
| --- | --- | --- | --- |
| Internal pull request | Yes | Yes | Yes |
| Push to `main` or `release/**` | Yes | Yes | Yes |
| Fork pull request | Yes | Yes | No |
Fork pull requests continue to run the full test coverage and
coverage-regression checks in the unprivileged CI workflow. Their
contributor-produced coverage reports are not passed to the privileged
SonarCloud workflow.
The fork scan reports static bugs, vulnerabilities, security hotspots,
and code smells. It:
- validates the PR state, repositories, branches, and tested SHA against
the GitHub API;
- checks out the exact revision tested by CI;
- replaces `sonar-project.properties` with the trusted default-branch
version;
- provides `SONAR_TOKEN` only to the scanner step;
- never builds, installs, or executes contributor code;
- keeps scanner-side Clippy and SCA disabled;
- clears coverage report paths and excludes coverage calculation for
forks.
Automatic Analysis remains disabled because it cannot be combined with
CI-based analysis and does not provide the required Rust and coverage
support.
This also configures Vitest to produce repository-relative LCOV paths
such as `SF:ts/lib/...`, allowing SonarCloud to resolve TypeScript files
when reports are downloaded into a separate checkout.
## How to test
### Checklist
- [x] I ran `just check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed. The remaining behavior is specific to GitHub's
`workflow_run` environment and can only be exercised end-to-end after
the workflow is available on the default branch.
### Details
The following checks passed locally:
- `just test-ts --coverage`
- `just test-ts --coverage --html`
- `just fmt`
- `just check`
- YAML and JSON parsing
- `git diff --check`
Both TypeScript coverage runs passed all 67 tests. The generated LCOV
report was also checked to confirm that source paths are
repository-relative and begin with `SF:ts/`.
After this lands, end-to-end validation should cover:
1. A push to `main`, confirming that Python, TypeScript, and Rust
coverage reports are imported.
2. An internal PR, confirming static analysis and coverage.
3. A fork PR, confirming static analysis while the coverage download is
skipped and no coverage condition is evaluated.
## Before / after behavior
Before:
- fork PR analysis fails at the protected checkout;
- the `actions/checkout` version comments are outdated;
- TypeScript LCOV paths are relative to `ts/` and may not resolve in the
SonarCloud checkout.
After:
- internal PRs and trusted branch pushes receive static analysis and
coverage;
- fork PRs receive static analysis without importing
contributor-produced coverage;
- fork tests and coverage-regression checks continue to run in regular
CI;
- TypeScript LCOV paths resolve from the repository root;
- technical scanner failures remain visible without making the Sonar
Quality Gate a required merge check.
## Risk / compatibility / migration
The fork analysis runs in a privileged `workflow_run` and necessarily
asks the SonarScanner to parse untrusted source while it has access to a
project-scoped token.
The workflow limits this exposure by using GitHub-hosted runners,
read-only permissions, trusted scanner configuration, an exact tested
SHA, API-validated PR metadata, and no builds, dependency installation,
local Actions, caches, or fork-produced coverage artifacts.
This is intentionally a smaller trust boundary than the data-only
snapshot and custom coverage parser proposed in #5498. It prevents
direct execution of contributor code but does not eliminate potential
vulnerabilities in GitHub Actions or Sonar analyzers.
Automatic Analysis must remain disabled, `SONAR_TOKEN` should remain
restricted to this project, and the SonarCloud check must remain
non-required.
## Linked issue (required)
Fixes#5342
## Summary / motivation (required)
When installing an add-on from AnkiWeb, Anki currently derives the
displayed
name from the AnkiWeb download filename and passes it as overriding
manifest
metadata. This can alter names when the filename has been sanitized by
AnkiWeb.
This change prefers a non-empty `name` from a valid packaged
`manifest.json`
when available, while retaining the existing AnkiWeb-derived name as a
fallback.
## Steps to reproduce (required, use N/A if not applicable)
1. Package an add-on with a `manifest.json` whose `name` contains a
character
that is removed from the AnkiWeb download filename, such as `&` or `𝕾`.
2. Upload the add-on to AnkiWeb and install it from there.
3. Open the Add-ons dialog and observe that the displayed name is
derived from
the sanitized AnkiWeb filename rather than the packaged `name`.
## How to test (required)
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
Added tests verifying that:
- a non-empty `name` from a valid packaged manifest is preferred for
AnkiWeb
installs;
- the existing filename-derived name remains the fallback when no usable
packaged name is available.
`./ninja check` passes locally.
Also manually verified with a local build by installing an AnkiWeb
add-on whose
packaged name differed from its AnkiWeb title; the Add-ons dialog used
the packaged name.
## Before / after behavior (optional)
Before: AnkiWeb installs always use the filename-derived name,
overriding a
`name` supplied in the packaged manifest.
After: A non-empty packaged `name` is preferred when available;
otherwise,
the existing AnkiWeb-derived name is retained.
## Risk / compatibility / migration (optional)
Low. The change is limited to AnkiWeb add-on installation, and the
existing
fallback behavior is preserved for add-ons without a usable packaged
name.
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue (required)
Fixes#5567
## Summary / motivation (required)
Pasting a base64 image in the new editor currently corrupts its bytes,
and the HTML paste path can put image markup into an image's `src`.
Decode base64 directly into bytes and return the saved filename from the
inline-image helper. Read the clipboard PNG preference's boolean value
so a disabled preference selects JPEG.
Add regressions through the public clipboard APIs for plain-text and
HTML data URLs and both image-format settings. Include editor route
sources and tests in the Vitest build inputs so edits to these
regressions reliably rerun the tests.
## Steps to reproduce (required, use N/A if not applicable)
Using a disposable profile on the base revision:
1. Open the new editor and disable "Paste without shift key strips
formatting" to enable extended paste.
2. Paste the base64 PNG data URL from the linked issue as plain text,
then as the source of an image in clipboard HTML. Inspect the stored
media bytes and inserted image source: the bytes change and the HTML
source is invalid.
3. Disable "Paste clipboard images as PNG" and paste a bitmap image
through the clipboard path. PNG is still selected.
4. After a successful `just test-ts`, edit only an editor route test and
rerun it. The build incorrectly reuses the prior Vitest result.
## How to test
- [x] `RELEASE=1 just check` passed on this focused upstream-based
branch (`ee16fc015`), using Apple Silicon macOS and the repository's
pinned toolchains.
- [x] Added or updated regression coverage for the changed behavior.
Clipboard regressions check exact decoded bytes, a local media filename
in the inserted image, and both PNG/JPEG preference values. Confirmed
route test edits invalidate the Vitest target. Clipboard/backend APIs
are mocked; no user collection is accessed.
## Risk / compatibility / migration (optional)
No database migration or new dependency. The legacy paste branch is
unchanged. The build input change only causes relevant source/test edits
to rerun Vitest. Complete local validation was on macOS; other platforms
depend on CI.
## UI evidence (required for visual changes; otherwise N/A)
N/A: no visual design changes. Regression tests inspect the inserted
image markup.
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Fixes#5336
## Summary / motivation
The Windows installation guide lists only a generic 64-bit Windows
requirement, even though Anki provides separate x64 and ARM64 installers
with different Windows requirements. Document both configurations and
explain how to match the processor type shown in Windows Settings to the
download label.
## Steps to reproduce
N/A: documentation only.
## How to test
- [x] `RELEASE=1 just check` passed on the focused branch on macOS Apple
Silicon.
- [x] No unit tests added for a documentation-only change.
- Confirmed the requirements and download labels against [Anki's
download page](https://apps.ankiweb.net/): Windows 10+ (x64) and Windows
11 (ARM).
- Confirmed that release 26.08.1 includes separate `win-x64.msi` and
`win-arm64.msi` installers.
- Checked the Settings path against [Microsoft's system-type
instructions](https://support.microsoft.com/en-us/windows/32-bit-and-64-bit-windows-frequently-asked-questions-c6ca9541-8dce-4d48-0415-94a3faa2e13d)
and [Windows ARM64
guidance](https://www.microsoft.com/en-us/software-download/windows11arm64).
- Mintlify 4.2.884: build validation and accessibility checks passed.
The site-wide accessibility scan reports 32 pre-existing parse warnings
on other pages; the edited Windows page is checked successfully.
## Risk / compatibility / migration
Documentation only. Existing guidance for older Windows releases and
32-bit systems is retained. No installer or platform support changes.
## UI evidence
N/A: documentation only.
## Scope
- [x] Focused on Windows requirements and choosing the matching
installer.
---------
Co-authored-by: Abdo <abdo@abdnh.net>