Skip to content

Commit

Permalink
add new image_cloze note types
Browse files Browse the repository at this point in the history
* there are four fields
    - Occlusion -> for cloze
    - Image
    - Header
    - Back Extra
* the implementation for shape generation to canvas added reviewer ts
  • Loading branch information
krmanik committed Feb 8, 2023
1 parent 6581d04 commit 7bf0e0b
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 67 deletions.
3 changes: 3 additions & 0 deletions ftl/core/notetypes.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ notetypes-note-types = Note Types
notetypes-options = Options
notetypes-please-add-another-note-type-first = Please add another note type first.
notetypes-type = Type
notetypes-image = Image
notetypes-occlusion = Occlusion
notetypes-image-occlusion-name = Image Occlusion
1 change: 1 addition & 0 deletions proto/anki/notetypes.proto
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ message StockNotetype {
BASIC_OPTIONAL_REVERSED = 2;
BASIC_TYPING = 3;
CLOZE = 4;
IMAGE_CLOZE = 5;
}

Kind kind = 1;
Expand Down
42 changes: 33 additions & 9 deletions rslib/src/cloze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,12 @@ impl ExtractedCloze<'_> {
buf.into()
}

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

fn parse_text_with_clozes(text: &str) -> Vec<TextOrCloze<'_>> {
Expand Down Expand Up @@ -217,6 +220,14 @@ fn reveal_cloze(
) {
let active = cloze.ordinal == cloze_ord;
*active_cloze_found_in_text |= active;
if let Some(image_occlusion_text) = cloze.image_occlusion() {
buf.push_str(&render_image_occlusion(
image_occlusion_text,
question,
active,
));
return;
}
match (question, active) {
(true, true) => {
// question side with active cloze; all inner content is elided
Expand All @@ -235,9 +246,8 @@ fn reveal_cloze(
}
write!(
buf,
r#"<span class="cloze" data-cloze="{}" {} data-ordinal="{}">[{}]</span>"#,
r#"<span class="cloze" data-cloze="{}" data-ordinal="{}">[{}]</span>"#,
encode_attribute(&content_buf),
cloze.image_occlusion(),
cloze.ordinal,
cloze.hint()
)
Expand All @@ -246,8 +256,7 @@ fn reveal_cloze(
(false, true) => {
write!(
buf,
r#"<span class="cloze" {} data-ordinal="{}">"#,
cloze.image_occlusion(),
r#"<span class="cloze" data-ordinal="{}">"#,
cloze.ordinal
)
.unwrap();
Expand All @@ -265,8 +274,7 @@ fn reveal_cloze(
// question or answer side inactive cloze; text shown, children may be active
write!(
buf,
r#"<span class="cloze-inactive" {} data-ordinal="{}">"#,
cloze.image_occlusion(),
r#"<span class="cloze-inactive" data-ordinal="{}">"#,
cloze.ordinal
)
.unwrap();
Expand All @@ -283,6 +291,22 @@ fn reveal_cloze(
}
}

fn render_image_occlusion(text: &str, question_side: bool, active: bool) -> String {
if question_side && active {
format!(
r#"<div class="cloze" {}></div>"#,
&get_image_cloze_data(text)
)
} else if (question_side && !active) || (!question_side && !active) {
format!(
r#"<div class="cloze-inactive" {}></div>"#,
&get_image_cloze_data(text)
)
} else {
"".into()
}
}

pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str> {
let mut buf = String::new();
let mut active_cloze_found_in_text = false;
Expand Down
54 changes: 21 additions & 33 deletions rslib/src/image_occlusion/imageocclusion.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
pub(crate) static ref IMAGE_OCCLUSION_REGEX: Regex = Regex::new(
r#"(?xsi)
::image-occlusion:
"#
)
.unwrap();
}

pub(crate) fn contains_image_occlusion(text: &str) -> bool {
IMAGE_OCCLUSION_REGEX.is_match(text)
}

// split following
// text = "rect:399.01,99.52,167.09,33.78:fill=#0a2cee:stroke=1"
// with
Expand All @@ -33,12 +17,12 @@ pub fn get_image_cloze_data(text: &str) -> String {
let mut rx = "";
let mut ry = "";
let mut points = "";
let mut quesmaskcolor = "";

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

if parts.len() >= 2 {
// parts[0] is image-occlusion, 1 is shape
shape = parts[1];
shape = parts[0];
for part in parts[1..].iter() {
let values: Vec<&str> = part.split('=').collect();
if values.len() >= 2 {
Expand All @@ -49,10 +33,10 @@ pub fn get_image_cloze_data(text: &str) -> String {
"height" => height = values[1],
"fill" => fill = values[1],
"stroke" => stroke = values[1],

"rx" => rx = values[1],
"ry" => ry = values[1],
"points" => points = values[1],
"quesmaskcolor" => quesmaskcolor = values[1],
_ => {}
}
}
Expand Down Expand Up @@ -117,32 +101,36 @@ pub fn get_image_cloze_data(text: &str) -> String {
result.push_str(&format!("data-stroke=\"{}\" ", stroke));
}

if !quesmaskcolor.is_empty() {
result.push_str(&format!("data-quesmaskcolor=\"{}\" ", quesmaskcolor));
}

result
}

//----------------------------------------
// Tests
//----------------------------------------

#[test]
fn test_contains_image_occlusion() {
assert!(contains_image_occlusion(
"{{c5::image-occlusion:rect:left=10.0:top=20:width=30:height=10:fill=#ffe34d:stroke=5}}"
));
}

#[test]
fn test_get_image_cloze_data() {
assert_eq!(
get_image_cloze_data("image-occlusion:rect:left=10:top=20:width=30:height=10:fill=#ffe34d:stroke=5"),
format!(r#"data-shape="rect" data-left="10" data-top="20" data-width="30" data-height="10" data-fill="{}" data-stroke="5" "#, "#ffe34d")
);
get_image_cloze_data(
"rect:left=10:top=20:width=30:height=10:fill=#ffe34d:stroke=5:quesmaskcolor=#ff0000"
),
format!(
r#"data-shape="rect" data-left="10" data-top="20" data-width="30" data-height="10" data-fill="{}" data-stroke="5" data-quesmaskcolor="{}" "#,
"#ffe34d", "#ff0000"
)
);
assert_eq!(
get_image_cloze_data("image-occlusion:ellipse:left=15:top=20:width=10:height=20:rx=10:ry=5:fill=red:stroke=5"),
get_image_cloze_data(
"ellipse:left=15:top=20:width=10:height=20:rx=10:ry=5:fill=red:stroke=5"
),
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" "#
);
);
assert_eq!(
get_image_cloze_data("image-occlusion:polygon:points=0,0 10,10 20,0:fill=blue:stroke=5"),
get_image_cloze_data("polygon:points=0,0 10,10 20,0:fill=blue:stroke=5"),
r#"data-shape="polygon" data-points="[[0,0],[10,10],[20,0]]" data-fill="blue" data-stroke="5" "#
);
);
}
27 changes: 27 additions & 0 deletions rslib/src/notetype/stock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub fn all_stock_notetypes(tr: &I18n) -> Vec<Notetype> {
basic_optional_reverse(tr),
basic_typing(tr),
cloze(tr),
image_cloze(tr),
]
}

Expand Down Expand Up @@ -123,3 +124,29 @@ pub(crate) fn cloze(tr: &I18n) -> Notetype {
nt.add_template(nt.name.clone(), qfmt, afmt);
nt
}

pub(crate) fn image_cloze(tr: &I18n) -> Notetype {
let mut nt = Notetype {
name: tr.notetypes_image_occlusion_name().into(),
config: NotetypeConfig::new_cloze(),
..Default::default()
};
let occlusion = tr.notetypes_occlusion();
nt.add_field(occlusion.as_ref());
let image = tr.notetypes_image();
nt.add_field(image.as_ref());
let header = tr.notetypes_header();
nt.add_field(header.as_ref());
let back_extra = tr.notetypes_back_extra_field();
nt.add_field(back_extra.as_ref());

let qfmt = format!(
"{{{{cloze:{}}}}}\n{{{{{}}}}}\n<script> anki.setupImageCloze() </script>",
occlusion, image);
let afmt = format!(
"{{{{{}}}}}\n{}\n{{{{{}}}}}\n<script> anki.setupImageCloze() </script>",
header, qfmt, back_extra
);
nt.add_template(nt.name.clone(), qfmt, afmt);
nt
}
25 changes: 0 additions & 25 deletions rslib/src/template_filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use regex::Regex;

use crate::cloze::cloze_filter;
use crate::cloze::cloze_only_filter;
use crate::image_occlusion::imageocclusion::IMAGE_OCCLUSION_REGEX;
use crate::template::RenderContext;
use crate::text::strip_html;

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

fn image_occlusion_filter(text: &str) -> Cow<str> {
let mut text = text.to_string();
text = text.replace("image-occlusion", "");
text.into()
}

// Tests
//----------------------------------------

Expand Down Expand Up @@ -288,21 +280,4 @@ field</a>
"[anki:tts lang=en_US voices=Bob,Jane]foo[/anki:tts]"
);
}

#[test]
fn test_image_occlusion_filter() {
let ctx = RenderContext {
fields: &Default::default(),
nonempty_fields: &Default::default(),
question_side: true,
card_ord: 0,
};
let text = r#"{{c1::image-occlusion:rect:left=10.0:top=20:width=30:height=10:fill=#ffe34d:stroke=5}}
{{c2::image-occlusion:ellipse:left=15:top=20:width=10:height=20:rx=10:ry=5:fill=red:stroke=5"#;
assert_eq!(
cloze_filter(text, &ctx),
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>
<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>"#
);
}
}
79 changes: 79 additions & 0 deletions ts/reviewer/image_occlusion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

export function setupImageCloze() {
const canvas: HTMLCanvasElement = document.createElement("CANVAS") as HTMLCanvasElement;
canvas.id = "image-occlusion-canvas";
canvas.style.backgroundSize = "100% 100%";
canvas.style.maxWidth = "100%";
canvas.style.maxHeight = "90vh";

const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
const imageFieldElem = document.getElementById("img") as HTMLImageElement;
imageFieldElem.style.display = "none";

const img: HTMLImageElement = new Image();
img.src = imageFieldElem.src;

img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
drawShapes(ctx);
};

document.getElementById("qa")!.appendChild(canvas);
}

function drawShapes(ctx: CanvasRenderingContext2D) {
const activeCloze = document.querySelectorAll(".cloze");
const inActiveCloze = document.querySelectorAll(".cloze-inactive");

for (let clz of activeCloze) {
let cloze = (<HTMLDivElement>clz);
const shape = cloze.dataset.shape!;
const fill = cloze.dataset.quesmaskcolor!;
draw(ctx, cloze, shape, fill);
}

for (let clz of inActiveCloze) {
let cloze = (<HTMLDivElement>clz);
const shape = cloze.dataset.shape!;
const fill = cloze.dataset.fill!;
draw(ctx, cloze, shape, fill);
}
}

function draw(ctx: CanvasRenderingContext2D, cloze: HTMLDivElement, shape: string, color: string) {
ctx.fillStyle = color;

const post_left = parseFloat(cloze.dataset.left!);
const pos_top = parseFloat(cloze.dataset.top!);
const width = parseFloat(cloze.dataset.width!);
const height = parseFloat(cloze.dataset.height!);

switch (shape) {
case "rect":
ctx.fillRect(post_left, pos_top, width, height);
break;

case "ellipse":
const rx = parseFloat(cloze.dataset.rx!);
const ry = parseFloat(cloze.dataset.ry!);
ctx.beginPath();
ctx.ellipse(post_left, pos_top, rx, ry, 0, 0, Math.PI * 2, false);
ctx.fill();
break;

case "polygon":
const points = JSON.parse(cloze.dataset.points!);
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.closePath();
ctx.fill();
break;
}
}
2 changes: 2 additions & 0 deletions ts/reviewer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import "css-browser-selector/css_browser_selector.min";
export { default as $, default as jQuery } from "jquery/dist/jquery";

import { mutateNextCardStates } from "./answering";
import { setupImageCloze } from "./image_occlusion";

globalThis.anki = globalThis.anki || {};
globalThis.anki.mutateNextCardStates = mutateNextCardStates;
globalThis.anki.setupImageCloze = setupImageCloze;

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

Expand Down
2 changes: 2 additions & 0 deletions ts/reviewer/reviewer_extras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
// When all clients are using reviewer.js directly, we can get rid of this.

import { mutateNextCardStates } from "./answering";
import { setupImageCloze } from "./image_occlusion";

globalThis.anki = globalThis.anki || {};
globalThis.anki.mutateNextCardStates = mutateNextCardStates;
globalThis.anki.setupImageCloze = setupImageCloze;

0 comments on commit 7bf0e0b

Please sign in to comment.