UNPKG

yanki

Version:

A CLI tool and TypeScript library for syncing Markdown to Anki flashcards.

1,628 lines (1,442 loc) 52.3 kB
/** Matches any [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive). @category Type */ type Primitive = | null | undefined | string | number | boolean | symbol | bigint; /** Returns a boolean for whether the given type is `any`. @link https://stackoverflow.com/a/49928360/1490091 Useful in type utilities, such as disallowing `any`s to be passed to a function. @example ``` import type {IsAny} from 'type-fest'; const typedObject = {a: 1, b: 2} as const; const anyObject: any = {a: 1, b: 2}; function get<O extends (IsAny<O> extends true ? {} : Record<string, number>), K extends keyof O = keyof O>(object: O, key: K) { return object[key]; } const typedA = get(typedObject, 'a'); //=> 1 const anyA = get(anyObject, 'a'); //=> any ``` @category Type Guard @category Utilities */ type IsAny<T> = 0 extends 1 & NoInfer<T> ? true : false; /** Returns a boolean for whether the given key is an optional key of type. This is useful when writing utility types or schema validators that need to differentiate `optional` keys. @example ``` import type {IsOptionalKeyOf} from 'type-fest'; type User = { name: string; surname: string; luckyNumber?: number; }; type Admin = { name: string; surname?: string; }; type T1 = IsOptionalKeyOf<User, 'luckyNumber'>; //=> true type T2 = IsOptionalKeyOf<User, 'name'>; //=> false type T3 = IsOptionalKeyOf<User, 'name' | 'luckyNumber'>; //=> boolean type T4 = IsOptionalKeyOf<User | Admin, 'name'>; //=> false type T5 = IsOptionalKeyOf<User | Admin, 'surname'>; //=> boolean ``` @category Type Guard @category Utilities */ type IsOptionalKeyOf<Type extends object, Key extends keyof Type> = IsAny<Type | Key> extends true ? never : Key extends keyof Type ? Type extends Record<Key, Type[Key]> ? false : true : false; /** Extract all optional keys from the given type. This is useful when you want to create a new type that contains different type values for the optional keys only. @example ``` import type {OptionalKeysOf, Except} from 'type-fest'; type User = { name: string; surname: string; luckyNumber?: number; }; const REMOVE_FIELD = Symbol('remove field symbol'); type UpdateOperation<Entity extends object> = Except<Partial<Entity>, OptionalKeysOf<Entity>> & { [Key in OptionalKeysOf<Entity>]?: Entity[Key] | typeof REMOVE_FIELD; }; const update1: UpdateOperation<User> = { name: 'Alice', }; const update2: UpdateOperation<User> = { name: 'Bob', luckyNumber: REMOVE_FIELD, }; ``` @category Utilities */ type OptionalKeysOf<Type extends object> = Type extends unknown // For distributing `Type` ? (keyof {[Key in keyof Type as IsOptionalKeyOf<Type, Key> extends false ? never : Key ]: never }) & keyof Type // Intersect with `keyof Type` to ensure result of `OptionalKeysOf<Type>` is always assignable to `keyof Type` : never; // Should never happen /** Extract all required keys from the given type. This is useful when you want to create a new type that contains different type values for the required keys only or use the list of keys for validation purposes, etc... @example ``` import type {RequiredKeysOf} from 'type-fest'; declare function createValidation< Entity extends object, Key extends RequiredKeysOf<Entity> = RequiredKeysOf<Entity>, >(field: Key, validator: (value: Entity[Key]) => boolean): (entity: Entity) => boolean; type User = { name: string; surname: string; luckyNumber?: number; }; const validator1 = createValidation<User>('name', value => value.length < 25); const validator2 = createValidation<User>('surname', value => value.length < 25); // @ts-expect-error const validator3 = createValidation<User>('luckyNumber', value => value > 0); // Error: Argument of type '"luckyNumber"' is not assignable to parameter of type '"name" | "surname"'. ``` @category Utilities */ type RequiredKeysOf<Type extends object> = Type extends unknown // For distributing `Type` ? Exclude<keyof Type, OptionalKeysOf<Type>> : never; // Should never happen /** Returns a boolean for whether the given type is `never`. @link https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919 @link https://stackoverflow.com/a/53984913/10292952 @link https://www.zhenghao.io/posts/ts-never Useful in type utilities, such as checking if something does not occur. @example ``` import type {IsNever, And} from 'type-fest'; type A = IsNever<never>; //=> true type B = IsNever<any>; //=> false type C = IsNever<unknown>; //=> false type D = IsNever<never[]>; //=> false type E = IsNever<object>; //=> false type F = IsNever<string>; //=> false ``` @example ``` import type {IsNever} from 'type-fest'; type IsTrue<T> = T extends true ? true : false; // When a distributive conditional is instantiated with `never`, the entire conditional results in `never`. type A = IsTrue<never>; // ^? type A = never // If you don't want that behaviour, you can explicitly add an `IsNever` check before the distributive conditional. type IsTrueFixed<T> = IsNever<T> extends true ? false : T extends true ? true : false; type B = IsTrueFixed<never>; // ^? type B = false ``` @category Type Guard @category Utilities */ type IsNever<T> = [T] extends [never] ? true : false; /** An if-else-like type that resolves depending on whether the given `boolean` type is `true` or `false`. Use-cases: - You can use this in combination with `Is*` types to create an if-else-like experience. For example, `If<IsAny<any>, 'is any', 'not any'>`. Note: - Returns a union of if branch and else branch if the given type is `boolean` or `any`. For example, `If<boolean, 'Y', 'N'>` will return `'Y' | 'N'`. - Returns the else branch if the given type is `never`. For example, `If<never, 'Y', 'N'>` will return `'N'`. @example ``` import type {If} from 'type-fest'; type A = If<true, 'yes', 'no'>; //=> 'yes' type B = If<false, 'yes', 'no'>; //=> 'no' type C = If<boolean, 'yes', 'no'>; //=> 'yes' | 'no' type D = If<any, 'yes', 'no'>; //=> 'yes' | 'no' type E = If<never, 'yes', 'no'>; //=> 'no' ``` @example ``` import type {If, IsAny, IsNever} from 'type-fest'; type A = If<IsAny<unknown>, 'is any', 'not any'>; //=> 'not any' type B = If<IsNever<never>, 'is never', 'not never'>; //=> 'is never' ``` @example ``` import type {If, IsEqual} from 'type-fest'; type IfEqual<T, U, IfBranch, ElseBranch> = If<IsEqual<T, U>, IfBranch, ElseBranch>; type A = IfEqual<string, string, 'equal', 'not equal'>; //=> 'equal' type B = IfEqual<string, number, 'equal', 'not equal'>; //=> 'not equal' ``` Note: Sometimes using the `If` type can make an implementation non–tail-recursive, which can impact performance. In such cases, it’s better to use a conditional directly. Refer to the following example: @example ``` import type {If, IsEqual, StringRepeat} from 'type-fest'; type HundredZeroes = StringRepeat<'0', 100>; // The following implementation is not tail recursive type Includes<S extends string, Char extends string> = S extends `${infer First}${infer Rest}` ? If<IsEqual<First, Char>, 'found', Includes<Rest, Char>> : 'not found'; // Hence, instantiations with long strings will fail // @ts-expect-error type Fails = Includes<HundredZeroes, '1'>; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Error: Type instantiation is excessively deep and possibly infinite. // However, if we use a simple conditional instead of `If`, the implementation becomes tail-recursive type IncludesWithoutIf<S extends string, Char extends string> = S extends `${infer First}${infer Rest}` ? IsEqual<First, Char> extends true ? 'found' : IncludesWithoutIf<Rest, Char> : 'not found'; // Now, instantiations with long strings will work type Works = IncludesWithoutIf<HundredZeroes, '1'>; //=> 'not found' ``` @category Type Guard @category Utilities */ type If<Type extends boolean, IfBranch, ElseBranch> = IsNever<Type> extends true ? ElseBranch : Type extends true ? IfBranch : ElseBranch; /** Matches any primitive, `void`, `Date`, or `RegExp` value. */ type BuiltIns = Primitive | void | Date | RegExp; /** Test if the given function has multiple call signatures. Needed to handle the case of a single call signature with properties. Multiple call signatures cannot currently be supported due to a TypeScript limitation. @see https://github.com/microsoft/TypeScript/issues/29732 */ type HasMultipleCallSignatures<T extends (...arguments_: any[]) => unknown> = T extends {(...arguments_: infer A): unknown; (...arguments_: infer B): unknown} ? B extends A ? A extends B ? false : true : true : false; /** Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability. @example ``` import type {Simplify} from 'type-fest'; type PositionProps = { top: number; left: number; }; type SizeProps = { width: number; height: number; }; // In your editor, hovering over `Props` will show a flattened object with all the properties. type Props = Simplify<PositionProps & SizeProps>; ``` Sometimes it is desired to pass a value as a function argument that has a different type. At first inspection it may seem assignable, and then you discover it is not because the `value`'s type definition was defined as an interface. In the following example, `fn` requires an argument of type `Record<string, unknown>`. If the value is defined as a literal, then it is assignable. And if the `value` is defined as type using the `Simplify` utility the value is assignable. But if the `value` is defined as an interface, it is not assignable because the interface is not sealed and elsewhere a non-string property could be added to the interface. If the type definition must be an interface (perhaps it was defined in a third-party npm package), then the `value` can be defined as `const value: Simplify<SomeInterface> = ...`. Then `value` will be assignable to the `fn` argument. Or the `value` can be cast as `Simplify<SomeInterface>` if you can't re-declare the `value`. @example ``` import type {Simplify} from 'type-fest'; interface SomeInterface { foo: number; bar?: string; baz: number | undefined; } type SomeType = { foo: number; bar?: string; baz: number | undefined; }; const literal = {foo: 123, bar: 'hello', baz: 456}; const someType: SomeType = literal; const someInterface: SomeInterface = literal; declare function fn(object: Record<string, unknown>): void; fn(literal); // Good: literal object type is sealed fn(someType); // Good: type is sealed // @ts-expect-error fn(someInterface); // Error: Index signature for type 'string' is missing in type 'someInterface'. Because `interface` can be re-opened fn(someInterface as Simplify<SomeInterface>); // Good: transform an `interface` into a `type` ``` @link https://github.com/microsoft/TypeScript/issues/15300 @see {@link SimplifyDeep} @category Object */ type Simplify<T> = {[KeyType in keyof T]: T[KeyType]} & {}; /** Omit any index signatures from the given object type, leaving only explicitly defined properties. This is the counterpart of `PickIndexSignature`. Use-cases: - Remove overly permissive signatures from third-party types. This type was taken from this [StackOverflow answer](https://stackoverflow.com/a/68261113/420747). It relies on the fact that an empty object (`{}`) is assignable to an object with just an index signature, like `Record<string, unknown>`, but not to an object with explicitly defined keys, like `Record<'foo' | 'bar', unknown>`. (The actual value type, `unknown`, is irrelevant and could be any type. Only the key type matters.) ``` const indexed: Record<string, unknown> = {}; // Allowed // @ts-expect-error const keyed: Record<'foo', unknown> = {}; // Error // => TS2739: Type '{}' is missing the following properties from type 'Record<"foo" | "bar", unknown>': foo, bar ``` Instead of causing a type error like the above, you can also use a [conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) to test whether a type is assignable to another: ``` type Indexed = {} extends Record<string, unknown> ? '✅ `{}` is assignable to `Record<string, unknown>`' : '❌ `{}` is NOT assignable to `Record<string, unknown>`'; // => '✅ `{}` is assignable to `Record<string, unknown>`' type Keyed = {} extends Record<'foo' | 'bar', unknown> ? '✅ `{}` is assignable to `Record<\'foo\' | \'bar\', unknown>`' : '❌ `{}` is NOT assignable to `Record<\'foo\' | \'bar\', unknown>`'; // => "❌ `{}` is NOT assignable to `Record<'foo' | 'bar', unknown>`" ``` Using a [mapped type](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#further-exploration), you can then check for each `KeyType` of `ObjectType`... ``` type OmitIndexSignature<ObjectType> = { [KeyType in keyof ObjectType // Map each key of `ObjectType`... ]: ObjectType[KeyType]; // ...to its original value, i.e. `OmitIndexSignature<Foo> == Foo`. }; ``` ...whether an empty object (`{}`) would be assignable to an object with that `KeyType` (`Record<KeyType, unknown>`)... ``` type OmitIndexSignature<ObjectType> = { [KeyType in keyof ObjectType // Is `{}` assignable to `Record<KeyType, unknown>`? as {} extends Record<KeyType, unknown> ? never // ✅ `{}` is assignable to `Record<KeyType, unknown>` : KeyType // ❌ `{}` is NOT assignable to `Record<KeyType, unknown>` ]: ObjectType[KeyType]; }; ``` If `{}` is assignable, it means that `KeyType` is an index signature and we want to remove it. If it is not assignable, `KeyType` is a "real" key and we want to keep it. @example ``` import type {OmitIndexSignature} from 'type-fest'; type Example = { // These index signatures will be removed. [x: string]: any; [x: number]: any; [x: symbol]: any; [x: `head-${string}`]: string; [x: `${string}-tail`]: string; [x: `head-${string}-tail`]: string; [x: `${bigint}`]: string; [x: `embedded-${number}`]: string; // These explicitly defined keys will remain. foo: 'bar'; qux?: 'baz'; }; type ExampleWithoutIndexSignatures = OmitIndexSignature<Example>; // => { foo: 'bar'; qux?: 'baz' | undefined; } ``` @see {@link PickIndexSignature} @category Object */ type OmitIndexSignature<ObjectType> = { [KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? never : KeyType]: ObjectType[KeyType]; }; /** Pick only index signatures from the given object type, leaving out all explicitly defined properties. This is the counterpart of `OmitIndexSignature`. @example ``` import type {PickIndexSignature} from 'type-fest'; declare const symbolKey: unique symbol; type Example = { // These index signatures will remain. [x: string]: unknown; [x: number]: unknown; [x: symbol]: unknown; [x: `head-${string}`]: string; [x: `${string}-tail`]: string; [x: `head-${string}-tail`]: string; [x: `${bigint}`]: string; [x: `embedded-${number}`]: string; // These explicitly defined keys will be removed. ['kebab-case-key']: string; [symbolKey]: string; foo: 'bar'; qux?: 'baz'; }; type ExampleIndexSignature = PickIndexSignature<Example>; // { // [x: string]: unknown; // [x: number]: unknown; // [x: symbol]: unknown; // [x: `head-${string}`]: string; // [x: `${string}-tail`]: string; // [x: `head-${string}-tail`]: string; // [x: `${bigint}`]: string; // [x: `embedded-${number}`]: string; // } ``` @see {@link OmitIndexSignature} @category Object */ type PickIndexSignature<ObjectType> = { [KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? KeyType : never]: ObjectType[KeyType]; }; // Merges two objects without worrying about index signatures. type SimpleMerge<Destination, Source> = { [Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key]; } & Source; /** Merge two types into a new type. Keys of the second type overrides keys of the first type. @example ``` import type {Merge} from 'type-fest'; type Foo = { [x: string]: unknown; [x: number]: unknown; foo: string; bar: symbol; }; type Bar = { [x: number]: number; [x: symbol]: unknown; bar: Date; baz: boolean; }; export type FooBar = Merge<Foo, Bar>; // => { // [x: string]: unknown; // [x: number]: number; // [x: symbol]: unknown; // foo: string; // bar: Date; // baz: boolean; // } ``` @category Object */ type Merge<Destination, Source> = Simplify< SimpleMerge<PickIndexSignature<Destination>, PickIndexSignature<Source>> & SimpleMerge<OmitIndexSignature<Destination>, OmitIndexSignature<Source>> >; /** Merges user specified options with default options. @example ``` type PathsOptions = {maxRecursionDepth?: number; leavesOnly?: boolean}; type DefaultPathsOptions = {maxRecursionDepth: 10; leavesOnly: false}; type SpecifiedOptions = {leavesOnly: true}; type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOptions>; //=> {maxRecursionDepth: 10; leavesOnly: true} ``` @example ``` // Complains if default values are not provided for optional options type PathsOptions = {maxRecursionDepth?: number; leavesOnly?: boolean}; type DefaultPathsOptions = {maxRecursionDepth: 10}; type SpecifiedOptions = {}; type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOptions>; // ~~~~~~~~~~~~~~~~~~~ // Property 'leavesOnly' is missing in type 'DefaultPathsOptions' but required in type '{ maxRecursionDepth: number; leavesOnly: boolean; }'. ``` @example ``` // Complains if an option's default type does not conform to the expected type type PathsOptions = {maxRecursionDepth?: number; leavesOnly?: boolean}; type DefaultPathsOptions = {maxRecursionDepth: 10; leavesOnly: 'no'}; type SpecifiedOptions = {}; type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOptions>; // ~~~~~~~~~~~~~~~~~~~ // Types of property 'leavesOnly' are incompatible. Type 'string' is not assignable to type 'boolean'. ``` @example ``` // Complains if an option's specified type does not conform to the expected type type PathsOptions = {maxRecursionDepth?: number; leavesOnly?: boolean}; type DefaultPathsOptions = {maxRecursionDepth: 10; leavesOnly: false}; type SpecifiedOptions = {leavesOnly: 'yes'}; type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOptions>; // ~~~~~~~~~~~~~~~~ // Types of property 'leavesOnly' are incompatible. Type 'string' is not assignable to type 'boolean'. ``` */ type ApplyDefaultOptions< Options extends object, Defaults extends Simplify<Omit<Required<Options>, RequiredKeysOf<Options>> & Partial<Record<RequiredKeysOf<Options>, never>>>, SpecifiedOptions extends Options, > = If<IsAny<SpecifiedOptions>, Defaults, If<IsNever<SpecifiedOptions>, Defaults, Simplify<Merge<Defaults, { [Key in keyof SpecifiedOptions as Key extends OptionalKeysOf<Options> ? undefined extends SpecifiedOptions[Key] ? never : Key : Key ]: SpecifiedOptions[Key] }> & Required<Options>>>>; /** @see {@link PartialDeep} */ type PartialDeepOptions = { /** Whether to affect the individual elements of arrays and tuples. @default false */ readonly recurseIntoArrays?: boolean; /** Allows `undefined` values in non-tuple arrays. - When set to `true`, elements of non-tuple arrays can be `undefined`. - When set to `false`, only explicitly defined elements are allowed in non-tuple arrays, ensuring stricter type checking. @default false @example You can allow `undefined` values in non-tuple arrays by passing `{recurseIntoArrays: true; allowUndefinedInNonTupleArrays: true}` as the second type argument: ``` import type {PartialDeep} from 'type-fest'; type Settings = { languages: string[]; }; declare const partialSettings: PartialDeep<Settings, {recurseIntoArrays: true; allowUndefinedInNonTupleArrays: true}>; partialSettings.languages = [undefined]; // OK ``` */ readonly allowUndefinedInNonTupleArrays?: boolean; }; type DefaultPartialDeepOptions = { recurseIntoArrays: false; allowUndefinedInNonTupleArrays: false; }; /** Create a type from another type with all keys and nested keys set to optional. Use-cases: - Merging a default settings/config object with another object, the second object would be a deep partial of the default object. - Mocking and testing complex entities, where populating an entire object with its keys would be redundant in terms of the mock or test. @example ``` import type {PartialDeep} from 'type-fest'; let settings = { textEditor: { fontSize: 14, fontColor: '#000000', fontWeight: 400, }, autocomplete: false, autosave: true, }; const applySavedSettings = (savedSettings: PartialDeep<typeof settings>) => ( {...settings, ...savedSettings, textEditor: {...settings.textEditor, ...savedSettings.textEditor}} ); settings = applySavedSettings({textEditor: {fontWeight: 500}}); ``` By default, this does not affect elements in array and tuple types. You can change this by passing `{recurseIntoArrays: true}` as the second type argument: ``` import type {PartialDeep} from 'type-fest'; type Shape = { dimensions: [number, number]; }; const partialShape: PartialDeep<Shape, {recurseIntoArrays: true}> = { dimensions: [], // OK }; partialShape.dimensions = [15]; // OK ``` @see {@link PartialDeepOptions} @category Object @category Array @category Set @category Map */ type PartialDeep<T, Options extends PartialDeepOptions = {}> = _PartialDeep<T, ApplyDefaultOptions<PartialDeepOptions, DefaultPartialDeepOptions, Options>>; type _PartialDeep<T, Options extends Required<PartialDeepOptions>> = T extends BuiltIns | ((new (...arguments_: any[]) => unknown)) ? T : T extends Map<infer KeyType, infer ValueType> ? PartialMapDeep<KeyType, ValueType, Options> : T extends Set<infer ItemType> ? PartialSetDeep<ItemType, Options> : T extends ReadonlyMap<infer KeyType, infer ValueType> ? PartialReadonlyMapDeep<KeyType, ValueType, Options> : T extends ReadonlySet<infer ItemType> ? PartialReadonlySetDeep<ItemType, Options> : T extends (...arguments_: any[]) => unknown ? IsNever<keyof T> extends true ? T // For functions with no properties : HasMultipleCallSignatures<T> extends true ? T : ((...arguments_: Parameters<T>) => ReturnType<T>) & PartialObjectDeep<T, Options> : T extends object ? T extends ReadonlyArray<infer ItemType> // Test for arrays/tuples, per https://github.com/microsoft/TypeScript/issues/35156 ? Options['recurseIntoArrays'] extends true ? ItemType[] extends T // Test for arrays (non-tuples) specifically ? readonly ItemType[] extends T // Differentiate readonly and mutable arrays ? ReadonlyArray<_PartialDeep<Options['allowUndefinedInNonTupleArrays'] extends false ? ItemType : ItemType | undefined, Options>> : Array<_PartialDeep<Options['allowUndefinedInNonTupleArrays'] extends false ? ItemType : ItemType | undefined, Options>> : PartialObjectDeep<T, Options> // Tuples behave properly : T // If they don't opt into array testing, just use the original type : PartialObjectDeep<T, Options> : unknown; /** Same as `PartialDeep`, but accepts only `Map`s and as inputs. Internal helper for `PartialDeep`. */ type PartialMapDeep<KeyType, ValueType, Options extends Required<PartialDeepOptions>> = {} & Map<_PartialDeep<KeyType, Options>, _PartialDeep<ValueType, Options>>; /** Same as `PartialDeep`, but accepts only `Set`s as inputs. Internal helper for `PartialDeep`. */ type PartialSetDeep<T, Options extends Required<PartialDeepOptions>> = {} & Set<_PartialDeep<T, Options>>; /** Same as `PartialDeep`, but accepts only `ReadonlyMap`s as inputs. Internal helper for `PartialDeep`. */ type PartialReadonlyMapDeep<KeyType, ValueType, Options extends Required<PartialDeepOptions>> = {} & ReadonlyMap<_PartialDeep<KeyType, Options>, _PartialDeep<ValueType, Options>>; /** Same as `PartialDeep`, but accepts only `ReadonlySet`s as inputs. Internal helper for `PartialDeep`. */ type PartialReadonlySetDeep<T, Options extends Required<PartialDeepOptions>> = {} & ReadonlySet<_PartialDeep<T, Options>>; /** Same as `PartialDeep`, but accepts only `object`s as inputs. Internal helper for `PartialDeep`. */ type PartialObjectDeep<ObjectType extends object, Options extends Required<PartialDeepOptions>> = { [KeyType in keyof ObjectType]?: _PartialDeep<ObjectType[KeyType], Options> }; //#region src/types/deck.d.ts type DeckStats = { deck_id: number; learn_count: number; name: string; new_count: number; review_count: number; total_in_deck: number; }; type DeckConfig = { autoplay: boolean; dyn: 1 | false; id: number; lapse: { delays: number[]; leechAction: number; leechFails: number; minInt: number; mult: number; }; maxTaken: number; mod: number; name: string; new: { bury: boolean; delays: number[]; initialFactor: number; ints: number[]; order: number; perDay: number; separate: boolean; }; replayq: boolean; rev: { bury: boolean; ease4: number; fuzz: number; ivlFct: number; maxIvl: number; minSpace: number; perDay: number; }; timer: number; usn: number; }; type DeckRequests = Request<'changeDeck', 6, { cards: number[]; deck: string; }> | Request<'cloneDeckConfigId', 6, { cloneFrom: number; name: string; }, false | number> | Request<'createDeck', 6, { deck: string; }, number> | Request<'deckNames', 6, never, string[]> | Request<'deckNamesAndIds', 6, never, Record<string, number>> | Request<'deleteDecks', 6, { cardsToo: true; decks: string[]; }> | Request<'getDeckConfig', 6, { deck: string; }, DeckConfig> | Request<'getDecks', 6, Record<'cards', number[]>, Record<string, number[]>> | Request<'getDeckStats', 6, { decks: string[]; }, Record<string, DeckStats>> | Request<'removeDeckConfigId', 6, { configId: number; }, boolean> | Request<'saveDeckConfig', 6, { config: DeckConfig; }, boolean> | Request<'setDeckConfigId', 6, { configId: number; decks: string[]; }, boolean>; //#endregion //#region src/types/note.d.ts type NoteModel = 'Basic' | 'Basic (and reversed card)' | 'Basic (type in the answer)' | 'Cloze' | (string & {}); type NoteMedia = { data?: string; fields?: string[]; filename: string; path?: string; skipHash?: string; url?: string; }; type Note = { audio?: NoteMedia[]; deckName: string; fields: Record<string, string>; modelName: NoteModel; picture?: NoteMedia[]; tags?: string[]; video?: NoteMedia[]; }; type NoteWithCreationOptions = Note & { options?: { allowDuplicate?: boolean; duplicateScope?: 'deck' | (string & {}); duplicateScopeOptions?: { checkAllModels?: boolean; checkChildren?: boolean; deckName?: null | string; }; }; }; type NoteRequests = Request<'addNote', 6, { note: NoteWithCreationOptions; }, null | number> | Request<'addNotes', 6, { notes: NoteWithCreationOptions[]; }, Array<null | string> | null> | Request<'addTags', 6, { notes: number[]; tags: string; }> | Request<'canAddNotes', 6, { notes: NoteWithCreationOptions[]; }, boolean[]> | Request<'canAddNotesWithErrorDetail', 6, { notes: NoteWithCreationOptions[]; }, Array<{ canAdd: false; error: string; } | { canAdd: true; }>> | Request<'clearUnusedTags', 6, never, string[]> | Request<'deleteNotes', 6, { notes: number[]; }> | Request<'findNotes', 6, { query: string; }, number[]> | Request<'getNoteTags', 6, { note: number; }, string[]> | Request<'getTags', 6, never, string[]> | Request<'notesInfo', 6, { notes: number[]; }, Array<{ cards: number[]; fields: Record<string, { order: number; value: string; }>; mod: number; modelName: string; noteId: number; profile: string; tags: string[]; }>> | Request<'notesModTime', 6, { notes: number[]; }, Array<{ mod: number; noteId: number; }>> | Request<'removeEmptyNotes', 6> | Request<'removeTags', 6, { notes: number[]; tags: string; }> | Request<'replaceTags', 6, { notes: number[]; replace_with_tag: string; tag_to_replace: string; }> | Request<'replaceTagsInAllNotes', 6, { replace_with_tag: string; tag_to_replace: string; }> | Request<'updateNote', 6, { note: { audio?: NoteMedia[]; fields: Record<string, string>; id: number; picture?: NoteMedia[]; tags?: string[]; video?: NoteMedia[]; } | { fields?: Record<string, string>; id: number; tags: string[]; }; }> | Request<'updateNoteFields', 6, { note: { audio?: NoteMedia[]; fields: Record<string, string>; id: number; picture?: NoteMedia[]; video?: NoteMedia[]; }; }> | Request<'updateNoteModel', 6, { note: { fields: Record<string, string>; id: number; modelName: string; tags: string[]; }; }> | Request<'updateNoteTags', 6, { note: number; tags: string[]; }>; //#endregion //#region src/types/graphical.d.ts type GraphicalRequests = Request<'guiAddCards', 6, { note: Note; }, number> | Request<'guiAnswerCard', 6, { ease: number; }, boolean> | Request<'guiBrowse', 6, { query: string; reorderCards?: { columnId: CardBrowserColumns; order: 'ascending' | 'descending'; }; }, number[]> | Request<'guiCheckDatabase', 6, never, true> | Request<'guiCurrentCard', 6, never, CardInfo | null> | Request<'guiDeckBrowser', 6> | Request<'guiDeckOverview', 6, { name: string; }, boolean> | Request<'guiDeckReview', 6, { name: string; }, boolean> | Request<'guiEditNote', 6, { note: number; }> | Request<'guiExitAnki', 6> | Request<'guiImportFile', 6, { path: string; }> | Request<'guiPlayAudio', 6, never, true> | Request<'guiSelectCard', 6, { card: number; }, boolean> | Request<'guiSelectedNotes', 6, never, number[]> | Request<'guiSelectNote', 6, { note: number; }, boolean> | Request<'guiShowAnswer', 6, never, boolean> | Request<'guiShowQuestion', 6, never, boolean> | Request<'guiStartCardTimer', 6, never, true> | Request<'guiUndo', 6, never, boolean>; //#endregion //#region src/types/media.d.ts type MediaRequests = Request<'deleteMediaFile', 6, { filename: string; }> | Request<'getMediaDirPath', 6, never, string> | Request<'getMediaFilesNames', 6, { pattern: string; }, string[]> | Request<'retrieveMediaFile', 6, { filename: string; }, false | string> | Request<'storeMediaFile', 6, { data?: string; deleteExisting?: boolean; filename: string; path?: string; url?: string; }, string>; //#endregion //#region src/types/miscellaneous.d.ts type MiscellaneousRequests = Request<'apiReflect', 6, { actions: null | string[]; scopes: Array<'actions'>; }, { actions: string[]; scopes: string[]; }> | Request<'exportPackage', 6, { deck: string; includeSched?: boolean; path: string; }, boolean> | Request<'getActiveProfile', 6, never, string> | Request<'getProfiles', 6, never, string[]> | Request<'importPackage', 6, { path: string; }, boolean> | Request<'loadProfile', 6, { name: string; }, true> | Request<'multi', 6, // Crazy, have to call this experimental { actions: Array<{ action: Requests['action']; params?: Requests['params']; version?: number; }>; }, Array<Requests['response'] | { error: null | string; result: Requests['response']; }>> | Request<'reloadCollection', 6> | Request<'requestPermission', 6, never, { permission: 'denied'; } | { permission: 'granted'; requireApiKey: boolean; version: boolean; }> | Request<'sync', 6> | Request<'version', 6, never, number>; //#endregion //#region src/types/model.d.ts type ModelField = { collapsed: boolean; description: string; excludeFromSearch: boolean; font: string; id: number; name: string; ord: number; plainText: boolean; preventDeletion: boolean; rtl: boolean; size: number; sticky: boolean; tag: null; }; type ModelTemplate = { afmt: string; bafmt: string; bfont: string; bqfmt: string; bsize: number; did: null; id: number; name: string; ord: number; qfmt: string; }; type Model = { css: string; did: null; flds: ModelField[]; id: number; latexPost: string; latexPre: string; latexsvg: boolean; mod: number; name: string; originalStockKind: number; req: Array<[number, string, number[]]>; sortf: number; tmpls: ModelTemplate[]; type: number; usn: number; }; type ModelToCreate = { cardTemplates: Array<{ [key: string]: string; Back: string; Front: string; }>; css?: string; inOrderFields: string[]; isCloze?: boolean; modelName: string; }; type ModelRequests = Request<'createModel', 6, ModelToCreate, Model> | Request<'findAndReplaceInModels', 6, { model: { back: boolean; css: boolean; fieldText: string; front: boolean; modelName: string; replaceText: string; }; }, number> | Request<'findModelsById', 6, { modelIds: number[]; }, Model[]> | Request<'findModelsByName', 6, { modelNames: string[]; }, Model[]> | Request<'modelFieldAdd', 6, { fieldName: string; index: number; modelName: string; }> | Request<'modelFieldDescriptions', 6, { description: string; fieldName: string; modelName: string; }, boolean> | Request<'modelFieldFonts', 6, { modelName: string; }, Record<string, { font: string; size: number; }>> | Request<'modelFieldNames', 6, { modelName: string; }, string[]> | Request<'modelFieldRemove', 6, { fieldName: string; modelName: string; }> | Request<'modelFieldRename', 6, { modelName: string; newFieldName: string; oldFieldName: string; }> | Request<'modelFieldReposition', 6, { fieldName: string; index: number; modelName: string; }> | Request<'modelFieldSetDescription', 6, { fieldName: string; index: number; modelName: string; }> | Request<'modelFieldSetFont', 6, { fieldName: string; font: string; modelName: string; }> | Request<'modelFieldSetFontSize', 6, { fieldName: string; fontSize: number; modelName: string; }> | Request<'modelFieldsOnTemplates', 6, { modelName: string; }, Record<string, [string[], string[]]>> | Request<'modelNames', 6, never, string[]> | Request<'modelNamesAndIds', 6, never, Record<string, number>> | Request<'modelStyling', 6, { modelName: string; }, { css: string; }> | Request<'modelTemplateAdd', 6, { modelName: string; template: { [key: string]: string; Back: string; Front: string; }; }> | Request<'modelTemplateRemove', 6, { modelName: string; templateName: string; }> | Request<'modelTemplateRename', 6, { modelName: string; newTemplateName: string; oldTemplateName: string; }> | Request<'modelTemplateReposition', 6, { index: number; modelName: string; templateName: string; }> | Request<'modelTemplates', 6, { modelName: string; }, Record<string, { [key: string]: string; Back: string; Front: string; }>> | Request<'updateModelStyling', 6, { model: { css: string; name: string; }; }> | Request<'updateModelTemplates', 6, { model: { name: string; templates: Record<string, { Back?: string; Front?: string; }>; }; }>; //#endregion //#region src/types/statistic.d.ts type ReviewStatisticTuple = [reviewTime: number, cardID: number, usn: number, buttonPressed: number, newInterval: number, previousInterval: number, newFactor: number, reviewDuration: number, reviewType: number]; type StatisticRequests = Request<'cardReviews', 6, { deck: string; startID: number; }, ReviewStatisticTuple[]> | Request<'getCollectionStatsHTML', 6, { wholeCollection: boolean; }, string> | Request<'getLatestReviewID', 6, { deck: string; }, number> | Request<'getNumCardsReviewedByDay', 6, never, Array<[string, number]>> | Request<'getNumCardsReviewedToday', 6, never, number> | Request<'getReviewsOfCards', 6, { cards: string[]; }, Record<string, Array<{ /** ButtonPressed */ ease: number; /** NewFactor */ factor: number; /** ReviewTime */ id: number; /** NewInterval */ ivl: number; /** PreviousInterval */ lastIvl: number; /** ReviewDuration */ time: number; /** ReviewType */ type: number; /** Usn */ usn: number; }>>> | Request<'insertReviews', 6, { reviews: ReviewStatisticTuple[]; }>; //#endregion //#region src/types/shared.d.ts /** * Abstract wrapper over an Anki Connect action / response */ type Request<Action extends string, Version extends AnkiConnectVersion, Params = never, Result = null> = { action: Action; params: Params; response: { error: null | string; result: Result; }; version: Version; }; /** * Requests */ type Requests = CardRequests | DeckRequests | GraphicalRequests | MediaRequests | MiscellaneousRequests | ModelRequests | NoteRequests | StatisticRequests; type AnkiConnectVersion = 6; type ParamsForAction<T extends Requests['action']> = Extract<Requests, { action: T; }>['params']; //#endregion //#region src/types/card.d.ts type CardBrowserColumns = 'answer' | 'cardDue' | 'cardEase' | 'cardIvl' | 'cardLapses' | 'cardMod' | 'cardReps' | 'deck' | 'note' | 'noteCrt' | 'noteFld' | 'noteMod' | 'noteTags' | 'question' | 'template' | (string & {}); type CardValueKeys = 'data' | 'did' | 'due' | 'factor' | 'flags' | 'id' | 'ivl' | 'lapses' | 'left' | 'mod' | 'odid' | 'odue' | 'ord' | 'queue' | 'reps' | 'type' | 'usn'; type CardInfo = { answer: string; buttons?: number[]; cardId: number; css: string; deckName: string; due: number; fieldOrder: number; fields: Record<string, { order: number; value: string; }>; interval: number; lapses: number; left: number; mod: number; modelName: string; nextReviews: string[]; note: number; ord: number; question: string; queue: number; reps: number; template: string; type: number; }; type CardRequests = Request<'answerCards', 6, { answers: Array<{ cardId: number; ease: number; }>; }, boolean[]> | Request<'areDue', 6, { cards: number[]; }, boolean[]> | Request<'areSuspended', 6, { cards: number[]; }, Array<boolean | null>> | Request<'cardsInfo', 6, { cards: number[]; }, CardInfo[]> | Request<'cardsModTime', 6, { cards: number[]; }, { cardId: number; mod: number; }> | Request<'cardsToNotes', 6, { cards: number[]; }, number[]> | Request<'findCards', 6, { query: string; }, number[]> | Request<'forgetCards', 6, { cards: number[]; }> | Request<'getEaseFactors', 6, { cards: number[]; }, number[]> | Request<'getIntervals', 6, { cards: number[]; complete?: boolean; }, number[] | number[][]> | Request<'relearnCards', 6, { cards: number[]; }> | Request<'setDueDate', 6, { cards: number[]; days: string; }, boolean> | Request<'setEaseFactors', 6, { cards: number[]; easeFactors: number[]; }, boolean[]> | Request<'setSpecificValueOfCard', 6, { card: number; keys: CardValueKeys[]; newValues: string[]; }, boolean[]> | Request<'suspend', 6, { cards: number[]; }, boolean> | Request<'suspended', 6, { card: number; }, boolean> | Request<'unsuspend', 6, { cards: number[]; }, boolean>; //#endregion //#region src/client.d.ts /** * Subset of built-in Fetch interface that's actually used by Anki, for ease of * external re-implementation when passing a custom fetch function to * YankiClient. */ type YankiFetchAdapter = (input: string, init?: { body?: string; headers?: Record<string, string>; method?: string; mode?: RequestMode; }) => Promise<undefined | { headers: Headers | Record<string, string>; json(): Promise<any>; status: number; }>; /** Optional options to pass when instantiating a new YankiConnect instance. */ type YankiConnectOptions = { /** * Attempt to open the desktop Anki.app if it's not already running. * * - `true` will always attempt to open Anki _when a request is made_. This * might introduce significant latency on the first launch. * - `false` will never attempt to open Anki. Requests will fail until * something or someone else opens the Anki app. * - `immediately` is a special option that will open Anki when the client is * instantiated. * * The Anki desktop app must be running for the client and the underlying * Anki-Connect service to work. * * Currently supported on macOS only. * * The client does not attempt to close the app. * @default false */ autoLaunch: 'immediately' | boolean; /** * Advanced option to customize the resource fetch implementation used to make requests to Anki-Connect. * * Note that the signature reflects the subset of the built-in Fetch interface that's actually used by yanki-connect. * * The exact signature of this option is subject to change in the future. * @default fetch */ fetchAdapter: undefined | YankiFetchAdapter; /** * Host where the Anki-Connect service is running. * @default 'http://127.0.0.1' */ host: string; /** * Anki-Connect security key (optional) * @default undefined */ key: string | undefined; /** * Port where the Anki-Connect service is running. * @default 8765 */ port: number; /** * Anki-Connect API version. * * Only API version 6 is supported for now. * @default 6 */ version: AnkiConnectVersion; }; declare const yankiModels: [{ readonly cardTemplates: [{ readonly Back: "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}"; readonly Front: "{{Front}}"; readonly YankiNamespace: "{{YankiNamespace}}"; }]; readonly inOrderFields: ["Front", "Back", "YankiNamespace"]; readonly modelName: "Yanki - Basic"; }, { readonly cardTemplates: [{ readonly Back: "{{cloze:Front}}<br>\n{{Back}}"; readonly Front: "{{cloze:Front}}"; readonly YankiNamespace: "{{YankiNamespace}}"; }]; readonly inOrderFields: ["Front", "Back", "YankiNamespace"]; readonly isCloze: true; readonly modelName: "Yanki - Cloze"; }, { readonly cardTemplates: [{ readonly Back: "{{Front}}\n\n<hr id=answer>\n\n{{type:Back}}"; readonly Front: "{{Front}}\n\n{{type:Back}}"; readonly YankiNamespace: "{{YankiNamespace}}"; }]; readonly inOrderFields: ["Front", "Back", "YankiNamespace"]; readonly modelName: "Yanki - Basic (type in the answer)"; }, { readonly cardTemplates: [{ readonly Back: "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}{{#Extra}}\n\n<hr>\n\n{{Extra}}{{/Extra}}"; readonly Front: "{{Front}}"; readonly YankiNamespace: "{{YankiNamespace}}"; }, { readonly Back: "{{FrontSide}}\n\n<hr id=answer>\n\n{{Front}}{{#Extra}}\n\n<hr>\n\n{{Extra}}{{/Extra}}"; readonly Front: "{{Back}}"; readonly YankiNamespace: "{{YankiNamespace}}"; }]; readonly inOrderFields: ["Front", "Back", "Extra", "YankiNamespace"]; readonly modelName: "Yanki - Basic (and reversed card with extra)"; }]; type YankiModelName = (typeof yankiModels)[number]['modelName']; type YankiNote = Simplify<Omit<ParamsForAction<'addNote'>['note'], 'fields' | 'modelName' | 'options'> & { cards?: number[]; fields: { Back: string; Extra?: string; Front: string; YankiNamespace: string; }; modelName: YankiModelName; noteId: number | undefined; }>; type FetchAdapter = YankiFetchAdapter; type ManageFilenames = 'off' | 'prompt' | 'response'; type SyncMediaAssets = 'all' | 'local' | 'off' | 'remote'; type FileAdapter = { readFile(filePath: string): Promise<string>; readFileBuffer(filePath: string): Promise<Uint8Array>; rename(oldPath: string, newPath: string): Promise<void>; stat(filePath: string): Promise<{ ctimeMs: number; mtimeMs: number; size: number; }>; writeFile(filePath: string, data: string): Promise<void>; }; type GlobalOptions = { /** * Used for wiki link resolution */ allFilePaths: string[]; ankiConnectOptions: YankiConnectOptions; /** * Automatically sync any changes to AnkiWeb after Yanki has finished syncing * locally. If false, only local Anki data is updated and you must manually * invoke a sync to AnkiWeb. This is the equivalent of pushing the "sync" * button in the Anki app. */ ankiWeb: boolean; /** Override where "/" should resolve to... useful in Obsidian to set the vault path as the "root" */ basePath: string | undefined; /** Run Anki's "Check Database" command after sync updates that might produce card corruption */ checkDatabase: boolean; cwd: string; dryRun: boolean; /** * Exposed for Obsidian, currently only used for getting URL content hashes * and inferring MIME types of URLs without extensions. * Note that ankiConnectOptions ALSO has a fetch adapter option specifically * for communicating with Anki-Connect. */ fetchAdapter: FetchAdapter | undefined; fileAdapter: FileAdapter | undefined; manageFilenames: ManageFilenames; /** Only applies if manageFilenames is `true`. Will _not_ truncate user-specified file names in other cases. */ maxFilenameLength: number; namespace: string; /** Ensures that wiki-style links work correctly */ obsidianVault: string | undefined; /** Exposed for testing only */ resolveUrls: boolean; /** * Whether to treat single newlines in Markdown as line breaks in the * resulting HTML (Obsidian has an application-level setting for this) */ strictLineBreaks: boolean; /** Only consider exact noteId matches between the local and remote copies to be equivalent, don't match local notes with "orphaned" remote notes based on content */ strictMatching: boolean; /** Sync image, video, and audio assets to Anki's media storage system */ syncMediaAssets: SyncMediaAssets; }; type CleanOptions = Pick<GlobalOptions, 'ankiConnectOptions' | 'ankiWeb' | 'dryRun' | 'namespace'>; declare const defaultCleanOptions: CleanOptions; type CleanResult = Simplify<Pick<GlobalOptions, 'ankiWeb' | 'dryRun' | 'namespace'> & { deletedDecks: string[]; deletedMedia: string[]; deletedNotes: YankiNote[]; duration: number; }>; /** * Deletes all remote notes in Anki associated with the given namespace. * * Use with significant caution. Mostly useful for testing. * @returns The IDs of the notes that were deleted * @throws {Error} If Anki is unreachable or another error occurs during deletion. */ declare function cleanNotes(options?: PartialDeep<CleanOptions>): Promise<CleanResult>; declare function formatCleanResult(result: CleanResult, verbose?: boolean): string; type ListOptions = Pick<GlobalOptions, 'ankiConnectOptions' | 'namespace'>; declare const defaultListOptions: ListOptions; type ListResult = { duration: number; namespace: string; notes: YankiNote[]; }; /** * Description List notes currently in Anki... */ declare function listNotes(options?: PartialDeep<ListOptions>): Promise<ListResult>; declare function formatListResult(result: ListResult): string; type LoadOptions = Pick<GlobalOptions, 'allFilePaths' | 'basePath' | 'fetchAdapter' | 'fileAdapter' | 'namespace' | 'obsidianVault' | 'strictLineBreaks' | 'syncMediaAssets'>; type LocalNote = { filePath: string; filePathOriginal: string; markdown: string; note: YankiNote; }; type RenameNotesOptions = Pick<GlobalOptions, 'dryRun' | 'fileAdapter' | 'manageFilenames' | 'maxFilenameLength' /** Included because this can technically change the content of the "first line" of a card */ | 'strictLineBreaks'>; type RenameFilesResult = { dryRun: boolean; notes: LocalNote[]; }; type RenameFilesOptions = Simplify<LoadOptions & RenameNotesOptions>; declare const defaultRenameFilesOptions: RenameFilesOptions; /** * Currently used for testing and by `yanki-obsidian`. */ declare function renameFiles(allLocalFilePaths: string[], options: Partial<RenameFilesOptions>): Promise<RenameFilesResult>; type SetStyleOptions = Pick<GlobalOptions, 'ankiConnectOptions' | 'ankiWeb' | 'dryRun'> & { css: string; }; declare const defaultSetStyleOptions: SetStyleOptions; type SetStyleResult = Simplify<Pick<GlobalOptions, 'ankiWeb' | 'dryRun'> & { duration: number; models: Array<{ action: 'unchanged' | 'updated'; name: string; }>; }>; type GetStyleOptions = Pick<GlobalOptions, 'ankiConnectOptions'>; declare const defaultGetStyleOptions: GetStyleOptions; declare function getStyle(options: PartialDeep<GetStyleOptions>): Promise<string>; declare function setStyle(options?: PartialDeep<SetStyleOptions>): Promise<SetStyleResult>; declare function formatSetStyleResult(result: SetStyleResult, verbose?: boolean): string; type SyncedNote = { action: 'ankiUnreachable' | 'created' | 'deleted' | 'matched' | 'unchanged' | 'updated'; note: YankiNote; }; type SyncNotesOptions = Pick<GlobalOptions, 'ankiConnectOptions' | 'ankiWeb' | 'checkDatabase' | 'dryRun' | 'namespace' | 'strictMatching'>; declare const defaultSyncNotesOptions: SyncNotesOptions; type SyncNotesResult = Simplify<Pick<GlobalOptions, 'ankiWeb' | 'dryRun' | 'namespace'> & { deletedDecks: string[]; deletedMedia: string[]; duration: number; fixedDatabase: boolean; synced: SyncedNote[]; }>; /** * Syncs local notes to Anki. * @param allLocalNotes All the YankiNotes to sync * @returns The synced notes (with new IDs where applicable), plus some stats * about the sync * @throws {Error} For various reasons... */ declare function syncNotes(allLocalNotes: YankiNote[], options?: PartialDeep<SyncNotesOptions>): Promise<SyncNotesResult>; type SyncFilesOptions = Simplify<Pick<GlobalOptions, 'allFilePaths' | 'basePath' | 'fetchAdapter' | 'fileAdapter' | 'manageFilenames' | 'maxFilenameLength' | 'obsidianVault' | 'strictLineBreaks' | 'syncMediaAssets'> & SyncNotesOptions>; declare const defaultSyncFilesOptions: SyncFilesOptions; type SyncedFile = Simplify<SyncedNote & { filePath: string | undefined; filePathOriginal: string | undefin