mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 07:22:23 -04:00
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:
parent
d482e90c6b
commit
d1ba48bc48
3 changed files with 74 additions and 23 deletions
|
@ -2,6 +2,8 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
import html
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from anki.consts import MODEL_CLOZE
|
from anki.consts import MODEL_CLOZE
|
||||||
|
@ -10,6 +12,12 @@ from anki.utils import is_win, strip_html
|
||||||
from tests.shared import getEmptyCol
|
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():
|
def test_modelDelete():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
note = col.newNote()
|
note = col.newNote()
|
||||||
|
@ -176,29 +184,41 @@ def test_cloze():
|
||||||
note = col.new_note(m)
|
note = col.new_note(m)
|
||||||
note["Text"] = "hello {{c1::world}}"
|
note["Text"] = "hello {{c1::world}}"
|
||||||
assert col.addNote(note) == 1
|
assert col.addNote(note) == 1
|
||||||
assert "hello <span class=cloze>[...]</span>" in note.cards()[0].question()
|
assert (
|
||||||
assert "hello <span class=cloze>world</span>" in note.cards()[0].answer()
|
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
|
# and with a comment
|
||||||
note = col.new_note(m)
|
note = col.new_note(m)
|
||||||
note["Text"] = "hello {{c1::world::typical}}"
|
note["Text"] = "hello {{c1::world::typical}}"
|
||||||
assert col.addNote(note) == 1
|
assert col.addNote(note) == 1
|
||||||
assert "<span class=cloze>[typical]</span>" in note.cards()[0].question()
|
assert (
|
||||||
assert "<span class=cloze>world</span>" in note.cards()[0].answer()
|
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
|
# and with 2 clozes
|
||||||
note = col.new_note(m)
|
note = col.new_note(m)
|
||||||
note["Text"] = "hello {{c1::world}} {{c2::bar}}"
|
note["Text"] = "hello {{c1::world}} {{c2::bar}}"
|
||||||
assert col.addNote(note) == 2
|
assert col.addNote(note) == 2
|
||||||
(c1, c2) = note.cards()
|
(c1, c2) = note.cards()
|
||||||
assert "<span class=cloze>[...]</span> bar" in c1.question()
|
assert (
|
||||||
assert "<span class=cloze>world</span> bar" in c1.answer()
|
f'<span class="cloze" data-cloze="{encode_attribute("world")}">[...]</span> bar'
|
||||||
assert "world <span class=cloze>[...]</span>" in c2.question()
|
in c1.question()
|
||||||
assert "world <span class=cloze>bar</span>" in c2.answer()
|
)
|
||||||
|
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
|
# if there are multiple answers for a single cloze, they are given in a
|
||||||
# list
|
# list
|
||||||
note = col.new_note(m)
|
note = col.new_note(m)
|
||||||
note["Text"] = "a {{c1::b}} {{c1::c}}"
|
note["Text"] = "a {{c1::b}} {{c1::c}}"
|
||||||
assert col.addNote(note) == 1
|
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()
|
note.cards()[0].answer()
|
||||||
)
|
)
|
||||||
# if we add another cloze, a card should be generated
|
# if we add another cloze, a card should be generated
|
||||||
|
@ -216,16 +236,42 @@ def test_cloze_mathjax():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
m = col.models.by_name("Cloze")
|
m = col.models.by_name("Cloze")
|
||||||
note = col.new_note(m)
|
note = col.new_note(m)
|
||||||
|
q1 = "ok"
|
||||||
|
q2 = "not ok"
|
||||||
|
q3 = "2"
|
||||||
|
q4 = "blah"
|
||||||
|
q5 = "text with \(x^2\) jax"
|
||||||
note[
|
note[
|
||||||
"Text"
|
"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 col.addNote(note)
|
||||||
assert len(note.cards()) == 5
|
assert len(note.cards()) == 5
|
||||||
assert "class=cloze" in note.cards()[0].question()
|
assert (
|
||||||
assert "class=cloze" in note.cards()[1].question()
|
f'class="cloze" data-cloze="{encode_attribute(q1)}"'
|
||||||
assert "class=cloze" not in note.cards()[2].question()
|
in note.cards()[0].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(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 = col.new_note(m)
|
||||||
note["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
|
note["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
|
||||||
|
@ -234,7 +280,7 @@ def test_cloze_mathjax():
|
||||||
assert (
|
assert (
|
||||||
note.cards()[0]
|
note.cards()[0]
|
||||||
.question()
|
.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 col.addNote(note) == 1
|
||||||
assert (
|
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()
|
in note.cards()[0].question()
|
||||||
)
|
)
|
||||||
assert (
|
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()
|
in note.cards()[0].answer()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -57,26 +57,30 @@ pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str>
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let text = caps.get(cloze_caps::TEXT).unwrap().as_str().to_owned();
|
||||||
if captured_ord != cloze_ord {
|
if captured_ord != cloze_ord {
|
||||||
// other cloze deletions are unchanged
|
// other cloze deletions are unchanged
|
||||||
return caps.get(cloze_caps::TEXT).unwrap().as_str().to_owned();
|
return text;
|
||||||
} else {
|
} else {
|
||||||
cloze_ord_was_in_text = true;
|
cloze_ord_was_in_text = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let text_attr;
|
||||||
let replacement;
|
let replacement;
|
||||||
if question {
|
if question {
|
||||||
|
text_attr = format!(r#" data-cloze="{}""#, htmlescape::encode_attribute(&text));
|
||||||
// hint provided?
|
// hint provided?
|
||||||
if let Some(hint) = caps.get(cloze_caps::HINT) {
|
if let Some(hint) = caps.get(cloze_caps::HINT) {
|
||||||
replacement = format!("[{}]", hint.as_str());
|
replacement = format!("[{}]", hint.as_str());
|
||||||
} else {
|
} else {
|
||||||
replacement = "[...]".to_string()
|
replacement = "[...]".to_string();
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if !cloze_ord_was_in_text {
|
||||||
|
|
|
@ -256,7 +256,7 @@ field</a>
|
||||||
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "[...] two");
|
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "[...] two");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cloze_filter(text, &ctx),
|
cloze_filter(text, &ctx),
|
||||||
"<span class=cloze>[...]</span> two"
|
r#"<span class="cloze" data-cloze="one">[...]</span> two"#
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.card_ord = 1;
|
ctx.card_ord = 1;
|
||||||
|
|
Loading…
Reference in a new issue