Basic (type in the answer)” strips text inside < > when answer is correct

Hey everyone,

I’ve run into an interesting issue with the Basic (type in the answer) card type. The problem occurs whenever the answer contains text that looks like an HTML tag.

Example:

Front:

Syntax of a Python list comprehension.

Back (typed answer field):

[<expr> for <member> in <iterable> if <condition>]

If I type an incorrect answer, everything displays fine.

But when I type the correct answer, any text inside < and > gets stripped out!

Here’s what I’ve checked so far:

The card’s HTML source shows that < and > are inserted correctly using html entity codes.

The issue only appears when Anki renders the correct answer during the type-answer comparison.

For now, I’ve added a note in my template to remind me about the issue and display the “Correct Answer” section manually.

Would appreciate any insights or fixes for this behavior! Thank you.

It’s hard to avoid the fact that type-answer wants to strip out all formatting – but this is a unique twist. It’s doing an accurate comparison on a wrong answer, and only has a problem when displaying the correct answer you typed.

I’m able to reproduce it [on Windows, 25.02.x] using your text, but I’m not sure if that helps here or not. :sweat_smile: Let’s see if a dev can figure out what would need to change in the code.

In a correct answer [left], the screen displays the whole answer as class typeGood, and it “helpfully” resolves anything that looks like custom HTML tags, by manufacturing closing tags for them. In an incorrect answer [right], the screen alternates between typeGood and typeMissed, which seems to be enough to get it to leave the bracketed text alone.


From a card template design perspective –

Perhaps you don’t need all of that warning language on the card. It seems like simply displaying the correct answer – with {{Back}} on your back template – is enough.

I’m also going to presumptuously ping @Eltaurus and @Anon_0000 , since I know they have experience outsmarting this particular note type, and they might have advice that would help here.

3 Likes

Thanks for the ping! :grinning_face:

This looks like a string escaping bug to me. Not sure if it was made this way intentionally to avoid some other issue (I’d like to take a look at the Anki reviewer’s source code a bit later), but it might be something like a misplaced innerHTML where innerText was supposed to be used instead.


For a workaround, the following script can be added to the backside of the card to restore the innerText for good answers:

<div id="corrAns" style="display:none">{{Front}}</div>

<script>
goodAns = document.querySelector("#typeans > span.typeGood:only-child");
if (goodAns) {
	goodAns.innerText = document.getElementById('corrAns').innerText;
}
</script>
4 Likes

Thanks so much for taking the time to dig into this, I really appreciate it! :folded_hands:

The solution suggested by @Eltaurus works really well for my use case, it preserves the correct answer display without stripping the HTML-like text. It’s much more usable now.

2 Likes

Thanks so much, @Eltaurus ! The solution works like a charm, my “type in the answer” cards now display the correct text properly. :raising_hands:

I was wondering if you might have any ideas for implementing the same approach with type-in cloze cards. The issue is that there’s no “{{Front}}" I can use to overwrite the stripped-out text when the cloze is revealed. In your script, replacing “{{Front}}" with “{{cloze:Text}}” overwrites it with the entire text in the cloze!

I’ve tried accessing the data-cloze attribute from the .cloze span which contains the hidden text, and feeding it into a variable (using localStorge) to restore the stripped text, but that’s beyond my current JS skills and my understanding of Anki’s inner workings.

No worries though, for now, a note in the template explaining the behavior works fine for me. Really appreciate your fix, it’s already a huge help!

2 Likes

Sure, I think this should do the trick:

<div id="corrAns" style="display:none">{{cloze:Text}}</div>

<script>
goodAns = document.querySelector("#typeans > span.typeGood:only-child");
if (goodAns) {
	goodAns.innerText = [...document.getElementById('corrAns').querySelectorAll('.cloze')].map(L=>L.innerText).join(", ");
}
</script>
3 Likes

@Eltaurus Thanks again for taking the time to respond! Your script didn’t quite work as-is, but it sent me down a rabbit hole that eventually led to a partial solution. :blush:

Here’s my current setup (for context, I’m using Obsidian_to_Anki to generate cards — mostly for code formatting):

Front Template:

{{cloze:Text}}
<br><br>
{{type:cloze:Text}}

<script>
(function() {
  // Get the first (active) cloze on the front and store its data-cloze value
  const activeCloze = document.querySelector('.cloze[data-cloze]');
  if (activeCloze) {
    const rawClozeData = activeCloze.getAttribute('data-cloze');

    // Convert HTML entities properly
    const tempDiv = document.createElement('div');
    tempDiv.innerHTML = rawClozeData;
    let clozeData = tempDiv.innerText.trim();

    // Escape any special characters so <dir> etc. are shown safely
    clozeData = clozeData
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#039;");

    localStorage.setItem('clozeData', clozeData);
  } else {
    localStorage.removeItem('clozeData'); // clear in case none
  }
})();
</script>

Back Template:

{{cloze:Text}}
<br><br>
{{type:cloze:Text}}

<hr>
<br>
{{Back Extra}}

<hr>
{{Obsidian File Link}}
<br>
{{Obsidian Context Field}}

<script>
(function() {
  const goodAns = document.querySelector("#typeans > span.typeGood:only-child");
  if (goodAns) {
    const tempDiv = document.createElement('div');
    tempDiv.innerHTML = localStorage.getItem('clozeData') || '';
    goodAns.innerText = tempDiv.innerText;
    localStorage.removeItem('clozeData'); // cleanup after use
  }
})();
</script>

The tempDiv bit is mostly from ChatGPT’s help, using it prevents the string from being double-escaped (where HTML entity codes like &lt; wouldn’t render properly).

It’s mostly working now, though I’m still seeing “ghost” tags appear in the rendered output, not sure where those are coming from yet.

Could you please clarify in what ways it wasn’t working as expected?

It looks like it’s a leftover from the original {{type:cloze:Text}}. The Anki input might be broken a bit more than originally expected in such cases :sweat_smile:.
It seems to be caused specifically by the <dir> part, likely because it is recognized as a standard HTML block tag, so it gets moved outside of the #typeans element when being autoclosed.

For this reason, simply replacing the contents of the #typeans does no longer suffice, but we can try removing all the redundant children of its parent and hope nothing can leak out to even higher DOM levels:

    const typeAns = document.getElementById('typeans');
    const parent = typeAns.parentNode;
    [...parent.children].forEach(L=>{
        if (L !== typeAns) {
            parent.removeChild(L);
        }
    });

(This is supposed to go at the end of the if (goodAns)'s body.)

1 Like

Not sure where better to write about it and who better ping, but this indeed seems like a bug in typeans.rs introduced in dc5fa60.

Before that, both correct and incorrect answers were displaying strings output by render_tokens (which in turn passed each token through htmlescape::encode_minimal), while after that, the string for a correct answer got manually inserted into <span class=typeGood>{}</span> (same as the expected result for rendering a single correct token, but without any escaping).
An empty answer (which, practically, displays the same thing as a correct answer only without the typeGood highlighting) still uses htmlescape::encode_minimal (added in #2658 to fix a similar issue), so I doubt for correct answers it was dropped intentionally.

If adding the escaping back (by explicitly calling htmlescape::encode_minimal in the respective if branch inside to_html) is an acceptable change, I’d like to make a PR with the fix and appropriate test functions to catch such regressions in the future.

3 Likes

Could you please clarify in what ways it wasn’t working as expected?

From my understanding, the <span class="cloze" data-cloze="..."> element (which contains the hidden/clozed text) only exists while the Front template is being rendered, that is, when the question is shown. At this stage, #typeans doesn’t exist yet.

Once the answer is revealed and the Back template is rendered, the data-cloze attribute is no longer present. That’s also when .typeGood becomes available. The script you gave me expected both of these to be available at the same time.

So my idea was to capture the data-cloze value while still on the Front side and persist it (via localStorage) so I could access it again on the Back side.

I’ve since realized that the same cloze data is actually still accessible on the Back, just under the .cloze class. I’d originally avoided that route because I was worried it might conflict with multiple cloze deletions on the same card, but I see they instead have .cloze-inactive. But I really enjoyed working through this, so I’ll keep the current solution at least until the underlying bug gets fixed.

And that cleanup script you suggested worked perfectly, thank you again for your time and help! :folded_hands:

Wow, I wouldn’t have guessed it went that deep into the codebase. Really appreciate you taking the time to dig into it. The workaround’s holding up fine for now, but I’d be happy to help test the fix once it’s available. I’m still new around here, so not entirely sure what I can do on my end, but I’ll keep an eye out for updates.

Big thanks to @Eltaurus and @Danika_Dakika ,really appreciate the effort and explanation! :folded_hands: I’m studying to become a software engineer, and honestly, tinkering with Anki might just be my muse after all :sweat_smile:

1 Like

It seems more like the correct answer case was just overlooked.

If making the change doesn’t consume too much of your time, you may want to open a PR and follow up there.

2 Likes

Not exactly. #typeans exists on both sides: on the front it is the input line where you type your answer into; on the back, it gets converted into the <code> element that shows the comparison between what you typed and what you was expected to type. The original script, however, was supposed to be placed on the back (just like for non-cloze typing cards), so it’s just a sidenote here.

Actually, no. The script didn’t rely on the data-close attribute, it was simply using inner text from elements with .cloze class, which is what the parts of {{cloze:Text}} related to the active cloze are marked as, regardless of the side (as you’ve noticed).

That’s correct. Also, multi-part and nested clozes should be handled this way as well, as they are all gathered together using querySelectorAll and automatically stripped of potential inner cloze formatting when converted to innerText.

On another sidenote, while you don’t really need it here, for similar things in the future, you might prefer using sessionStorage instead, which is just a bit better suited for such temporary data.

2 Likes

PR:

2 Likes

@Eltaurus Noted with thanks, really appreciate you taking the time to explain everything.

2 Likes