Skip to content

Commit 7bf0e0b

Browse files
committed
add new image_cloze note types
* there are four fields - Occlusion -> for cloze - Image - Header - Back Extra * the implementation for shape generation to canvas added reviewer ts
1 parent 6581d04 commit 7bf0e0b

File tree

9 files changed

+168
-67
lines changed

9 files changed

+168
-67
lines changed

ftl/core/notetypes.ftl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ notetypes-note-types = Note Types
3535
notetypes-options = Options
3636
notetypes-please-add-another-note-type-first = Please add another note type first.
3737
notetypes-type = Type
38+
notetypes-image = Image
39+
notetypes-occlusion = Occlusion
40+
notetypes-image-occlusion-name = Image Occlusion

proto/anki/notetypes.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ message StockNotetype {
123123
BASIC_OPTIONAL_REVERSED = 2;
124124
BASIC_TYPING = 3;
125125
CLOZE = 4;
126+
IMAGE_CLOZE = 5;
126127
}
127128

128129
Kind kind = 1;

rslib/src/cloze.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,12 @@ impl ExtractedCloze<'_> {
140140
buf.into()
141141
}
142142

143-
fn image_occlusion(&self) -> Cow<str> {
144-
get_image_cloze_data(&self.clozed_text()).into()
145-
}
143+
/// If cloze starts with image-occlusion:, return the text following that.
144+
fn image_occlusion(&self) -> Option<&str> {
145+
let Some(first_node) = self.nodes.get(0) else { return None };
146+
let TextOrCloze::Text(text) = first_node else { return None };
147+
text.strip_prefix("image-occlusion:")
148+
}
146149
}
147150

