Syntax error from internal Anki JS on iOS Mobile when using template with <script> tag

Anki Mobile Version: 25.02.1
iOS Version: 18.3.1 (iPhone 16 Pro)

Hi. I have been using a <script> tag in my template with JavaScript that adds a timer to cards and sounds an alarm after an interval to help stay focused during reviews. I got the idea from this script Foxy-null made[0] and it has been working perfectly on Anki Web, Desktop, and Mobile until today. It is still working as expected on Desktop (version 24.06.3) and Web, but on the Mobile iOS app the timer does not appear as usual and the script appears to not be working at all, even though nothing has changed on my template.

I added a debug script that outputs a stack trace when window.onerror is triggered. When my timer script is in the template, I see this stack trace on my card:

Error: SyntaxError: Unexpected token '%'
Source: ankifile://reviewer/
Line: 13:3571
Stack:
replaceChild@[native code]
Z@ankifile://reviewer/res/web/js/reviewer.js:1:3571
@ankifile://reviewer/res/web/js/reviewer.js:1:4028
r@ankifile://reviewer/res/web/js/reviewer.js:1:498

This is very odd, because there is no “%” character anywhere in my script or template or CSS. Not in my fields either as far as I can tell. I will paste the contents of my script below. When I remove the script, the error goes away, but of course that’s not helpful because then I can’t use my timer.

Again it’s working in Web, and Desktop. It even works when I visit AnkiWeb in the browser on iOS, but just not in the app.

I know this is a niche issue but I hope I can get help with this because this custom functionality is very important to me as I am a bit ADHD minded and this has made Anki much more useable for me.

I went ahead and made a test card with the minimum HTML/CSS/JS needed to reproduce the issue. Here is the contents of each template. Replace {{Any-Field}} with a field that exists in your note.

FRONT:
Expected behavior: see contents of your field and a timer counting down to zero, followed by a chime playing.

{{Any-Field}}

<span id="s1" style="font-size: 16px; color: black"></span>

<div class="alert-box" id="show-alert" style="display: none">
    Wake up! You have been looking at<br />the question for
    <span id="seconds" style="font-weight: bold">???</span> seconds!
    <div id="alert-audio"></div>
</div>

<script>
    var time_min = 0;
    var time_sec = 15; // how long the user has before "Time is up"
    var warn_sec = 10; // after this much time has passed, show a warning and play a sound

    // After time is up, how long to wait before repeating beeps
    var timeUpDelay = 10; // in seconds
    // Interval for repeated beeps (in ms) after timeUpDelay
    var beepIntervalMs = 1000;

    var warn_ms = (time_min * 60 + time_sec - warn_sec) * 1000;
    var audio_url = "https://assets.mixkit.co/active_storage/sfx/765/765-preview.mp3";
    var audio_str = '<audio autoplay><source src="' + audio_url + '" type="audio/mp3" /></audio>';

    // We'll use two separate storage keys so we can clear them easily
    // one for timeouts, one for intervals
    var _timeout_storage_key = "anki_timeout_id";
    var _interval_storage_key = "anki_interval_id";

    // -----------------------------
    // Cancel any previously set timeout or interval
    // -----------------------------
    function cancelTimersAndIntervals() {
        let tId = sessionStorage.getItem(_timeout_storage_key);
        if (tId !== null) {
            clearTimeout(parseInt(tId));
            sessionStorage.removeItem(_timeout_storage_key);
        }
        let iId = sessionStorage.getItem(_interval_storage_key);
        if (iId !== null) {
            clearInterval(parseInt(iId));
            sessionStorage.removeItem(_interval_storage_key);
        }
    }

    // -----------------------------
    // setTimeout that saves the ID
    // -----------------------------
    function setTimeoutCancellable(func, delay) {
        let new_id = setTimeout(func, delay);
        sessionStorage.setItem(_timeout_storage_key, new_id.toString());
    }

    // -----------------------------
    // setInterval that saves the ID
    // -----------------------------
    function setIntervalCancellable(func, delay) {
        let new_id = setInterval(func, delay);
        sessionStorage.setItem(_interval_storage_key, new_id.toString());
    }

    // -----------------------------
    // Time formatting helper
    // -----------------------------
    function formatTime(milliseconds) {
        // toISOString() -> "1970-01-01T00:00:10.000Z"
        return new Date(milliseconds)
            .toISOString()
            .substring(11, 19)
            .replace(/00:0|00:/, "");
    }

    // -----------------------------
    // The main countdown
    // -----------------------------
    function countdown(elementName, seconds) {
        let element = document.getElementById(elementName);
        let endTime = +new Date() + 1000 * seconds;
        let alert_shown = false;

        // Ensure no leftover timers/intervals from previous cards
        cancelTimersAndIntervals();

        function showAlerts() {
            // Show alert box
            document.getElementById("show-alert").style.display = "block";
            // Insert audio HTML
            document.getElementById("alert-audio").innerHTML = audio_str;

            // Hide alert after 1 second
            setTimeout(function() {
                document.getElementById("show-alert").style.display = "none";
            }, 1000);
        }

        function updateTimer() {
            let msLeft = endTime - +new Date();

            if (msLeft > 0) {
                if (msLeft < warn_ms) {
                    if (!alert_shown) {
                        showAlerts();
                        alert_shown = true;
                    }
                }
                element.innerHTML = formatTime(msLeft);
                setTimeoutCancellable(updateTimer, 100);
            } else {
                element.innerHTML = "<span style='color:#CC5B5B'>Time is up!</span>";

                // After time is up, wait 10s
                setTimeoutCancellable(function() {
                    // Then beep every 1s
                    setIntervalCancellable(function() {
                        document.getElementById("alert-audio").innerHTML = audio_str;
                    }, beepIntervalMs);
                }, timeUpDelay * 1000);
            }
        }

        updateTimer();
    }

    // Start the countdown
    countdown("s1", 60 * time_min + time_sec);
    document.getElementById("seconds").innerHTML = warn_sec;
</script>

BACK:

{{Any-Field}}

STYLES: (can be empty)

Thank you :pray:

[0] Forum won’t let me add inline links, but for reference, see gist.github . com /Foxy-null/7fb59c6f2e913633ea92bcfc5effe22d