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:
Damien Elmes 2023-05-26 12:49:44 +10:00 committed by GitHub
parent 8d1e5c373b
commit 15dcb09036
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 365 additions and 240 deletions

View file

@ -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

View file

@ -2,10 +2,6 @@
set -e set -e
# check author has added themselves to CONTRIBUTORS
echo "--- Checking CONTRIBUTORS"
.buildkite/linux/check_contributors
echo "+++ Building and testing" echo "+++ Building and testing"
ln -sf out/node_modules . ln -sf out/node_modules .

14
Cargo.lock generated
View file

@ -222,9 +222,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.70" version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]] [[package]]
name = "apple-bundles" name = "apple-bundles"
@ -2286,6 +2286,16 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "minilints"
version = "0.0.0"
dependencies = [
"anyhow",
"camino",
"once_cell",
"walkdir",
]
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"

View file

@ -18,6 +18,7 @@ members = [
"build/runner", "build/runner",
"ftl", "ftl",
"tools/workspace-hack", "tools/workspace-hack",
"tools/minilints",
"qt/bundle/win", "qt/bundle/win",
"qt/bundle/mac", "qt/bundle/mac",
] ]

View file

@ -17,10 +17,10 @@ use ninja_gen::Build;
use ninja_gen::Result; use ninja_gen::Result;
use pylib::build_pylib; use pylib::build_pylib;
use pylib::check_pylib; use pylib::check_pylib;
use python::check_copyright;
use python::check_python; use python::check_python;
use python::setup_venv; use python::setup_venv;
use rust::build_rust; use rust::build_rust;
use rust::check_minilints;
use rust::check_rust; use rust::check_rust;
use web::build_and_check_web; use web::build_and_check_web;
use web::check_sql; use web::check_sql;
@ -52,7 +52,7 @@ fn main() -> Result<()> {
check_python(build)?; check_python(build)?;
check_proto(build)?; check_proto(build)?;
check_sql(build)?; check_sql(build)?;
check_copyright(build)?; check_minilints(build)?;
build.trailing_text = "default pylib/anki qt/aqt\n".into(); build.trailing_text = "default pylib/anki qt/aqt\n".into();

View file

@ -236,43 +236,3 @@ fn add_pylint(build: &mut Build) -> Result<()> {
Ok(()) 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(())
}

View file

@ -1,6 +1,8 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// 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
use ninja_gen::action::BuildAction;
use ninja_gen::build::FilesHandle;
use ninja_gen::cargo::CargoBuild; use ninja_gen::cargo::CargoBuild;
use ninja_gen::cargo::CargoClippy; use ninja_gen::cargo::CargoClippy;
use ninja_gen::cargo::CargoFormat; use ninja_gen::cargo::CargoFormat;
@ -9,6 +11,7 @@ use ninja_gen::cargo::CargoTest;
use ninja_gen::cargo::RustOutput; use ninja_gen::cargo::RustOutput;
use ninja_gen::git::SyncSubmodule; use ninja_gen::git::SyncSubmodule;
use ninja_gen::glob; use ninja_gen::glob;
use ninja_gen::input::BuildInput;
use ninja_gen::inputs; use ninja_gen::inputs;
use ninja_gen::Build; use ninja_gen::Build;
use ninja_gen::Result; use ninja_gen::Result;
@ -151,3 +154,57 @@ pub fn check_rust(build: &mut Build) -> Result<()> {
Ok(()) 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(())
}

View file

@ -1 +1,2 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

View file

@ -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)

View 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
View 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);
}
}

View file