148151
fn parse_text_with_clozes(text: &str) -> Vec<TextOrCloze<'_>> {
@@ -217,6 +220,14 @@ fn reveal_cloze(
217220
) {
218221
let active = cloze.ordinal == cloze_ord;
219222
*active_cloze_found_in_text |= active;
223+
if let Some(image_occlusion_text) = cloze.image_occlusion() {
224+
buf.push_str(&render_image_occlusion(
225+
image_occlusion_text,
226+
question,
227+
active,
228+
));
229+
return;
230+
}
220231
match (question, active) {
221232
(true, true) => {
222233
// question side with active cloze; all inner content is elided
@@ -235,9 +246,8 @@ fn reveal_cloze(
235246
}
236247
write!(
237248
buf,
238-
r#"<span class="cloze" data-cloze="{}" {} data-ordinal="{}">[{}]</span>"#,
249+
r#"<span class="cloze" data-cloze="{}" data-ordinal="{}">[{}]</span>"#,
239250
encode_attribute(&content_buf),
240-
cloze.image_occlusion(),
241251
cloze.ordinal,
242252
cloze.hint()
243253
)
@@ -246,8 +256,7 @@ fn reveal_cloze(
246256
(false, true) => {
247257
write!(
248258
buf,
249-
r#"<span class="cloze" {} data-ordinal="{}">"#,
250-
cloze.image_occlusion(),
259+
r#"<span class="cloze" data-ordinal="{}">"#,
251260
cloze.ordinal
252261
)
253262
.unwrap();
@@ -265,8 +274,7 @@ fn reveal_cloze(
265274
// question or answer side inactive cloze; text shown, children may be active
266275
write!(
267276
buf,
268-
r#"<span class="cloze-inactive" {} data-ordinal="{}">"#,
269-
cloze.image_occlusion(),
277+
r#"<span class="cloze-inactive" data-ordinal="{}">"#,
270278
cloze.ordinal
271279
)
272280
.unwrap();
@@ -283,6 +291,22 @@ fn reveal_cloze(
283291
}
284292
}
285293

294+
fn render_image_occlusion(text: &str, question_side: bool, active: bool) -> String {
295+
if question_side && active {
296+
format!(
297+
r#"<div class="cloze" {}></div>"#,
298+
&get_image_cloze_data(text)
299+
)
300+
} else if (question_side && !active) || (!question_side && !active) {
301+
format!(
302+
r#"<div class="cloze-inactive" {}></div>"#,
303+
&get_image_cloze_data(text)
304+
)
305+
} else {
306+
"".into()
307+
}
308+
}
309+
286310
pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str> {
287311
let mut buf = String::new();
288312
let mut active_cloze_found_in_text = false;

rslib/src/image_occlusion/imageocclusion.rs

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,6 @@
11
// Copyright: Ankitects Pty Ltd and contributors
22
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
33

4-
use lazy_static::lazy_static;
5-
use regex::Regex;
6-
7-
lazy_static! {
8-
pub(crate) static ref IMAGE_OCCLUSION_REGEX: Regex = Regex::new(
9-
r#"(?xsi)
10-
::image-occlusion:
11-
"#
12-
)
13-
.unwrap();
14-
}
15-
16-
pub(crate) fn contains_image_occlusion(text: &str) -> bool {
17-
IMAGE_OCCLUSION_REGEX.is_match(text)
18-
}
19-
204
// split following
215
// text = "rect:399.01,99.52,167.09,33.78:fill=#0a2cee:stroke=1"
226
// with
@@ -33,12 +17,12 @@ pub fn get_image_cloze_data(text: &str) -> String {
3317
let mut rx = "";
3418
let mut ry = "";
3519
let mut points = "";
20+
let mut quesmaskcolor = "";
3621

3722
let parts: Vec<&str> = text.split(':').collect();
3823

3924
if parts.len() >= 2 {
40-
// parts[0] is image-occlusion, 1 is shape
41-
shape = parts[1];
25+
shape = parts[0];
4226
for part in parts[1..].iter() {
4327
let values: Vec<&str> = part.split('=').collect();
4428
if values.len() >= 2 {
@@ -49,10 +33,10 @@ pub fn get_image_cloze_data(text: &str) -> String {
4933
"height" => height = values[1],
5034
"fill" => fill = values[1],
5135
"stroke" => stroke = values[1],
52-
5336
"rx" => rx = values[1],
5437
"ry" => ry = values[1],
5538
"points" => points = values[1],
39+
"quesmaskcolor" => quesmaskcolor = values[1],
5640
_ => {}
5741
}
5842
}
@@ -117,32 +101,36 @@ pub fn get_image_cloze_data(text: &str) -> String {
117101
result.push_str(&format!("data-stroke=\"{}\" ", stroke));
118102
}
119103

104+
if !quesmaskcolor.is_empty() {
105+
result.push_str(&format!("data-quesmaskcolor=\"{}\" ", quesmaskcolor));
106+
}
107+
120108
result
121109
}
122110

123111
//----------------------------------------
124112
// Tests
125113
//----------------------------------------
126114

127-
#[test]
128-
fn test_contains_image_occlusion() {
129-
assert!(contains_image_occlusion(
130-
"{{c5::image-occlusion:rect:left=10.0:top=20:width=30:height=10:fill=#ffe34d:stroke=5}}"
131-
));
132-
}
133-
134115
#[test]
135116
fn test_get_image_cloze_data() {
136117
assert_eq!(
137-
get_image_cloze_data("image-occlusion:rect:left=10:top=20:width=30:height=10:fill=#ffe34d:stroke=5"),
138-
format!(r#"data-shape="rect" data-left="10" data-top="20" data-width="30" data-height="10" data-fill="{}" data-stroke="5" "#, "#ffe34d")
139-
);
118+
get_image_cloze_data(
119+
"rect:left=10:top=20:width=30:height=10:fill=#ffe34d:stroke=5:quesmaskcolor=#ff0000"
120+
),
121+
format!(
122+
r#"data-shape="rect" data-left="10" data-top="20" data-width="30" data-height="10" data-fill="{}" data-stroke="5" data-quesmaskcolor="{}" "#,
123+
"#ffe34d", "#ff0000"
124+
)
125+
);
140126
assert_eq!(
141-
get_image_cloze_data("image-occlusion:ellipse:left=15:top=20:width=10:height=20:rx=10:ry=5:fill=red:stroke=5"),
127+
get_image_cloze_data(
128+
"ellipse:left=15:top=20:width=10:height=20:rx=10:ry=5:fill=red:stroke=5"
129+
),
142130
r#"data-shape="ellipse" data-rx="10" data-ry="5" data-left="15" data-top="20" data-width="10" data-height="20" data-fill="red" data-stroke="5" "#
143-
);
131+
);
144132
assert_eq!(
145-
get_image_cloze_data("image-occlusion:polygon:points=0,0 10,10 20,0:fill=blue:stroke=5"),
133+
get_image_cloze_data("polygon:points=0,0 10,10 20,0:fill=blue:stroke=5"),
146134
r#"data-shape="polygon" data-points="[[0,0],[10,10],[20,0]]" data-fill="blue" data-stroke="5" "#
147-
);
135+
);
148136
}

rslib/src/notetype/stock.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub fn all_stock_notetypes(tr: &I18n) -> Vec<Notetype> {
3737
basic_optional_reverse(tr),
3838
basic_typing(tr),
3939
cloze(tr),
40+
image_cloze(tr),
4041
]
4142
}
4243

@@ -123,3 +124,29 @@ pub(crate) fn cloze(tr: &I18n) -> Notetype {
123124
nt.add_template(nt.name.clone(), qfmt, afmt);
124125
nt
125126
}
127+
128+
pub(crate) fn image_cloze(tr: &I18n) -> Notetype {
129+
let mut nt = Notetype {
130+
name: tr.notetypes_image_occlusion_name().into(),
131+
config: NotetypeConfig::new_cloze(),
132+
..Default::default()
133+
};
134+
let occlusion = tr.notetypes_occlusion();
135+
nt.add_field(occlusion.as_ref());
136+
let image = tr.notetypes_image();
137+
nt.add_field(image.as_ref());
138+
let header = tr.notetypes_header();
139+
nt.add_field(header.as_ref());
140+
let back_extra = tr.notetypes_back_extra_field();
141+
nt.add_field(back_extra.as_ref());
142+
143+
let qfmt = format!(
144+
"{{{{cloze:{}}}}}\n{{{{{}}}}}\n<script> anki.setupImageCloze() </script>",
145+
occlusion, image);
146+
let afmt = format!(
147+
"{{{{{}}}}}\n{}\n{{{{{}}}}}\n<script> anki.setupImageCloze() </script>",
148+
header, qfmt, back_extra
149+
);
150+
nt.add_template(nt.name.clone(), qfmt, afmt);
151+
nt
152+
}

rslib/src/template_filters.rs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use regex::Regex;
1010

1111
use crate::cloze::cloze_filter;
1212
use crate::cloze::cloze_only_filter;
13-
use crate::image_occlusion::imageocclusion::IMAGE_OCCLUSION_REGEX;
1413
use crate::template::RenderContext;
1514
use crate::text::strip_html;
1615

@@ -80,7 +79,6 @@ fn apply_filter<'a>(
8079
"hint" => hint_filter(text, field_name),
8180
"cloze" => cloze_filter(text, context),
8281
"cloze-only" => cloze_only_filter(text, context),
83-
"image-occlusion" => image_occlusion_filter(text),
8482
// an empty filter name (caused by using two colons) is ignored
8583
"" => text.into(),
8684
_ => {
@@ -198,12 +196,6 @@ fn tts_filter(options: &str, text: &str) -> String {
198196
format!("[anki:tts lang={}]{}[/anki:tts]", options, text)
199197
}
200198

201-
fn image_occlusion_filter(text: &str) -> Cow<str> {
202-
let mut text = text.to_string();
203-
text = text.replace("image-occlusion", "");
204-
text.into()
205-
}
206-
207199
// Tests
208200
//----------------------------------------
209201

@@ -288,21 +280,4 @@ field</a>
288280
"[anki:tts lang=en_US voices=Bob,Jane]foo[/anki:tts]"
289281
);
290282
}
291-
292-
#[test]
293-
fn test_image_occlusion_filter() {
294-
let ctx = RenderContext {
295-
fields: &Default::default(),
296-
nonempty_fields: &Default::default(),
297-
question_side: true,
298-
card_ord: 0,
299-
};
300-
let text = r#"{{c1::image-occlusion:rect:left=10.0:top=20:width=30:height=10:fill=#ffe34d:stroke=5}}
301-
{{c2::image-occlusion:ellipse:left=15:top=20:width=10:height=20:rx=10:ry=5:fill=red:stroke=5"#;
302-
assert_eq!(
303-
cloze_filter(text, &ctx),
304-
r#"<span class="cloze" data-cloze="image-occlusion" data-shape="rect" data-left="10.0" data-top="20" data-width="30" data-height="10" data-fill="\#ffe34d" data-stroke="5" data-ordinal="1">[...]</span>
305-
<span class="cloze-inactive" data-shape="ellipse" data-left="15" data-top="20" data-width="10" data-height="20" data-rx="10" data-ry="5" data-fill="red" data-stroke="5" data-ordinal="2">two</span>"#
306-
);
307-
}
308283
}

ts/reviewer/image_occlusion.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright: Ankitects Pty Ltd and contributors
2+
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3+
4+
export function setupImageCloze() {
5+
const canvas: HTMLCanvasElement = document.createElement("CANVAS") as HTMLCanvasElement;
6+
canvas.id = "image-occlusion-canvas";
7+
canvas.style.backgroundSize = "100% 100%";
8+
canvas.style.maxWidth = "100%";
9+
canvas.style.maxHeight = "90vh";
10+
11+
const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
12+
const imageFieldElem = document.getElementById("img") as HTMLImageElement;
13+
imageFieldElem.style.display = "none";
14+
15+
const img: HTMLImageElement = new Image();
16+
img.src = imageFieldElem.src;
17+
18+
img.onload = () => {
19+
canvas.width = img.width;
20+
canvas.height = img.height;
21+
ctx.drawImage(img, 0, 0, img.width, img.height);
22+
drawShapes(ctx);
23+
};
24+
25+
document.getElementById("qa")!.appendChild(canvas);
26+
}
27+
28+
function drawShapes(ctx: CanvasRenderingContext2D) {
29+
const activeCloze = document.querySelectorAll(".cloze");
30+
const inActiveCloze = document.querySelectorAll(".cloze-inactive");
31+
32+
for (let clz of activeCloze) {
33+
let cloze = (<HTMLDivElement>clz);
34+
const shape = cloze.dataset.shape!;
35+
const fill = cloze.dataset.quesmaskcolor!;
36+
draw(ctx, cloze, shape, fill);
37+
}
38+
39+
for (let clz of inActiveCloze) {
40+
let cloze = (<HTMLDivElement>clz);
41+
const shape = cloze.dataset.shape!;
42+
const fill = cloze.dataset.fill!;
43+
draw(ctx, cloze, shape, fill);
44+
}
45+
}
46+
47+
function draw(ctx: CanvasRenderingContext2D, cloze: HTMLDivElement, shape: string, color: string) {
48+
ctx.fillStyle = color;
49+
50+
const post_left = parseFloat(cloze.dataset.left!);
51+
const pos_top = parseFloat(cloze.dataset.top!);
52+
const width = parseFloat(cloze.dataset.width!);
53+
const height = parseFloat(cloze.dataset.height!);
54+
55+
switch (shape) {
56+
case "rect":
57+
ctx.fillRect(post_left, pos_top, width, height);
58+
break;
59+
60+
case "ellipse":
61+
const rx = parseFloat(cloze.dataset.rx!);
62+
const ry = parseFloat(cloze.dataset.ry!);
63+
ctx.beginPath();
64+
ctx.ellipse(post_left, pos_top, rx, ry, 0, 0, Math.PI * 2, false);
65+
ctx.fill();
66+
break;
67+
68+
case "polygon":
69+
const points = JSON.parse(cloze.dataset.points!);
70+
ctx.beginPath();
71+
ctx.moveTo(points[0][0], points[0][1]);
72+
for (let i = 1; i < points.length; i++) {
73+
ctx.lineTo(points[i][0], points[i][1]);
74+
}
75+
ctx.closePath();
76+
ctx.fill();
77+
break;
78+
}
79+
}

ts/reviewer/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import "css-browser-selector/css_browser_selector.min";
1010
export { default as $, default as jQuery } from "jquery/dist/jquery";
1111

1212
import { mutateNextCardStates } from "./answering";
13+
import { setupImageCloze } from "./image_occlusion";
1314

1415
globalThis.anki = globalThis.anki || {};
1516
globalThis.anki.mutateNextCardStates = mutateNextCardStates;
17+
globalThis.anki.setupImageCloze = setupImageCloze;
1618

1719
import { bridgeCommand } from "@tslib/bridgecommand";
1820

ts/reviewer/reviewer_extras.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
// When all clients are using reviewer.js directly, we can get rid of this.
1010

1111
import { mutateNextCardStates } from "./answering";
12+
import { setupImageCloze } from "./image_occlusion";
1213

1314
globalThis.anki = globalThis.anki || {};
1415
globalThis.anki.mutateNextCardStates = mutateNextCardStates;
16+
globalThis.anki.setupImageCloze = setupImageCloze;

0 commit comments

Comments
 (0)