Expose cloze text as HTML attribute on question side (#1968)

* Expose cloze text as attribute on front side

* Update test_models.py

* Update template_filters.rs

* Escape HTML for data-attribute

* Use minimal HTML encoding in Rust

to match Python's html.escape and pass tests.

* Rename attribute to data-cloze

to make it more generic.

* Run formatter

* Revert to using Rust encode_attribute and add helper function for tests
This commit is contained in:
Matthias Metelka 2022-07-18 20:22:57 +02:00 committed by GitHub
parent d482e90c6b
commit d1ba48bc48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 23 deletions

View file

@ -2,6 +2,8 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# coding: utf-8
import html
import re
import time
from anki.consts import MODEL_CLOZE
@ -10,6 +12,12 @@ from anki.utils import is_win, strip_html
from tests.shared import getEmptyCol
def encode_attribute(s):
return "".join(
c if c.isalnum() else "&#x{:X};".format(ord(c)) for c in html.escape(s)
)
def test_modelDelete():
col = getEmptyCol()
note = col.newNote()
@ -176,29 +184,41 @@ def test_cloze():
note = col.new_note(m)
note["Text"] = "hello {{c1::world}}"
assert col.addNote(note) == 1
assert "hello <span class=cloze>[...]</span>" in note.cards()[0].question()
assert "hello <span class=cloze>world</span>" in note.cards()[0].answer()
assert (
f'hello <span class="cloze" data-cloze="{encode_attribute("world")}">[...]</span>'
in note.cards()[0].question()
)
assert 'hello <span class="cloze">world</span>' in note.cards()[0].answer()
# and with a comment
note = col.new_note(m)
note["Text"] = "hello {{c1::world::typical}}"
assert col.addNote(note) == 1
assert "<span class=cloze>[typical]</span>" in note.cards()[0].question()
assert "<span class=cloze>world</span>" in note.cards()[0].answer()
assert (
f'<span class="cloze" data-cloze="{encode_attribute("world")}">[typical]</span>'
in note.cards()[0].question()
)
assert '<span class="cloze">world</span>' in note.cards()[0].answer()
# and with 2 clozes
note = col.new_note(m)
note["Text"] = "hello {{c1::world}} {{c2::bar}}"
assert col.addNote(note) == 2
(c1, c2) = note.cards()
assert "<span class=cloze>[...]</span> bar" in c1.question()
assert "<span class=cloze>world</span> bar" in c1.answer()
assert "world <span class=cloze>[...]</span>" in c2.question()
assert "world <span class=cloze>bar</span>" in c2.answer()
assert (
f'<span class="cloze" data-cloze="{encode_attribute("world")}">[...]</span> bar'
in c1.question()
)
assert '<span class="cloze">world</span> bar' in c1.answer()
assert (
f'world <span class="cloze" data-cloze="{encode_attribute("bar")}">[...]</span>'
in c2.question()
)
assert 'world <span class="cloze">bar</span>' in c2.answer()
# if there are multiple answers for a single cloze, they are given in a
# list
note = col.new_note(m)
note["Text"] = "a {{c1::b}} {{c1::c}}"
assert col.addNote(note) == 1
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (
assert '<span class="cloze">b</span> <span class="cloze">c</span>' in (
note.cards()[0].answer()
)
# if we add another cloze, a card should be generated
@ -216,16 +236,42 @@ def test_cloze_mathjax():
col = getEmptyCol()
m = col.models.by_name("Cloze")
note = col.new_note(m)
q1 = "ok"
q2 = "not ok"
q3 = "2"
q4 = "blah"
q5 = "text with \(x^2\) jax"
note[
"Text"
] = r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}"
] = "{{{{c1::{}}}}} \(2^2\) {{{{c2::{}}}}} \(2^{{{{c3::{}}}}}\) \(x^3\) {{{{c4::{}}}}} {{{{c5::{}}}}}".format(
q1,
q2,
q3,
q4,
q5,
)
assert col.addNote(note)
assert len(note.cards()) == 5
assert "class=cloze" in note.cards()[0].question()
assert "class=cloze" in note.cards()[1].question()
assert "class=cloze" not in note.cards()[2].question()
assert "class=cloze" in note.cards()[3].question()
assert "class=cloze" in note.cards()[4].question()
assert (
f'class="cloze" data-cloze="{encode_attribute(q1)}"'
in note.cards()[0].question()
)
assert (
f'class="cloze" data-cloze="{encode_attribute(q2)}"'
in note.cards()[1].question()
)
assert (
f'class="cloze" data-cloze="{encode_attribute(q3)}"'
not in note.cards()[2].question()
)
assert (
f'class="cloze" data-cloze="{encode_attribute(q4)}"'
in note.cards()[3].question()
)
assert (
f'class="cloze" data-cloze="{encode_attribute(q5)}"'
in note.cards()[4].question()
)
note = col.new_note(m)
note["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
@ -234,7 +280,7 @@ def test_cloze_mathjax():
assert (
note.cards()[0]
.question()
.endswith(r"\(a\) <span class=cloze>[...]</span> \[ [...] \]")
.endswith(r'\(a\) <span class="cloze" data-cloze="b">[...]</span> \[ [...] \]')
)
@ -278,11 +324,12 @@ def test_chained_mods():
)
assert col.addNote(note) == 1
assert (
"This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes."
f'This <span class="cloze" data-cloze="{encode_attribute("phrase")}">[sentence]</span>'
f' demonstrates <span class="cloze" data-cloze="{encode_attribute("en chaine")}">[chained]</span> clozes.'
in note.cards()[0].question()
)
assert (
"This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes."
f'This <span class="cloze">phrase</span> demonstrates <span class="cloze">en chaine</span> clozes.'
in note.cards()[0].answer()
)

View file

@ -57,26 +57,30 @@ pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str>
.parse()
.unwrap_or(0);
let text = caps.get(cloze_caps::TEXT).unwrap().as_str().to_owned();
if captured_ord != cloze_ord {
// other cloze deletions are unchanged
return caps.get(cloze_caps::TEXT).unwrap().as_str().to_owned();
return text;
} else {
cloze_ord_was_in_text = true;
}
let text_attr;
let replacement;
if question {
text_attr = format!(r#" data-cloze="{}""#, htmlescape::encode_attribute(&text));
// hint provided?
if let Some(hint) = caps.get(cloze_caps::HINT) {
replacement = format!("[{}]", hint.as_str());
} else {
replacement = "[...]".to_string()
replacement = "[...]".to_string();
}
} else {
replacement = caps.get(cloze_caps::TEXT).unwrap().as_str().to_owned();
text_attr = "".to_string();
replacement = text;
}
format!("<span class=cloze>{}</span>", replacement)
format!(r#"<span class="cloze"{}>{}</span>"#, text_attr, replacement)
});
if !cloze_ord_was_in_text {

View file

@ -256,7 +256,7 @@ field</a>
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "[...] two");
assert_eq!(
cloze_filter(text, &ctx),
"<span class=cloze>[...]</span> two"
r#"<span class="cloze" data-cloze="one">[...]</span> two"#
);
ctx.card_ord = 1;