@ -11,12 +11,12 @@ function nullToNegativeOne(list: (number | null)[]): number[] {
return list.map((val) => val ?? -1); return list.map((val) => val ?? -1);
} }
/// Public only for tests. /** Public only for tests. */
export function negativeOneToNull(list: number[]): (number | null)[] { export function negativeOneToNull(list: number[]): (number | null)[] {
return list.map((val) => (val === -1 ? null : val)); 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 { export class ChangeNotetypeInfoWrapper {
fields: (number | null)[]; fields: (number | null)[];
templates?: (number | null)[]; templates?: (number | null)[];
@ -33,26 +33,26 @@ export class ChangeNotetypeInfoWrapper {
this.oldNotetypeName = info.oldNotetypeName; this.oldNotetypeName = info.oldNotetypeName;
} }
/// A list with an entry for each field/template in the new notetype, with /** A list with an entry for each field/template in the new notetype, with
/// the values pointing back to indexes in the original notetype. the values pointing back to indexes in the original notetype. */
mapForContext(ctx: MapContext): (number | null)[] { mapForContext(ctx: MapContext): (number | null)[] {
return ctx == MapContext.Template ? this.templates ?? [] : this.fields; return ctx == MapContext.Template ? this.templates ?? [] : this.fields;
} }
/// Return index of old fields/templates, with null values mapped to "Nothing" /** Return index of old fields/templates, with null values mapped to "Nothing"
/// at the end. at the end.*/
getOldIndex(ctx: MapContext, newIdx: number): number { getOldIndex(ctx: MapContext, newIdx: number): number {
const map = this.mapForContext(ctx); const map = this.mapForContext(ctx);
const val = map[newIdx]; const val = map[newIdx];
return val ?? this.getOldNamesIncludingNothing(ctx).length - 1; 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[] { getOldNamesIncludingNothing(ctx: MapContext): string[] {
return [...this.getOldNames(ctx), tr.changeNotetypeNothing()]; return [...this.getOldNames(ctx), tr.changeNotetypeNothing()];
} }
/// Old names without "Nothing" at the end. /** Old names without "Nothing" at the end. */
getOldNames(ctx: MapContext): string[] { getOldNames(ctx: MapContext): string[] {
return ctx == MapContext.Template return ctx == MapContext.Template
? this.info.oldTemplateNames ? this.info.oldTemplateNames
@ -93,7 +93,7 @@ export class ChangeNotetypeInfoWrapper {
return this.info.input as Notetypes.ChangeNotetypeRequest; 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 { intoInput(): Notetypes.ChangeNotetypeRequest {
const input = this.info.input as Notetypes.ChangeNotetypeRequest; const input = this.info.input as Notetypes.ChangeNotetypeRequest;
input.newFields = nullToNegativeOne(this.fields); input.newFields = nullToNegativeOne(this.fields);

View file

@ -18,8 +18,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let input: HTMLInputElement; let input: HTMLInputElement;
let focused = false; let focused = false;
/// Set value to a new number, clamping it to a valid range, and /** Set value to a new number, clamping it to a valid range, and
/// leaving it unchanged if `newValue` is NaN. leaving it unchanged if `newValue` is NaN. */
function updateValue(newValue: number) { function updateValue(newValue: number) {
if (Number.isNaN(newValue)) { if (Number.isNaN(newValue)) {
// avoid updating the value // avoid updating the value

View file

@ -24,7 +24,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let state: DeckOptionsState; 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> { async function commitEditing(): Promise<void> {
if (document.activeElement instanceof HTMLElement) { if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur(); document.activeElement.blur();

View file

@ -21,7 +21,7 @@ export interface ParentLimits {
reviews: number; reviews: number;
} }
/// Info for showing the top selector /** Info for showing the top selector */
export interface ConfigListEntry { export interface ConfigListEntry {
idx: number; idx: number;
name: string; name: string;
@ -124,17 +124,17 @@ export class DeckOptionsState {
this.updateConfigList(); this.updateConfigList();
} }
/// Adds a new config, making it current. /** Adds a new config, making it current. */
addConfig(name: string): void { addConfig(name: string): void {
this.addConfigFrom(name, this.defaults); this.addConfigFrom(name, this.defaults);
} }
/// Clone the current config, making it current. /** Clone the current config, making it current. */
cloneConfig(name: string): void { cloneConfig(name: string): void {
this.addConfigFrom(name, this.configs[this.selectedIdx].config.config!); 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 { private addConfigFrom(name: string, source: DeckConfig.DeckConfig.IConfig): void {
const uniqueName = this.ensureNewNameUnique(name); const uniqueName = this.ensureNewNameUnique(name);
const config = DeckConfig.DeckConfig.create({ const config = DeckConfig.DeckConfig.create({
@ -158,7 +158,7 @@ export class DeckOptionsState {
return this.configs[this.selectedIdx].config.id === 1; 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 { removeCurrentConfig(): void {
const currentId = this.configs[this.selectedIdx].config.id; const currentId = this.configs[this.selectedIdx].config.id;
if (currentId === 1) { if (currentId === 1) {
@ -250,12 +250,12 @@ export class DeckOptionsState {
this.configListSetter?.(this.getConfigList()); 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 { private getCurrentConfig(): DeckConfig.DeckConfig.Config {
return cloneDeep(this.configs[this.selectedIdx].config.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> { private getCurrentAuxData(): Record<string, unknown> {
const conf = this.configs[this.selectedIdx].config.config!; const conf = this.configs[this.selectedIdx].config.config!;
return bytesToObject(conf.other); return bytesToObject(conf.other);

View file

@ -25,7 +25,7 @@ import { GraphRange } from "./graph-helpers";
import { setDataAvailable } from "./graph-helpers"; import { setDataAvailable } from "./graph-helpers";
import { hideTooltip, showTooltip } from "./tooltip"; import { hideTooltip, showTooltip } from "./tooltip";
/// 4 element array /** 4 element array */
type ButtonCounts = number[]; type ButtonCounts = number[];
export interface GraphData { export interface GraphData {

View file

@ -99,8 +99,8 @@ export type SearchDispatch = <EventKey extends Extract<keyof SearchEventMap, str
detail: SearchEventMap[EventKey], detail: SearchEventMap[EventKey],
) => void; ) => void;
/// Convert a protobuf map that protobufjs represents as an object with string /** Convert a protobuf map that protobufjs represents as an object with string
/// keys into a Map with numeric keys. keys into a Map with numeric keys. */
export function numericMap<T>(obj: { [k: string]: T }): Map<number, T> { export function numericMap<T>(obj: { [k: string]: T }): Map<number, T> {
return new Map(Object.entries(obj).map(([k, v]) => [Number(k), v])); return new Map(Object.entries(obj).map(([k, v]) => [Number(k), v]));
} }

View file

@ -76,7 +76,7 @@ function totalsForBin(bin: BinType): number[] {
return total; 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 { function cumulativeBinValue(bin: BinType, idx: number): number {
return sum(totalsForBin(bin).slice(0, idx + 1)); return sum(totalsForBin(bin).slice(0, idx + 1));
} }

View file

@ -1,15 +1,15 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// 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
/// Keep property if true. /** Keep property if true. */
type StylingPredicate = (property: string, value: string) => boolean; type StylingPredicate = (property: string, value: string) => boolean;
const keep = (_key: string, _value: string) => true; const keep = (_key: string, _value: string) => true;
const discard = (_key: string, _value: string) => false; const discard = (_key: string, _value: string) => false;
/// Return a function that filters out certain styles. /** Return a function that filters out certain styles.
/// - If the style is listed in `exceptions`, the provided predicate is used. - If the style is listed in `exceptions`, the provided predicate is used.
/// - If the style is not listed, the default predicate is used instead. - If the style is not listed, the default predicate is used instead. */
function filterStyling( function filterStyling(
defaultPredicate: StylingPredicate, defaultPredicate: StylingPredicate,
exceptions: Record<string, StylingPredicate>, exceptions: Record<string, StylingPredicate>,

View file

@ -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 { export function bridgeLink(command: string, label: string): string {
return `<a href="javascript:bridgeCommand('${command}')">${label}</a>`; return `<a href="javascript:bridgeCommand('${command}')">${label}</a>`;
} }

View file

@ -9,17 +9,17 @@ export enum CardType {
} }
export enum CardQueue { export enum CardQueue {
/// due is the order cards are shown in /** due is the order cards are shown in */
New = 0, New = 0,
/// due is a unix timestamp /** due is a unix timestamp */
Learn = 1, Learn = 1,
/// due is days since creation date /** due is days since creation date */
Review = 2, Review = 2,
DayLearn = 3, DayLearn = 3,
/// due is a unix timestamp. /** due is a unix timestamp. */
/// preview cards only placed here when failed. /** preview cards only placed here when failed. */
PreviewRepeat = 4, PreviewRepeat = 4,
/// cards are not due in these states /** cards are not due in these states */
Suspended = -1, Suspended = -1,
SchedBuried = -2, SchedBuried = -2,
UserBuried = -3, UserBuried = -3,

View file

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// 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
/// 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 = { export const HelpPage = {
DeckOptions: { DeckOptions: {
maximumInterval: "https://docs.ankiweb.net/deck-options.html#maximum-interval", maximumInterval: "https://docs.ankiweb.net/deck-options.html#maximum-interval",

View file

@ -64,8 +64,8 @@ export function localeCompare(
return first.localeCompare(second, langs, options); return first.localeCompare(second, langs, options);
} }
/// Treat text like HTML, merging multiple spaces and converting /** Treat text like HTML, merging multiple spaces and converting
/// newlines to spaces. newlines to spaces. */
export function withCollapsedWhitespace(s: string): string { export function withCollapsedWhitespace(s: string): string {
return s.replace(/\s+/g, " "); return s.replace(/\s+/g, " ");
} }

View file

@ -1,8 +1,8 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// 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
/// Add night-mode class to documentElement if hash location is #night, and /** Add night-mode class to documentElement if hash location is #night, and
/// return true if added. return true if added. */
export function checkNightMode(): boolean { export function checkNightMode(): boolean {
const nightMode = window.location.hash == "#night"; const nightMode = window.location.hash == "#night";
if (nightMode) { if (nightMode) {

View file

@ -31,8 +31,8 @@ type PackageDeprecation<T extends Record<string, unknown>> = {
[key in keyof T]?: string; [key in keyof T]?: string;
}; };
/// This can be extended to allow require() calls at runtime, for packages /** This can be extended to allow require() calls at runtime, for packages
/// that are not included at bundling time. that are not included at bundling time. */
const runtimePackages: Partial<Record<AnkiPackages, Record<string, unknown>>> = {}; const runtimePackages: Partial<Record<AnkiPackages, Record<string, unknown>>> = {};
const prohibit = () => false; const prohibit = () => false;

View file

@ -141,8 +141,8 @@ function innerShortcut(
export interface RegisterShortcutRestParams { export interface RegisterShortcutRestParams {
target: EventTarget; target: EventTarget;
/// There might be no good reason to use `keyup` other /** There might be no good reason to use `keyup` other
/// than to circumvent Qt bugs than to circumvent Qt bugs */
event: "keydown" | "keyup"; event: "keydown" | "keyup";
} }

View file

@ -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 { export function unitSeconds(unit: TimespanUnit): number {
switch (unit) { switch (unit) {
case TimespanUnit.Seconds: case TimespanUnit.Seconds:
@ -75,7 +75,7 @@ export function unitAmount(unit: TimespanUnit, secs: number): number {
return secs / unitSeconds(unit); 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 { export function naturalWholeUnit(secs: number): TimespanUnit {
let unit = naturalUnit(secs); let unit = naturalUnit(secs);
while (unit != TimespanUnit.Seconds) { while (unit != TimespanUnit.Seconds) {
@ -142,10 +142,10 @@ function i18nFuncForUnit(
} }
} }
/// Describe the given seconds using the largest appropriate unit. /** Describe the given seconds using the largest appropriate unit.
/// If precise is true, show to two decimal places, eg If precise is true, show to two decimal places, eg
/// eg 70 seconds -> "1.17 minutes" eg 70 seconds -> "1.17 minutes"
/// If false, seconds and days are shown without decimals. If false, seconds and days are shown without decimals. */
export function timeSpan(seconds: number, short = false): string { export function timeSpan(seconds: number, short = false): string {
const unit = naturalUnit(seconds); const unit = naturalUnit(seconds);
const amount = unitAmount(unit, seconds); const amount = unitAmount(unit, seconds);

View file

@ -15,9 +15,9 @@ function getThemeFromRoot(): ThemeInfo {
} }
let setPageTheme: ((theme: ThemeInfo) => void) | null = null; let setPageTheme: ((theme: ThemeInfo) => void) | null = null;
/// The current theme that applies to this document/shadow root. When /** The current theme that applies to this document/shadow root. When
/// previewing cards in the card layout screen, this may not match the previewing cards in the card layout screen, this may not match the
/// theme Anki is using in its UI. theme Anki is using in its UI. */
export const pageTheme = readable(getThemeFromRoot(), (set) => { export const pageTheme = readable(getThemeFromRoot(), (set) => {
setPageTheme = set; setPageTheme = set;
}); });