mirror of
https://github.com/ankitects/anki.git
synced 2026-01-13 14:03:55 -05:00
Merge branch 'main' into pycache
This commit is contained in:
commit
670a32870b
16 changed files with 200 additions and 139 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
25.06b4
|
||||
25.06b5
|
||||
|
|
|
|||
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -117,7 +117,7 @@ dependencies = [
|
|||
"id_tree",
|
||||
"inflections",
|
||||
"itertools 0.14.0",
|
||||
"nom",
|
||||
"nom 8.0.0",
|
||||
"num_cpus",
|
||||
"num_enum",
|
||||
"once_cell",
|
||||
|
|
@ -4117,6 +4117,15 @@ dependencies = [
|
|||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.3.0"
|
||||
|
|
@ -6258,7 +6267,7 @@ dependencies = [
|
|||
"bytesize",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"nom",
|
||||
"nom 7.1.3",
|
||||
"time",
|
||||
"winapi",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ junction = "1.2.0"
|
|||
libc = "0.2"
|
||||
libc-stdhandle = "0.1"
|
||||
maplit = "1.0.2"
|
||||
nom = "7.1.3"
|
||||
nom = "8.0.0"
|
||||
num-format = "0.4.4"
|
||||
num_cpus = "1.17.0"
|
||||
num_enum = "0.7.3"
|
||||
|
|
|
|||
|
|
@ -2645,6 +2645,15 @@
|
|||
"license_file": null,
|
||||
"description": "A byte-oriented, zero-copy, parser combinators library"
|
||||
},
|
||||
{
|
||||
"name": "nom",
|
||||
"version": "8.0.0",
|
||||
"authors": "contact@geoffroycouprie.com",
|
||||
"repository": "https://github.com/rust-bakery/nom",
|
||||
"license": "MIT",
|
||||
"license_file": null,
|
||||
"description": "A byte-oriented, zero-copy, parser combinators library"
|
||||
},
|
||||
{
|
||||
"name": "ntapi",
|
||||
"version": "0.4.1",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
set -e
|
||||
|
||||
test -f update.sh || {
|
||||
test -f build.sh || {
|
||||
echo "run from release folder"
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -63,6 +63,9 @@ echo "Generated pyproject.toml with version $VERSION"
|
|||
# Show diff if .old file exists
|
||||
if [ -f pyproject.toml.old ]; then
|
||||
echo
|
||||
echo "Differences from previous version:"
|
||||
echo "Differences from previous release version:"
|
||||
diff -u --color=always pyproject.toml.old pyproject.toml || true
|
||||
fi
|
||||
|
||||
echo "Building wheel..."
|
||||
"$UV" build --wheel --out-dir "$PROJ_ROOT/out/wheels"
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Get the project root (two levels up from qt/release)
|
||||
PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
|
||||
# Use extracted uv binary
|
||||
UV="$PROJ_ROOT/out/extracted/uv/uv"
|
||||
|
||||
rm -rf dist
|
||||
"$UV" build --wheel
|
||||
|
||||
#UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) "$UV" publish --index testpypi
|
||||
UV_PUBLISH_TOKEN=$(pass show w/pypi-api) "$UV" publish
|
||||
|
|
@ -14,14 +14,14 @@ use nom::combinator::recognize;
|
|||
use nom::combinator::rest;
|
||||
use nom::combinator::success;
|
||||
use nom::combinator::value;
|
||||
use nom::multi::fold_many0;
|
||||
use nom::multi::many0;
|
||||
use nom::sequence::delimited;
|
||||
use nom::sequence::pair;
|
||||
use nom::sequence::preceded;
|
||||
use nom::sequence::separated_pair;
|
||||
use nom::sequence::terminated;
|
||||
use nom::sequence::tuple;
|
||||
use nom::Input;
|
||||
use nom::Parser;
|
||||
|
||||
use super::CardNodes;
|
||||
use super::Directive;
|
||||
|
|
@ -86,9 +86,12 @@ impl<'a> Directive<'a> {
|
|||
}
|
||||
|
||||
/// Consume 0 or more of anything in " \t\r\n" after `parser`.
|
||||
fn trailing_whitespace0<'parser, 's, P, O>(parser: P) -> impl FnMut(&'s str) -> IResult<'s, O>
|
||||
fn trailing_whitespace0<I, O, E, P>(parser: P) -> impl Parser<I, Output = O, Error = E>
|
||||
where
|
||||
P: FnMut(&'s str) -> IResult<'s, O> + 'parser,
|
||||
I: Input,
|
||||
<I as Input>::Item: nom::AsChar,
|
||||
E: nom::error::ParseError<I>,
|
||||
P: Parser<I, Output = O, Error = E>,
|
||||
{
|
||||
terminated(parser, multispace0)
|
||||
}
|
||||
|
|
@ -97,11 +100,11 @@ where
|
|||
fn is_not0<'parser, 'arr: 'parser, 's: 'parser>(
|
||||
arr: &'arr str,
|
||||
) -> impl FnMut(&'s str) -> IResult<'s, &'s str> + 'parser {
|
||||
alt((is_not(arr), success("")))
|
||||
move |s| alt((is_not(arr), success(""))).parse(s)
|
||||
}
|
||||
|
||||
fn node(s: &str) -> IResult<Node> {
|
||||
alt((sound_node, tag_node, text_node))(s)
|
||||
alt((sound_node, tag_node, text_node)).parse(s)
|
||||
}
|
||||
|
||||
/// A sound tag `[sound:resource]`, where `resource` is pointing to a sound or
|
||||
|
|
@ -110,11 +113,11 @@ fn sound_node(s: &str) -> IResult<Node> {
|
|||
map(
|
||||
delimited(tag("[sound:"), is_not("]"), tag("]")),
|
||||
Node::SoundOrVideo,
|
||||
)(s)
|
||||
)
|
||||
.parse(s)
|
||||
}
|
||||
|
||||
fn take_till_potential_tag_start(s: &str) -> IResult<&str> {
|
||||
use nom::InputTake;
|
||||
// first char could be '[', but wasn't part of a node, so skip (eof ends parse)
|
||||
let (after, offset) = anychar(s).map(|(s, c)| (s, c.len_utf8()))?;
|
||||
Ok(match after.find('[') {
|
||||
|
|
@ -127,7 +130,7 @@ fn take_till_potential_tag_start(s: &str) -> IResult<&str> {
|
|||
fn tag_node(s: &str) -> IResult<Node> {
|
||||
/// Match the start of an opening tag and return its name.
|
||||
fn name(s: &str) -> IResult<&str> {
|
||||
preceded(tag("[anki:"), is_not("] \t\r\n"))(s)
|
||||
preceded(tag("[anki:"), is_not("] \t\r\n")).parse(s)
|
||||
}
|
||||
|
||||
/// Return a parser to match an opening `name` tag and return its options.
|
||||
|
|
@ -138,31 +141,35 @@ fn tag_node(s: &str) -> IResult<Node> {
|
|||
/// empty.
|
||||
fn options(s: &str) -> IResult<Vec<(&str, &str)>> {
|
||||
fn key(s: &str) -> IResult<&str> {
|
||||
is_not("] \t\r\n=")(s)
|
||||
is_not("] \t\r\n=").parse(s)
|
||||
}
|
||||
|
||||
fn val(s: &str) -> IResult<&str> {
|
||||
alt((
|
||||
delimited(tag("\""), is_not0("\""), tag("\"")),
|
||||
is_not0("] \t\r\n\""),
|
||||
))(s)
|
||||
))
|
||||
.parse(s)
|
||||
}
|
||||
|
||||
many0(trailing_whitespace0(separated_pair(key, tag("="), val)))(s)
|
||||
many0(trailing_whitespace0(separated_pair(key, tag("="), val))).parse(s)
|
||||
}
|
||||
|
||||
delimited(
|
||||
pair(tag("[anki:"), trailing_whitespace0(tag(name))),
|
||||
options,
|
||||
tag("]"),
|
||||
)
|
||||
move |s| {
|
||||
delimited(
|
||||
pair(tag("[anki:"), trailing_whitespace0(tag(name))),
|
||||
options,
|
||||
tag("]"),
|
||||
)
|
||||
.parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a parser to match a closing `name` tag.
|
||||
fn closing_parser<'parser, 'name: 'parser, 's: 'parser>(
|
||||
name: &'name str,
|
||||
) -> impl FnMut(&'s str) -> IResult<'s, ()> + 'parser {
|
||||
value((), tuple((tag("[/anki:"), tag(name), tag("]"))))
|
||||
move |s| value((), (tag("[/anki:"), tag(name), tag("]"))).parse(s)
|
||||
}
|
||||
|
||||
/// Return a parser to match and return anything until a closing `name` tag
|
||||
|
|
@ -170,12 +177,13 @@ fn tag_node(s: &str) -> IResult<Node> {
|
|||
fn content_parser<'parser, 'name: 'parser, 's: 'parser>(
|
||||
name: &'name str,
|
||||
) -> impl FnMut(&'s str) -> IResult<'s, &'s str> + 'parser {
|
||||
recognize(fold_many0(
|
||||
pair(not(closing_parser(name)), take_till_potential_tag_start),
|
||||
// we don't need to accumulate anything
|
||||
|| (),
|
||||
|_, _| (),
|
||||
))
|
||||
move |s| {
|
||||
recognize(many0(pair(
|
||||
not(closing_parser(name)),
|
||||
take_till_potential_tag_start,
|
||||
)))
|
||||
.parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
let (_, tag_name) = name(s)?;
|
||||
|
|
@ -185,11 +193,12 @@ fn tag_node(s: &str) -> IResult<Node> {
|
|||
closing_parser(tag_name),
|
||||
),
|
||||
|(options, content)| Node::Directive(Directive::new(tag_name, options, content)),
|
||||
)(s)
|
||||
)
|
||||
.parse(s)
|
||||
}
|
||||
|
||||
fn text_node(s: &str) -> IResult<Node> {
|
||||
map(take_till_potential_tag_start, Node::Text)(s)
|
||||
map(take_till_potential_tag_start, Node::Text).parse(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use nom::bytes::complete::tag;
|
|||
use nom::bytes::complete::take_while;
|
||||
use nom::combinator::map;
|
||||
use nom::IResult;
|
||||
use nom::Parser;
|
||||
use regex::Captures;
|
||||
use regex::Regex;
|
||||
|
||||
|
|
@ -72,7 +73,7 @@ fn tokenize(mut text: &str) -> impl Iterator<Item = Token> {
|
|||
}
|
||||
|
||||
fn close_cloze(text: &str) -> IResult<&str, Token> {
|
||||
map(tag("}}"), |_| Token::CloseCloze)(text)
|
||||
map(tag("}}"), |_| Token::CloseCloze).parse(text)
|
||||
}
|
||||
|
||||
/// Match a run of text until an open/close marker is encountered.
|
||||
|
|
@ -87,7 +88,7 @@ fn tokenize(mut text: &str) -> impl Iterator<Item = Token> {
|
|||
// start with the no-match case
|
||||
let mut index = text.len();
|
||||
for (idx, _) in text.char_indices() {
|
||||
if other_token(&text[idx..]).is_ok() {
|
||||
if other_token.parse(&text[idx..]).is_ok() {
|
||||
index = idx;
|
||||
break;
|
||||
}
|
||||
|
|
@ -99,8 +100,9 @@ fn tokenize(mut text: &str) -> impl Iterator<Item = Token> {
|
|||
if text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let (remaining_text, token) =
|
||||
alt((open_cloze, close_cloze, normal_text))(text).unwrap();
|
||||
let (remaining_text, token) = alt((open_cloze, close_cloze, normal_text))
|
||||
.parse(text)
|
||||
.unwrap();
|
||||
text = remaining_text;
|
||||
Some(token)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use nom::character::complete::char;
|
|||
use nom::error::ErrorKind;
|
||||
use nom::sequence::preceded;
|
||||
use nom::sequence::separated_pair;
|
||||
use nom::Parser;
|
||||
|
||||
fn unescape(text: &str) -> String {
|
||||
text.replace("\\:", ":")
|
||||
|
|
@ -22,11 +23,12 @@ pub fn parse_image_cloze(text: &str) -> Option<ImageOcclusionShape> {
|
|||
if let Some((shape, _)) = text.split_once(':') {
|
||||
let mut properties = vec![];
|
||||
let mut remaining = &text[shape.len()..];
|
||||
while let Ok((rem, (name, value))) = separated_pair::<_, _, _, _, (_, ErrorKind), _, _, _>(
|
||||
while let Ok((rem, (name, value))) = separated_pair::<_, _, _, (_, ErrorKind), _, _, _>(
|
||||
preceded(tag(":"), is_not("=")),
|
||||
tag("="),
|
||||
escaped(is_not("\\:"), '\\', char(':')),
|
||||
)(remaining)
|
||||
)
|
||||
.parse(remaining)
|
||||
{
|
||||
remaining = rem;
|
||||
let value = unescape(value);
|
||||
|
|
|
|||
|
|
@ -105,77 +105,92 @@ impl Collection {
|
|||
progress.update(true, |state| state.current_cards = idx as u32 + 1)?;
|
||||
let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?;
|
||||
let original = card.clone();
|
||||
if let (Some(req), Some(item)) = (&req, item) {
|
||||
card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?;
|
||||
if let Some(req) = &req {
|
||||
// Store decay and desired retention in the card so that add-ons, card info,
|
||||
// stats and browser search/sorts don't need to access the deck config.
|
||||
// Unlike memory states, scheduler doesn't use decay and dr stored in the card.
|
||||
card.desired_retention = desired_retention;
|
||||
card.decay = decay;
|
||||
// if rescheduling
|
||||
if let Some(reviews) = &last_revlog_info {
|
||||
// and we have a last review time for the card
|
||||
if let Some(last_info) = reviews.get(&card.id) {
|
||||
if let Some(last_review) = &last_info.last_reviewed_at {
|
||||
let days_elapsed =
|
||||
timing.next_day_at.elapsed_days_since(*last_review) as i32;
|
||||
// and the card's not new
|
||||
if let Some(state) = &card.memory_state {
|
||||
// or in (re)learning
|
||||
if card.ctype == CardType::Review {
|
||||
let deck = self
|
||||
.get_deck(card.original_or_current_deck_id())?
|
||||
.or_not_found(card.original_or_current_deck_id())?;
|
||||
let deckconfig_id = deck.config_id().unwrap();
|
||||
// reschedule it
|
||||
let original_interval = card.interval;
|
||||
let interval = fsrs.next_interval(
|
||||
Some(state.stability),
|
||||
card.desired_retention.unwrap(),
|
||||
0,
|
||||
);
|
||||
card.interval = rescheduler
|
||||
.as_mut()
|
||||
.and_then(|r| {
|
||||
r.find_interval(
|
||||
interval,
|
||||
1,
|
||||
req.max_interval,
|
||||
days_elapsed as u32,
|
||||
deckconfig_id,
|
||||
get_fuzz_seed(&card, true),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
with_review_fuzz(
|
||||
card.get_fuzz_factor(true),
|
||||
interval,
|
||||
1,
|
||||
req.max_interval,
|
||||
)
|
||||
});
|
||||
let due = if card.original_due != 0 {
|
||||
&mut card.original_due
|
||||
} else {
|
||||
&mut card.due
|
||||
};
|
||||
let new_due = (timing.days_elapsed as i32) - days_elapsed
|
||||
+ card.interval as i32;
|
||||
if let Some(rescheduler) = &mut rescheduler {
|
||||
rescheduler.update_due_cnt_per_day(
|
||||
*due,
|
||||
new_due,
|
||||
deckconfig_id,
|
||||
if let Some(item) = item {
|
||||
card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?;
|
||||
// if rescheduling
|
||||
if let Some(reviews) = &last_revlog_info {
|
||||
// and we have a last review time for the card
|
||||
if let Some(last_info) = reviews.get(&card.id) {
|
||||
if let Some(last_review) = &last_info.last_reviewed_at {
|
||||
let days_elapsed =
|
||||
timing.next_day_at.elapsed_days_since(*last_review) as i32;
|
||||
// and the card's not new
|
||||
if let Some(state) = &card.memory_state {
|
||||
// or in (re)learning
|
||||
if card.ctype == CardType::Review {
|
||||
let deck = self
|
||||
.get_deck(card.original_or_current_deck_id())?
|
||||
.or_not_found(card.original_or_current_deck_id())?;
|
||||
let deckconfig_id = deck.config_id().unwrap();
|
||||
// reschedule it
|
||||
let original_interval = card.interval;
|
||||
let interval = fsrs.next_interval(
|
||||
Some(state.stability),
|
||||
desired_retention.unwrap(),
|
||||
0,
|
||||
);
|
||||
card.interval = rescheduler
|
||||
.as_mut()
|
||||
.and_then(|r| {
|
||||
r.find_interval(
|
||||
interval,
|
||||
1,
|
||||
req.max_interval,
|
||||
days_elapsed as u32,
|
||||
deckconfig_id,
|
||||
get_fuzz_seed(&card, true),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
with_review_fuzz(
|
||||
card.get_fuzz_factor(true),
|
||||
interval,
|
||||
1,
|
||||
req.max_interval,
|
||||
)
|
||||
});
|
||||
let due = if card.original_due != 0 {
|
||||
&mut card.original_due
|
||||
} else {
|
||||
&mut card.due
|
||||
};
|
||||
let new_due = (timing.days_elapsed as i32)
|
||||
- days_elapsed
|
||||
+ card.interval as i32;
|
||||
if let Some(rescheduler) = &mut rescheduler {
|
||||
rescheduler.update_due_cnt_per_day(
|
||||
*due,
|
||||
new_due,
|
||||
deckconfig_id,
|
||||
);
|
||||
}
|
||||
*due = new_due;
|
||||
// Add a rescheduled revlog entry
|
||||
self.log_rescheduled_review(
|
||||
&card,
|
||||
original_interval,
|
||||
usn,
|
||||
)?;
|
||||
}
|
||||
*due = new_due;
|
||||
// Add a rescheduled revlog entry
|
||||
self.log_rescheduled_review(&card, original_interval, usn)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// clear memory states if item is None
|
||||
card.memory_state = None;
|
||||
}
|
||||
} else {
|
||||
// clear FSRS data if FSRS is disabled
|
||||
card.memory_state = None;
|
||||
card.desired_retention = None;
|
||||
card.decay = None;
|
||||
}
|
||||
self.update_card_inner(&mut card, original, usn)?;
|
||||
}
|
||||
|
|
@ -213,8 +228,6 @@ impl Collection {
|
|||
decay,
|
||||
})
|
||||
} else {
|
||||
card.memory_state = None;
|
||||
card.desired_retention = None;
|
||||
Ok(ComputeMemoryStateResponse {
|
||||
state: None,
|
||||
desired_retention,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use crate::collection::Collection;
|
|||
use crate::config::StringKey;
|
||||
use crate::error::Result;
|
||||
use crate::prelude::*;
|
||||
use crate::scheduler::timing::is_unix_epoch_timestamp;
|
||||
|
||||
impl Card {
|
||||
/// Make card due in `days_from_today`.
|
||||
|
|
@ -27,6 +28,7 @@ impl Card {
|
|||
fn set_due_date(
|
||||
&mut self,
|
||||
today: u32,
|
||||
next_day_start: i64,
|
||||
days_from_today: u32,
|
||||
ease_factor: f32,
|
||||
force_reset: bool,
|
||||
|
|
@ -34,8 +36,15 @@ impl Card {
|
|||
let new_due = (today + days_from_today) as i32;
|
||||
let fsrs_enabled = self.memory_state.is_some();
|
||||
let new_interval = if fsrs_enabled {
|
||||
self.interval
|
||||
.saturating_add_signed(new_due - self.original_or_current_due())
|
||||
let due = self.original_or_current_due();
|
||||
let due_diff = if is_unix_epoch_timestamp(due) {
|
||||
let offset = (due as i64 - next_day_start) / 86_400;
|
||||
let due = (today as i64 + offset) as i32;
|
||||
new_due - due
|
||||
} else {
|
||||
new_due - due
|
||||
};
|
||||
self.interval.saturating_add_signed(due_diff)
|
||||
} else if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) {
|
||||
days_from_today.max(1)
|
||||
} else {
|
||||
|
|
@ -114,6 +123,7 @@ impl Collection {
|
|||
let spec = parse_due_date_str(days)?;
|
||||
let usn = self.usn()?;
|
||||
let today = self.timing_today()?.days_elapsed;
|
||||
let next_day_start = self.timing_today()?.next_day_at.0;
|
||||
let mut rng = rand::rng();
|
||||
let distribution = Uniform::new_inclusive(spec.min, spec.max).unwrap();
|
||||
let mut decks_initial_ease: HashMap<DeckId, f32> = HashMap::new();
|
||||
|
|
@ -137,7 +147,13 @@ impl Collection {
|
|||
};
|
||||
let original = card.clone();
|
||||
let days_from_today = distribution.sample(&mut rng);
|
||||
card.set_due_date(today, days_from_today, ease_factor, spec.force_reset);
|
||||
card.set_due_date(
|
||||
today,
|
||||
next_day_start,
|
||||
days_from_today,
|
||||
ease_factor,
|
||||
spec.force_reset,
|
||||
);
|
||||
col.log_manually_scheduled_review(&card, original.interval, usn)?;
|
||||
col.update_card_inner(&mut card, original, usn)?;
|
||||
}
|
||||
|
|
@ -228,26 +244,26 @@ mod test {
|
|||
let mut c = Card::new(NoteId(0), 0, DeckId(0), 0);
|
||||
|
||||
// setting the due date of a new card will convert it
|
||||
c.set_due_date(5, 2, 1.8, false);
|
||||
c.set_due_date(5, 0, 2, 1.8, false);
|
||||
assert_eq!(c.ctype, CardType::Review);
|
||||
assert_eq!(c.due, 7);
|
||||
assert_eq!(c.interval, 2);
|
||||
assert_eq!(c.ease_factor, 1800);
|
||||
|
||||
// reschedule it again the next day, shifting it from day 7 to day 9
|
||||
c.set_due_date(6, 3, 2.5, false);
|
||||
c.set_due_date(6, 0, 3, 2.5, false);
|
||||
assert_eq!(c.due, 9);
|
||||
assert_eq!(c.interval, 2);
|
||||
assert_eq!(c.ease_factor, 1800); // interval doesn't change
|
||||
|
||||
// we can bring cards forward too - return it to its original due date
|
||||
c.set_due_date(6, 1, 2.4, false);
|
||||
c.set_due_date(6, 0, 1, 2.4, false);
|
||||
assert_eq!(c.due, 7);
|
||||
assert_eq!(c.interval, 2);
|
||||
assert_eq!(c.ease_factor, 1800); // interval doesn't change
|
||||
|
||||
// we can force the interval to be reset instead of shifted
|
||||
c.set_due_date(6, 3, 2.3, true);
|
||||
c.set_due_date(6, 0, 3, 2.3, true);
|
||||
assert_eq!(c.due, 9);
|
||||
assert_eq!(c.interval, 3);
|
||||
assert_eq!(c.ease_factor, 1800); // interval doesn't change
|
||||
|
|
@ -259,7 +275,7 @@ mod test {
|
|||
c.original_deck_id = DeckId(1);
|
||||
c.due = -10000;
|
||||
c.queue = CardQueue::New;
|
||||
c.set_due_date(6, 1, 2.2, false);
|
||||
c.set_due_date(6, 0, 1, 2.2, false);
|
||||
assert_eq!(c.due, 7);
|
||||
assert_eq!(c.interval, 2);
|
||||
assert_eq!(c.ease_factor, 2200);
|
||||
|
|
@ -271,7 +287,7 @@ mod test {
|
|||
c.ctype = CardType::Relearn;
|
||||
c.original_due = c.due;
|
||||
c.due = 12345678;
|
||||
c.set_due_date(6, 10, 2.1, false);
|
||||
c.set_due_date(6, 0, 10, 2.1, false);
|
||||
assert_eq!(c.due, 16);
|
||||
assert_eq!(c.interval, 2);
|
||||
assert_eq!(c.ease_factor, 2200); // interval doesn't change
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use nom::error::ErrorKind as NomErrorKind;
|
|||
use nom::multi::many0;
|
||||
use nom::sequence::preceded;
|
||||
use nom::sequence::separated_pair;
|
||||
use nom::Parser;
|
||||
use regex::Captures;
|
||||
use regex::Regex;
|
||||
|
||||
|
|
@ -202,18 +203,19 @@ fn group_inner(input: &str) -> IResult<Vec<Node>> {
|
|||
}
|
||||
|
||||
fn whitespace0(s: &str) -> IResult<Vec<char>> {
|
||||
many0(one_of(" \u{3000}"))(s)
|
||||
many0(one_of(" \u{3000}")).parse(s)
|
||||
}
|
||||
|
||||
/// Optional leading space, then a (negated) group or text
|
||||
fn node(s: &str) -> IResult<Node> {
|
||||
preceded(whitespace0, alt((negated_node, group, text)))(s)
|
||||
preceded(whitespace0, alt((negated_node, group, text))).parse(s)
|
||||
}
|
||||
|
||||
fn negated_node(s: &str) -> IResult<Node> {
|
||||
map(preceded(char('-'), alt((group, text))), |node| {
|
||||
Node::Not(Box::new(node))
|
||||
})(s)
|
||||
})
|
||||
.parse(s)
|
||||
}
|
||||
|
||||
/// One or more nodes surrounded by brackets, eg (one OR two)
|
||||
|
|
@ -233,7 +235,7 @@ fn group(s: &str) -> IResult<Node> {
|
|||
|
||||
/// Either quoted or unquoted text
|
||||
fn text(s: &str) -> IResult<Node> {
|
||||
alt((quoted_term, partially_quoted_term, unquoted_term))(s)
|
||||
alt((quoted_term, partially_quoted_term, unquoted_term)).parse(s)
|
||||
}
|
||||
|
||||
/// Quoted text, including the outer double quotes.
|
||||
|
|
@ -248,7 +250,8 @@ fn partially_quoted_term(s: &str) -> IResult<Node> {
|
|||
escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(" \u{3000}")),
|
||||
char(':'),
|
||||
quoted_term_str,
|
||||
)(s)?;
|
||||
)
|
||||
.parse(s)?;
|
||||
Ok((
|
||||
remaining,
|
||||
Node::Search(search_node_for_text_with_argument(key, val)?),
|
||||
|
|
@ -296,7 +299,7 @@ fn unquoted_term(s: &str) -> IResult<Node> {
|
|||
fn quoted_term_str(s: &str) -> IResult<&str> {
|
||||
let (opened, _) = char('"')(s)?;
|
||||
if let Ok((tail, inner)) =
|
||||
escaped::<_, ParseError, _, _, _, _>(is_not(r#""\"#), '\\', anychar)(opened)
|
||||
escaped::<_, ParseError, _, _>(is_not(r#""\"#), '\\', anychar).parse(opened)
|
||||
{
|
||||
if let Ok((remaining, _)) = char::<_, ParseError>('"')(tail) {
|
||||
Ok((remaining, inner))
|
||||
|
|
@ -321,7 +324,8 @@ fn search_node_for_text(s: &str) -> ParseResult<SearchNode> {
|
|||
// leading : is only possible error for well-formed input
|
||||
let (tail, head) = verify(escaped(is_not(r":\"), '\\', anychar), |t: &str| {
|
||||
!t.is_empty()
|
||||
})(s)
|
||||
})
|
||||
.parse(s)
|
||||
.map_err(|_: nom::Err<ParseError>| parse_failure(s, FailKind::MissingKey))?;
|
||||
if tail.is_empty() {
|
||||
Ok(SearchNode::UnqualifiedText(unescape(head)?))
|
||||
|
|
@ -407,7 +411,7 @@ fn parse_resched(s: &str) -> ParseResult<SearchNode> {
|
|||
|
||||
/// eg prop:ivl>3, prop:ease!=2.5
|
||||
fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
|
||||
let (tail, prop) = alt::<_, _, ParseError, _>((
|
||||
let (tail, prop) = alt((
|
||||
tag("ivl"),
|
||||
tag("due"),
|
||||
tag("reps"),
|
||||
|
|
@ -421,8 +425,9 @@ fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
|
|||
tag("r"),
|
||||
recognize(preceded(tag("cdn:"), alphanumeric1)),
|
||||
recognize(preceded(tag("cds:"), alphanumeric1)),
|
||||
))(prop_clause)
|
||||
.map_err(|_| {
|
||||
))
|
||||
.parse(prop_clause)
|
||||
.map_err(|_: nom::Err<ParseError>| {
|
||||
parse_failure(
|
||||
prop_clause,
|
||||
FailKind::InvalidPropProperty {
|
||||
|
|
@ -431,15 +436,16 @@ fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
|
|||
)
|
||||
})?;
|
||||
|
||||
let (num, operator) = alt::<_, _, ParseError, _>((
|
||||
let (num, operator) = alt((
|
||||
tag("<="),
|
||||
tag(">="),
|
||||
tag("!="),
|
||||
tag("="),
|
||||
tag("<"),
|
||||
tag(">"),
|
||||
))(tail)
|
||||
.map_err(|_| {
|
||||
))
|
||||
.parse(tail)
|
||||
.map_err(|_: nom::Err<ParseError>| {
|
||||
parse_failure(
|
||||
prop_clause,
|
||||
FailKind::InvalidPropOperator {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use nom::bytes::complete::tag;
|
|||
use nom::bytes::complete::take_until;
|
||||
use nom::combinator::map;
|
||||
use nom::sequence::delimited;
|
||||
use nom::Parser;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::cloze::cloze_number_in_fields;
|
||||
|
|
@ -67,7 +68,8 @@ impl TemplateMode {
|
|||
tag(self.end_tag()),
|
||||
),
|
||||
|out| classify_handle(out),
|
||||
)(s)
|
||||
)
|
||||
.parse(s)
|
||||
}
|
||||
|
||||
/// Return the next handlebar, comment or text token.
|
||||
|
|
@ -127,7 +129,8 @@ fn comment_token(s: &str) -> nom::IResult<&str, Token> {
|
|||
tag(COMMENT_END),
|
||||
),
|
||||
Token::Comment,
|
||||
)(s)
|
||||
)
|
||||
.parse(s)
|
||||
}
|
||||
|
||||
fn tokens(mut template: &str) -> impl Iterator<Item = TemplateResult<Token<'_>>> {
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ path = "main.rs"
|
|||
name = "anki-sync-server"
|
||||
|
||||
[dependencies]
|
||||
anki.workspace = true
|
||||
anki = { workspace = true, features = ["rustls"] }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -eo pipefail
|
||||
|
||||
rm -rf out/wheels/*
|
||||
RELEASE=2 ./ninja wheels
|
||||
(cd qt/release && ./build.sh)
|
||||
echo "wheels are in out/wheels"
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export function renderReviews(
|
|||
|
||||
const yTickFormat = (n: number): string => {
|
||||
if (showTime) {
|
||||
return timeSpan(n / 1000, true, false, TimespanUnit.Hours);
|
||||
return timeSpan(n / 1000, true, true, TimespanUnit.Hours);
|
||||
} else {
|
||||
if (Math.round(n) != n) {
|
||||
return "";
|
||||
|
|
|
|||
Loading…
Reference in a new issue