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
# 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
View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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]));
}

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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, " ");
}

View file

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

View file

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

View file

@ -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";
}

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 {
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);

View file

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