mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Detect incorrect usage of triple slash in TypeScript (#2524)
* Migrate check_copyright to Rust * Add a new lint to check accidental usages of /// in ts/svelte comments * Fix a bunch of incorrect jdoc comments * Move contributor check into minilints Will allow users to detect the issue locally with './ninja check' before pushing to CI. * Make Cargo.toml consistent with other crates
This commit is contained in:
parent
8d1e5c373b
commit
15dcb09036
28 changed files with 365 additions and 240 deletions
|
@ -1,26 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail ${SHELLFLAGS}
|
||||
|
||||
antispam=", at the domain "
|
||||
|
||||
headAuthor=$(git log -1 --pretty=format:'%ae')
|
||||
authorAt=$(echo "$headAuthor" | sed "s/@/$antispam/")
|
||||
if [ $headAuthor = "49699333+dependabot[bot]@users.noreply.github.com" ]; then
|
||||
echo "Dependabot whitelisted."
|
||||
elif git log --pretty=format:'%ae' CONTRIBUTORS | grep -i "$headAuthor" > /dev/null; then
|
||||
echo "Author found in CONTRIBUTORS"
|
||||
else
|
||||
echo "All contributors:"
|
||||
git log --pretty=format:' - %ae' CONTRIBUTORS |sort |uniq |sort -f | sed "s/@/$antispam/"
|
||||
|
||||
echo "Author $authorAt NOT found in list"
|
||||
echo
|
||||
cat <<EOF
|
||||
Please make sure you modify the CONTRIBUTORS file using the email address you
|
||||
are committing from. If you have GitHub configured to hide your email address,
|
||||
you may need to make a change to the CONTRIBUTORS file using the GitHub UI,
|
||||
then try again.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
set -e
|
||||
|
||||
# check author has added themselves to CONTRIBUTORS
|
||||
echo "--- Checking CONTRIBUTORS"
|
||||
.buildkite/linux/check_contributors
|
||||
|
||||
echo "+++ Building and testing"
|
||||
ln -sf out/node_modules .
|
||||
|
||||
|
|
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -222,9 +222,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.70"
|
||||
version = "1.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
||||
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
|
||||
[[package]]
|
||||
name = "apple-bundles"
|
||||
|
@ -2286,6 +2286,16 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minilints"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
"once_cell",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
|
|
|
@ -18,6 +18,7 @@ members = [
|
|||
"build/runner",
|
||||
"ftl",
|
||||
"tools/workspace-hack",
|
||||
"tools/minilints",
|
||||
"qt/bundle/win",
|
||||
"qt/bundle/mac",
|
||||
]
|
||||
|
|
|
@ -17,10 +17,10 @@ use ninja_gen::Build;
|
|||
use ninja_gen::Result;
|
||||
use pylib::build_pylib;
|
||||
use pylib::check_pylib;
|
||||
use python::check_copyright;
|
||||
use python::check_python;
|
||||
use python::setup_venv;
|
||||
use rust::build_rust;
|
||||
use rust::check_minilints;
|
||||
use rust::check_rust;
|
||||
use web::build_and_check_web;
|
||||
use web::check_sql;
|
||||
|
@ -52,7 +52,7 @@ fn main() -> Result<()> {
|
|||
check_python(build)?;
|
||||
check_proto(build)?;
|
||||
check_sql(build)?;
|
||||
check_copyright(build)?;
|
||||
check_minilints(build)?;
|
||||
|
||||
build.trailing_text = "default pylib/anki qt/aqt\n".into();
|
||||
|
||||
|
|
|
@ -236,43 +236,3 @@ fn add_pylint(build: &mut Build) -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_copyright(build: &mut Build) -> Result<()> {
|
||||
let script = inputs!["tools/copyright_headers.py"];
|
||||
let files = inputs![glob![
|
||||
"{build,rslib,pylib,qt,ftl,python,sass,tools,ts}/**/*.{py,rs,ts,svelte,mjs}",
|
||||
"qt/bundle/PyOxidizer/**"
|
||||
]];
|
||||
build.add(
|
||||
"check:copyright",
|
||||
RunCommand {
|
||||
command: "$runner",
|
||||
args: "run --stamp=$out $pyenv_bin $script check",
|
||||
inputs: hashmap! {
|
||||
"pyenv_bin" => inputs![":pyenv:bin"],
|
||||
"script" => script.clone(),
|
||||
"script" => script.clone(),
|
||||
"" => files.clone(),
|
||||
},
|
||||
outputs: hashmap! {
|
||||
"out" => vec!["tests/copyright.check.marker"]
|
||||
},
|
||||
},
|
||||
)?;
|
||||
build.add(
|
||||
"fix:copyright",
|
||||
RunCommand {
|
||||
command: "$runner",
|
||||
args: "run --stamp=$out $pyenv_bin $script fix",
|
||||
inputs: hashmap! {
|
||||
"pyenv_bin" => inputs![":pyenv:bin"],
|
||||
"script" => script,
|
||||
"" => files,
|
||||
},
|
||||
outputs: hashmap! {
|
||||
"out" => vec!["tests/copyright.fix.marker"]
|
||||
},
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use ninja_gen::action::BuildAction;
|
||||
use ninja_gen::build::FilesHandle;
|
||||
use ninja_gen::cargo::CargoBuild;
|
||||
use ninja_gen::cargo::CargoClippy;
|
||||
use ninja_gen::cargo::CargoFormat;
|
||||
|
@ -9,6 +11,7 @@ use ninja_gen::cargo::CargoTest;
|
|||
use ninja_gen::cargo::RustOutput;
|
||||
use ninja_gen::git::SyncSubmodule;
|
||||
use ninja_gen::glob;
|
||||
use ninja_gen::input::BuildInput;
|
||||
use ninja_gen::inputs;
|
||||
use ninja_gen::Build;
|
||||
use ninja_gen::Result;
|
||||
|
@ -151,3 +154,57 @@ pub fn check_rust(build: &mut Build) -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_minilints(build: &mut Build) -> Result<()> {
|
||||
struct RunMinilints {
|
||||
pub deps: BuildInput,
|
||||
pub fix: bool,
|
||||
}
|
||||
|
||||
impl BuildAction for RunMinilints {
|
||||
fn command(&self) -> &str {
|
||||
"$minilints_bin $fix"
|
||||
}
|
||||
|
||||
fn files(&mut self, build: &mut impl FilesHandle) {
|
||||
build.add_inputs("minilints_bin", inputs![":build:minilints"]);
|
||||
build.add_inputs("", &self.deps);
|
||||
build.add_variable("fix", if self.fix { "fix" } else { "" });
|
||||
build.add_output_stamp(format!("tests/minilints.{}", self.fix));
|
||||
}
|
||||
|
||||
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
|
||||
build.add(
|
||||
"build:minilints",
|
||||
CargoBuild {
|
||||
inputs: inputs![glob!("tools/minilints/**/*")],
|
||||
outputs: &[RustOutput::Binary("minilints")],
|
||||
target: None,
|
||||
extra_args: "-p minilints",
|
||||
release_override: Some(false),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let files = inputs![glob![
|
||||
"**/*.{py,rs,ts,svelte,mjs}",
|
||||
"{node_modules,qt/bundle/PyOxidizer}/**"
|
||||
]];
|
||||
|
||||
build.add(
|
||||
"check:minilints",
|
||||
RunMinilints {
|
||||
deps: files.clone(),
|
||||
fix: false,
|
||||
},
|
||||
)?;
|
||||
build.add(
|
||||
"fix:minilints",
|
||||
RunMinilints {
|
||||
deps: files,
|
||||
fix: true,
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
want_fix = sys.argv[1] == "fix"
|
||||
checked_for_dirty = False
|
||||
|
||||
nonstandard_header = {
|
||||
"pylib/anki/_vendor/stringcase.py",
|
||||
"pylib/anki/importing/pauker.py",
|
||||
"pylib/anki/importing/supermemo_xml.py",
|
||||
"pylib/anki/statsbg.py",
|
||||
"pylib/tools/protoc-gen-mypy.py",
|
||||
"python/pyqt/install.py",
|
||||
"python/write_wheel.py",
|
||||
"qt/aqt/mpv.py",
|
||||
"qt/aqt/winpaths.py",
|
||||
"qt/bundle/build.rs",
|
||||
"qt/bundle/src/main.rs",
|
||||
}
|
||||
|
||||
ignored_folders = [
|
||||
"out",
|
||||
"node_modules",
|
||||
"qt/forms",
|
||||
"tools/workspace-hack",
|
||||
"qt/bundle/PyOxidizer",
|
||||
]
|
||||
|
||||
|
||||
def fix(path: Path) -> None:
|
||||
with open(path, "r", encoding="utf8") as f:
|
||||
existing_text = f.read()
|
||||
path_str = str(path)
|
||||
if path_str.endswith(".py"):
|
||||
header = """\
|
||||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
"""
|
||||
elif (
|
||||
path_str.endswith(".ts")
|
||||
or path_str.endswith(".rs")
|
||||
or path_str.endswith(".mjs")
|
||||
):
|
||||
header = """\
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
"""
|
||||
elif path_str.endswith(".svelte"):
|
||||
header = """\
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
|
||||
"""
|
||||
with open(path, "w", encoding="utf8") as f:
|
||||
f.write(header + existing_text)
|
||||
|
||||
|
||||
found = False
|
||||
if sys.platform == "win32":
|
||||
ignored_folders = [f.replace("/", "\\") for f in ignored_folders]
|
||||
nonstandard_header = {f.replace("/", "\\") for f in nonstandard_header}
|
||||
|
||||
for dirpath, dirnames, fnames in os.walk("."):
|
||||
dir = Path(dirpath)
|
||||
|
||||
# avoid infinite recursion with old symlink
|
||||
if ".bazel" in dirnames:
|
||||
dirnames.remove(".bazel")
|
||||
|
||||
ignore = False
|
||||
for folder in ignored_folders:
|
||||
if folder in dirpath:
|
||||
ignore = True
|
||||
break
|
||||
if ignore:
|
||||
continue
|
||||
|
||||
for fname in fnames:
|
||||
for ext in ".py", ".ts", ".rs", ".svelte", ".mjs":
|
||||
if fname.endswith(ext):
|
||||
path = dir / fname
|
||||
with open(path, encoding="utf8") as f:
|
||||
top = f.read(256)
|
||||
if not top.strip():
|
||||
continue
|
||||
if str(path) in nonstandard_header:
|
||||
continue
|
||||
if fname.endswith(".d.ts"):
|
||||
continue
|
||||
missing = "Ankitects Pty Ltd and contributors" not in top
|
||||
if missing:
|
||||
if want_fix:
|
||||
if not checked_for_dirty:
|
||||
if subprocess.getoutput("git diff"):
|
||||
print("stage any changes first")
|
||||
sys.exit(1)
|
||||
checked_for_dirty = True
|
||||
fix(path)
|
||||
else:
|
||||
print("missing standard copyright header:", path)
|
||||
found = True
|
||||
|
||||
if found:
|
||||
sys.exit(1)
|
15
tools/minilints/Cargo.toml
Normal file
15
tools/minilints/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "minilints"
|
||||
publish = false
|
||||
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
camino = "1.1.4"
|
||||
once_cell = "1.17.1"
|
||||
walkdir = "2.3.3"
|
224
tools/minilints/src/main.rs
Normal file
224
tools/minilints/src/main.rs
Normal file
|
@ -0,0 +1,224 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use camino::Utf8Path;
|
||||
use once_cell::unsync::Lazy;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const NONSTANDARD_HEADER: &[&str] = &[
|
||||
"./pylib/anki/_vendor/stringcase.py",
|
||||
"./pylib/anki/importing/pauker.py",
|
||||
"./pylib/anki/importing/supermemo_xml.py",
|
||||
"./pylib/anki/statsbg.py",
|
||||
"./pylib/tools/protoc-gen-mypy.py",
|
||||
"./python/pyqt/install.py",
|
||||
"./python/write_wheel.py",
|
||||
"./qt/aqt/mpv.py",
|
||||
"./qt/aqt/winpaths.py",
|
||||
"./qt/bundle/build.rs",
|
||||
"./qt/bundle/src/main.rs",
|
||||
];
|
||||
|
||||
const IGNORED_FOLDERS: &[&str] = &[
|
||||
"./out",
|
||||
"./node_modules",
|
||||
"./qt/aqt/forms",
|
||||
"./tools/workspace-hack",
|
||||
"./qt/bundle/PyOxidizer",
|
||||
];
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let want_fix = env::args().nth(1) == Some("fix".to_string());
|
||||
let mut ctx = LintContext::new(want_fix);
|
||||
ctx.check_contributors()?;
|
||||
ctx.walk_folders(Path::new("."))?;
|
||||
if ctx.found_problems {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct LintContext {
|
||||
want_fix: bool,
|
||||
unstaged_changes: Lazy<()>,
|
||||
found_problems: bool,
|
||||
nonstandard_headers: HashSet<&'static Utf8Path>,
|
||||
}
|
||||
|
||||
impl LintContext {
|
||||
pub fn new(want_fix: bool) -> Self {
|
||||
Self {
|
||||
want_fix,
|
||||
unstaged_changes: Lazy::new(check_for_unstaged_changes),
|
||||
found_problems: false,
|
||||
nonstandard_headers: NONSTANDARD_HEADER.iter().map(Utf8Path::new).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_folders(&mut self, root: &Path) -> Result<()> {
|
||||
let ignored_folders: HashSet<_> = IGNORED_FOLDERS.iter().map(Utf8Path::new).collect();
|
||||
let walker = WalkDir::new(root).into_iter();
|
||||
for entry in walker.filter_entry(|e| {
|
||||
!ignored_folders.contains(&Utf8Path::from_path(e.path()).expect("utf8"))
|
||||
}) {
|
||||
let entry = entry.unwrap();
|
||||
let path = Utf8Path::from_path(entry.path()).context("utf8")?;
|
||||
|
||||
let exts: HashSet<_> = ["py", "ts", "rs", "svelte", "mjs"]
|
||||
.into_iter()
|
||||
.map(Some)
|
||||
.collect();
|
||||
if exts.contains(&path.extension()) {
|
||||
self.check_copyright(path)?;
|
||||
self.check_triple_slash(path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_copyright(&mut self, path: &Utf8Path) -> Result<()> {
|
||||
if path.file_name().unwrap().ends_with(".d.ts") {
|
||||
return Ok(());
|
||||
}
|
||||
let head = head_of_file(path)?;
|
||||
if head.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if self.nonstandard_headers.contains(&path) {
|
||||
return Ok(());
|
||||
}
|
||||
let missing = !head.contains("Ankitects Pty Ltd and contributors");
|
||||
if missing {
|
||||
if self.want_fix {
|
||||
Lazy::force(&self.unstaged_changes);
|
||||
fix_copyright(path)?;
|
||||
} else {
|
||||
println!("missing standard copyright header: {:?}", path);
|
||||
self.found_problems = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_triple_slash(&mut self, path: &Utf8Path) -> Result<()> {
|
||||
if !matches!(path.extension(), Some("ts") | Some("svelte")) {
|
||||
return Ok(());
|
||||
}
|
||||
for line in fs::read_to_string(path)?.lines() {
|
||||
if line.contains("///") && !line.contains("/// <reference") {
|
||||
println!("not a docstring: {path}: {line}");
|
||||
self.found_problems = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_contributors(&self) -> Result<()> {
|
||||
let antispam = ", at the domain ";
|
||||
|
||||
let last_author = String::from_utf8(
|
||||
Command::new("git")
|
||||
.args(["log", "-1", "--pretty=format:%ae"])
|
||||
.output()?
|
||||
.stdout,
|
||||
)?;
|
||||
|
||||
let all_contributors = String::from_utf8(
|
||||
Command::new("git")
|
||||
.args(["log", "--pretty=format:%ae", "CONTRIBUTORS"])
|
||||
.output()?
|
||||
.stdout,
|
||||
)?;
|
||||
let all_contributors = all_contributors.lines().collect::<HashSet<&str>>();
|
||||
|
||||
if last_author == "49699333+dependabot[bot]@users.noreply.github.com" {
|
||||
println!("Dependabot whitelisted.");
|
||||
return Ok(());
|
||||
} else if all_contributors.contains(last_author.as_str()) {
|
||||
println!("Author found in CONTRIBUTORS");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("All contributors:");
|
||||
println!("{}", {
|
||||
let mut contribs: Vec<_> = all_contributors
|
||||
.iter()
|
||||
.map(|s| s.replace('@', antispam))
|
||||
.collect();
|
||||
contribs.sort();
|
||||
contribs.join("\n")
|
||||
});
|
||||
|
||||
println!(
|
||||
"Author {} NOT found in list",
|
||||
last_author.replace('@', antispam)
|
||||
);
|
||||
|
||||
println!(
|
||||
"\nPlease make sure you modify the CONTRIBUTORS file using the email address you \
|
||||
are committing from. If you have GitHub configured to hide your email address, \
|
||||
you may need to make a change to the CONTRIBUTORS file using the GitHub UI, \
|
||||
then try again."
|
||||
);
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn head_of_file(path: &Utf8Path) -> Result<String> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut buffer = vec![0; 256];
|
||||
let size = file.read(&mut buffer)?;
|
||||
buffer.truncate(size);
|
||||
Ok(String::from_utf8(buffer).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn fix_copyright(path: &Utf8Path) -> Result<()> {
|
||||
let header = match path.extension().unwrap() {
|
||||
"py" => {
|
||||
r#"# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
"#
|
||||
}
|
||||
"ts" | "rs" | "mjs" => {
|
||||
r#"// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
"#
|
||||
}
|
||||
"svelte" => {
|
||||
r#"<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
"#
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let data = fs::read_to_string(path).with_context(|| format!("reading {path}"))?;
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(path)
|
||||
.with_context(|| format!("opening {path}"))?;
|
||||
write!(file, "{}{}", header, data).with_context(|| format!("writing {path}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_for_unstaged_changes() {
|
||||
let output = Command::new("git").arg("diff").output().unwrap();
|
||||
if !output.stdout.is_empty() {
|
||||
println!("stage any changes first");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
|
@ -11,12 +11,12 @@ function nullToNegativeOne(list: (number | null)[]): number[] {
|
|||
return list.map((val) => val ?? -1);
|
||||
}
|
||||
|
||||
/// Public only for tests.
|
||||
/** Public only for tests. */
|
||||
export function negativeOneToNull(list: number[]): (number | null)[] {
|
||||
return list.map((val) => (val === -1 ? null : val));
|
||||
}
|
||||
|
||||
/// Wrapper for the protobuf message to make it more ergonomic.
|
||||
/** Wrapper for the protobuf message to make it more ergonomic. */
|
||||
export class ChangeNotetypeInfoWrapper {
|
||||
fields: (number | null)[];
|
||||
templates?: (number | null)[];
|
||||
|
@ -33,26 +33,26 @@ export class ChangeNotetypeInfoWrapper {
|
|||
this.oldNotetypeName = info.oldNotetypeName;
|
||||
}
|
||||
|
||||
/// A list with an entry for each field/template in the new notetype, with
|
||||
/// the values pointing back to indexes in the original notetype.
|
||||
/** A list with an entry for each field/template in the new notetype, with
|
||||
the values pointing back to indexes in the original notetype. */
|
||||
mapForContext(ctx: MapContext): (number | null)[] {
|
||||
return ctx == MapContext.Template ? this.templates ?? [] : this.fields;
|
||||
}
|
||||
|
||||
/// Return index of old fields/templates, with null values mapped to "Nothing"
|
||||
/// at the end.
|
||||
/** Return index of old fields/templates, with null values mapped to "Nothing"
|
||||
at the end.*/
|
||||
getOldIndex(ctx: MapContext, newIdx: number): number {
|
||||
const map = this.mapForContext(ctx);
|
||||
const val = map[newIdx];
|
||||
return val ?? this.getOldNamesIncludingNothing(ctx).length - 1;
|
||||
}
|
||||
|
||||
/// Return all the old names, with "Nothing" at the end.
|
||||
/** Return all the old names, with "Nothing" at the end. */
|
||||
getOldNamesIncludingNothing(ctx: MapContext): string[] {
|
||||
return [...this.getOldNames(ctx), tr.changeNotetypeNothing()];
|
||||
}
|
||||
|
||||
/// Old names without "Nothing" at the end.
|
||||
/** Old names without "Nothing" at the end. */
|
||||
getOldNames(ctx: MapContext): string[] {
|
||||
return ctx == MapContext.Template
|
||||
? this.info.oldTemplateNames
|
||||
|
@ -93,7 +93,7 @@ export class ChangeNotetypeInfoWrapper {
|
|||
return this.info.input as Notetypes.ChangeNotetypeRequest;
|
||||
}
|
||||
|
||||
/// Pack changes back into input message for saving.
|
||||
/** Pack changes back into input message for saving. */
|
||||
intoInput(): Notetypes.ChangeNotetypeRequest {
|
||||
const input = this.info.input as Notetypes.ChangeNotetypeRequest;
|
||||
input.newFields = nullToNegativeOne(this.fields);
|
||||
|
|
|
@ -18,8 +18,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let input: HTMLInputElement;
|
||||
let focused = false;
|
||||
|
||||
/// Set value to a new number, clamping it to a valid range, and
|
||||
/// leaving it unchanged if `newValue` is NaN.
|
||||
/** Set value to a new number, clamping it to a valid range, and
|
||||
leaving it unchanged if `newValue` is NaN. */
|
||||
function updateValue(newValue: number) {
|
||||
if (Number.isNaN(newValue)) {
|
||||
// avoid updating the value
|
||||
|
|
|
@ -24,7 +24,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
export let state: DeckOptionsState;
|
||||
|
||||
/// Ensure blur handler has fired so changes get committed.
|
||||
/** Ensure blur handler has fired so changes get committed. */
|
||||
async function commitEditing(): Promise<void> {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
|
|
|
@ -21,7 +21,7 @@ export interface ParentLimits {
|
|||
reviews: number;
|
||||
}
|
||||
|
||||
/// Info for showing the top selector
|
||||
/** Info for showing the top selector */
|
||||
export interface ConfigListEntry {
|
||||
idx: number;
|
||||
name: string;
|
||||
|
@ -124,17 +124,17 @@ export class DeckOptionsState {
|
|||
this.updateConfigList();
|
||||
}
|
||||
|
||||
/// Adds a new config, making it current.
|
||||
/** Adds a new config, making it current. */
|
||||
addConfig(name: string): void {
|
||||
this.addConfigFrom(name, this.defaults);
|
||||
}
|
||||
|
||||
/// Clone the current config, making it current.
|
||||
/** Clone the current config, making it current. */
|
||||
cloneConfig(name: string): void {
|
||||
this.addConfigFrom(name, this.configs[this.selectedIdx].config.config!);
|
||||
}
|
||||
|
||||
/// Clone the current config, making it current.
|
||||
/** Clone the current config, making it current. */
|
||||
private addConfigFrom(name: string, source: DeckConfig.DeckConfig.IConfig): void {
|
||||
const uniqueName = this.ensureNewNameUnique(name);
|
||||
const config = DeckConfig.DeckConfig.create({
|
||||
|
@ -158,7 +158,7 @@ export class DeckOptionsState {
|
|||
return this.configs[this.selectedIdx].config.id === 1;
|
||||
}
|
||||
|
||||
/// Will throw if the default deck is selected.
|
||||
/** Will throw if the default deck is selected. */
|
||||
removeCurrentConfig(): void {
|
||||
const currentId = this.configs[this.selectedIdx].config.id;
|
||||
if (currentId === 1) {
|
||||
|
@ -250,12 +250,12 @@ export class DeckOptionsState {
|
|||
this.configListSetter?.(this.getConfigList());
|
||||
}
|
||||
|
||||
/// Returns a copy of the currently selected config.
|
||||
/** Returns a copy of the currently selected config. */
|
||||
private getCurrentConfig(): DeckConfig.DeckConfig.Config {
|
||||
return cloneDeep(this.configs[this.selectedIdx].config.config!);
|
||||
}
|
||||
|
||||
/// Extra data associated with current config (for add-ons)
|
||||
/** Extra data associated with current config (for add-ons) */
|
||||
private getCurrentAuxData(): Record<string, unknown> {
|
||||
const conf = this.configs[this.selectedIdx].config.config!;
|
||||
return bytesToObject(conf.other);
|
||||
|
|
|
@ -25,7 +25,7 @@ import { GraphRange } from "./graph-helpers";
|
|||
import { setDataAvailable } from "./graph-helpers";
|
||||
import { hideTooltip, showTooltip } from "./tooltip";
|
||||
|
||||
/// 4 element array
|
||||
/** 4 element array */
|
||||
type ButtonCounts = number[];
|
||||
|
||||
export interface GraphData {
|
||||
|
|
|
@ -99,8 +99,8 @@ export type SearchDispatch = <EventKey extends Extract<keyof SearchEventMap, str
|
|||
detail: SearchEventMap[EventKey],
|
||||
) => void;
|
||||
|
||||
/// Convert a protobuf map that protobufjs represents as an object with string
|
||||
/// keys into a Map with numeric keys.
|
||||
/** Convert a protobuf map that protobufjs represents as an object with string
|
||||
keys into a Map with numeric keys. */
|
||||
export function numericMap<T>(obj: { [k: string]: T }): Map<number, T> {
|
||||
return new Map(Object.entries(obj).map(([k, v]) => [Number(k), v]));
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ function totalsForBin(bin: BinType): number[] {
|
|||
return total;
|
||||
}
|
||||
|
||||
/// eg idx=0 is mature count, idx=1 is mature+young count, etc
|
||||
/** eg idx=0 is mature count, idx=1 is mature+young count, etc */
|
||||
function cumulativeBinValue(bin: BinType, idx: number): number {
|
||||
return sum(totalsForBin(bin).slice(0, idx + 1));
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// Keep property if true.
|
||||
/** Keep property if true. */
|
||||
type StylingPredicate = (property: string, value: string) => boolean;
|
||||
|
||||
const keep = (_key: string, _value: string) => true;
|
||||
const discard = (_key: string, _value: string) => false;
|
||||
|
||||
/// Return a function that filters out certain styles.
|
||||
/// - If the style is listed in `exceptions`, the provided predicate is used.
|
||||
/// - If the style is not listed, the default predicate is used instead.
|
||||
/** Return a function that filters out certain styles.
|
||||
- If the style is listed in `exceptions`, the provided predicate is used.
|
||||
- If the style is not listed, the default predicate is used instead. */
|
||||
function filterStyling(
|
||||
defaultPredicate: StylingPredicate,
|
||||
exceptions: Record<string, StylingPredicate>,
|
||||
|
|
|
@ -9,7 +9,7 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
/// HTML <a> tag pointing to a bridge command.
|
||||
/** HTML <a> tag pointing to a bridge command. */
|
||||
export function bridgeLink(command: string, label: string): string {
|
||||
return `<a href="javascript:bridgeCommand('${command}')">${label}</a>`;
|
||||
}
|
||||
|
|
|
@ -9,17 +9,17 @@ export enum CardType {
|
|||
}
|
||||
|
||||
export enum CardQueue {
|
||||
/// due is the order cards are shown in
|
||||
/** due is the order cards are shown in */
|
||||
New = 0,
|
||||
/// due is a unix timestamp
|
||||
/** due is a unix timestamp */
|
||||
Learn = 1,
|
||||
/// due is days since creation date
|
||||
/** due is days since creation date */
|
||||
Review = 2,
|
||||
DayLearn = 3,
|
||||
/// due is a unix timestamp.
|
||||
/// preview cards only placed here when failed.
|
||||
/** due is a unix timestamp. */
|
||||
/** preview cards only placed here when failed. */
|
||||
PreviewRepeat = 4,
|
||||
/// cards are not due in these states
|
||||
/** cards are not due in these states */
|
||||
Suspended = -1,
|
||||
SchedBuried = -2,
|
||||
UserBuried = -3,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// These links are checked in CI to ensure they are valid.
|
||||
/** These links are checked in CI to ensure they are valid. */
|
||||
export const HelpPage = {
|
||||
DeckOptions: {
|
||||
maximumInterval: "https://docs.ankiweb.net/deck-options.html#maximum-interval",
|
||||
|
|
|
@ -64,8 +64,8 @@ export function localeCompare(
|
|||
return first.localeCompare(second, langs, options);
|
||||
}
|
||||
|
||||
/// Treat text like HTML, merging multiple spaces and converting
|
||||
/// newlines to spaces.
|
||||
/** Treat text like HTML, merging multiple spaces and converting
|
||||
newlines to spaces. */
|
||||
export function withCollapsedWhitespace(s: string): string {
|
||||
return s.replace(/\s+/g, " ");
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// Add night-mode class to documentElement if hash location is #night, and
|
||||
/// return true if added.
|
||||
/** Add night-mode class to documentElement if hash location is #night, and
|
||||
return true if added. */
|
||||
export function checkNightMode(): boolean {
|
||||
const nightMode = window.location.hash == "#night";
|
||||
if (nightMode) {
|
||||
|
|
|
@ -31,8 +31,8 @@ type PackageDeprecation<T extends Record<string, unknown>> = {
|
|||
[key in keyof T]?: string;
|
||||
};
|
||||
|
||||
/// This can be extended to allow require() calls at runtime, for packages
|
||||
/// that are not included at bundling time.
|
||||
/** This can be extended to allow require() calls at runtime, for packages
|
||||
that are not included at bundling time. */
|
||||
const runtimePackages: Partial<Record<AnkiPackages, Record<string, unknown>>> = {};
|
||||
const prohibit = () => false;
|
||||
|
||||
|
|
|
@ -141,8 +141,8 @@ function innerShortcut(
|
|||
|
||||
export interface RegisterShortcutRestParams {
|
||||
target: EventTarget;
|
||||
/// There might be no good reason to use `keyup` other
|
||||
/// than to circumvent Qt bugs
|
||||
/** There might be no good reason to use `keyup` other
|
||||
than to circumvent Qt bugs */
|
||||
event: "keydown" | "keyup";
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ export function naturalUnit(secs: number): TimespanUnit {
|
|||
}
|
||||
}
|
||||
|
||||
/// Number of seconds in a given unit.
|
||||
/** Number of seconds in a given unit. */
|
||||
export function unitSeconds(unit: TimespanUnit): number {
|
||||
switch (unit) {
|
||||
case TimespanUnit.Seconds:
|
||||
|
@ -75,7 +75,7 @@ export function unitAmount(unit: TimespanUnit, secs: number): number {
|
|||
return secs / unitSeconds(unit);
|
||||
}
|
||||
|
||||
/// Largest unit provided seconds can be divided by without a remainder.
|
||||
/** Largest unit provided seconds can be divided by without a remainder. */
|
||||
export function naturalWholeUnit(secs: number): TimespanUnit {
|
||||
let unit = naturalUnit(secs);
|
||||
while (unit != TimespanUnit.Seconds) {
|
||||
|
@ -142,10 +142,10 @@ function i18nFuncForUnit(
|
|||
}
|
||||
}
|
||||
|
||||
/// Describe the given seconds using the largest appropriate unit.
|
||||
/// If precise is true, show to two decimal places, eg
|
||||
/// eg 70 seconds -> "1.17 minutes"
|
||||
/// If false, seconds and days are shown without decimals.
|
||||
/** Describe the given seconds using the largest appropriate unit.
|
||||
If precise is true, show to two decimal places, eg
|
||||
eg 70 seconds -> "1.17 minutes"
|
||||
If false, seconds and days are shown without decimals. */
|
||||
export function timeSpan(seconds: number, short = false): string {
|
||||
const unit = naturalUnit(seconds);
|
||||
const amount = unitAmount(unit, seconds);
|
||||
|
|
|
@ -15,9 +15,9 @@ function getThemeFromRoot(): ThemeInfo {
|
|||
}
|
||||
|
||||
let setPageTheme: ((theme: ThemeInfo) => void) | null = null;
|
||||
/// The current theme that applies to this document/shadow root. When
|
||||
/// previewing cards in the card layout screen, this may not match the
|
||||
/// theme Anki is using in its UI.
|
||||
/** The current theme that applies to this document/shadow root. When
|
||||
previewing cards in the card layout screen, this may not match the
|
||||
theme Anki is using in its UI. */
|
||||
export const pageTheme = readable(getThemeFromRoot(), (set) => {
|
||||
setPageTheme = set;
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue