Skip to content

JavaScript plugins

This specification describes the JSON format and JavaScript API for plugins that provide manga from an online source to Mankai.

JavaScript plugins are read-only sources. The app loads the manifest from a URL or from pasted JSON, then executes the callback scripts in a hidden WebKit web view.

The manifest is a JSON object. The decoder requires only the id field. You must also supply callback scripts for the operations your plugin supports.

type ScriptName =
| 'isOnline'
| 'getSuggestion'
| 'search'
| 'getList'
| 'getMangas'
| 'getMangaUpdates'
| 'getDetailedManga'
| 'getChapter'
| 'getImage'
type PluginCapability =
| 'onlineCheck'
| 'suggestions'
| 'list'
| 'listByGenre'
| 'listByStatus'
| 'search'
| 'searchByGenre'
| 'searchByStatus'
| 'searchByAuthor'
| 'mangaDetails'
| 'batchMangas'
| 'mangaUpdates'
| 'chapter'
| 'image'
interface JsPluginManifest {
id: string
name?: string
version?: string
description?: string
authors?: string[]
repository?: string
updatesUrl?: string
availableGenres?: Genre[]
scripts?: Record<ScriptName, string>
configs?: Config[]
getImageHeaders?: Record<string, string>
cooldown?: Cooldown
capabilities?: PluginCapability[]
}
Field Type Description
id string Stable, globally unique plugin identifier. It is also used to scope persistent storage.
name string Display name. Defaults to the ID when omitted.
version string Plugin version shown in the app and compared for updates.
description string Short description displayed in source settings.
authors string[] Plugin authors. Defaults to [].
repository string Source-code or project URL.
updatesUrl string URL of a manifest to check for updates.
availableGenres Genre[] Genres supported by the source. Defaults to [].
scripts Record<string, string> JavaScript source keyed by the script names below. Unknown keys are ignored.
configs Config[] User-configurable values exposed to the scripts. Defaults to [].
getImageHeaders Record<string, string> If present, Mankai downloads every image URL with these native request headers and does not call the getImage script.
cooldown Cooldown Optional request throttling and image concurrency limits.
capabilities PluginCapability[] Operations supported by the plugin. By default, every capability except mangaUpdates is enabled.

Every script value is a string containing a function and an export marker:

async function isOnline() {
const response = await fetch('https://example.com/')
return response.ok
}
export{isOnline as default};

The marker must use the form export{functionName as default};. Mankai removes the marker before execution and calls the named function. Functions may be synchronous or asynchronous. Mankai awaits their result. A script can contain helper functions as well as its exported function.

The optional capabilities field accepts the values listed below.

onlineCheck, suggestions, list, listByGenre, listByStatus, search, searchByGenre, searchByStatus, searchByAuthor, mangaDetails, batchMangas, mangaUpdates, chapter, image

The keys and function signatures are:

Key Function signature Return value
isOnline isOnline() boolean
getSuggestion getSuggestion(query) string[]
search search(query, page, genre, status, isAuthor) Manga[]
getList getList(page, genre, status) Manga[]
getMangas getMangas(ids) Manga[]
getMangaUpdates getMangaUpdates(mangas) MangaUpdate[] metadata patches and update flags
getDetailedManga getDetailedManga(id) DetailedManga
getChapter getChapter(manga, chapter) string[] of image URLs
getImage getImage(url) Base64 image data, or an image proxy request object

Details for each argument and result follow.

Return true when the source is reachable and usable. Otherwise, return false. Throwing or returning a non-boolean result is treated as a failed plugin call.

Return an array of strings suitable for search suggestions. The manifest key is singular: getSuggestion.

async function getSuggestion(query) {
const response = await fetch(
`https://example.com/suggestions?q=${encodeURIComponent(query)}`,
)
if (!response.ok) return []
return await response.json()
}
export{getSuggestion as default};

search(query, page, genre, status, isAuthor)

Section titled “search(query, page, genre, status, isAuthor)”

Search for manga by title or author. page is a 1-based page number. genre is one of the strings in Genres, and status is one of the numeric values in Statuses. When isAuthor is true, search the source’s author field instead of its title field. Return an array of lightweight Manga objects.

Return a paginated list from the source, using the same page, genre, and status arguments as search.

Return an array of lightweight manga objects for the requested string IDs. The result may contain fewer entries if the source no longer has some IDs.

Each request entry contains a manga ID and the latest chapter currently known by Mankai. This callback lets the plugin control update behavior. Exclude the mangaUpdates capability when the plugin should use Mankai’s default batch comparison instead.

interface MangaUpdateRequest {
id: string
latestChapter: Chapter
}

Return a MangaUpdate[]. Every result must include id and updates. The updates flag alone controls whether Mankai marks the saved manga as updated. Metadata is refreshed for every returned result, including results where updates is false.

interface MangaUpdate extends Manga {
updates: boolean
}
async function getMangaUpdates(mangas) {
const response = await fetch('https://example.com/manga/updates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mangas),
})
if (!response.ok) return []
return await response.json()
}
export{getMangaUpdates as default};

For example, a plugin can return { id: "completed-series", updates: false, status: 2 } to refresh the status without showing an unread update.

Return a full DetailedManga object. Mankai serializes the returned value to JSON before decoding it, so return a JavaScript object rather than a JSON string.

Return the ordered image URLs for chapter. The manga argument is the full object returned by getDetailedManga, and chapter is the selected chapter object. Image URLs should be absolute URLs because Mankai later passes each one to getImage.

async function getChapter(manga, chapter) {
const response = await fetch(
`https://example.com/manga/${encodeURIComponent(manga.id)}/chapter/${encodeURIComponent(chapter.id)}`,
)
if (!response.ok) return []
return await response.json()
}
export{getChapter as default};

When getImageHeaders is not present, Mankai calls this script for covers and chapter pages. It accepts either of these results:

  1. A base64-encoded string containing the raw image bytes. Do not include a data:image/...;base64, prefix.

  2. An object that tells Mankai to make a native request:

    interface ImageProxyRequest {
    url: string
    headers: Record<string, string>
    }

For example:

async function getImage(url) {
return {
url,
headers: {
Referer: 'https://example.com/',
'User-Agent': 'Mozilla/5.0',
},
}
}
export{getImage as default};

If the manifest contains getImageHeaders, the equivalent native request is made automatically for every image URL:

{
"getImageHeaders": {
"Referer": "https://example.com/",
"User-Agent": "Mozilla/5.0"
}
}

The following shapes are accepted by the callback results. Optional properties may be omitted.

interface Chapter {
id: string
title?: string
locked?: boolean
}
interface Manga {
id: string
title?: string
cover?: string
status?: Status
latestChapter?: Chapter
meta?: string
}
interface ChapterGroup {
id?: string
title: string
chapters: Chapter[]
}
interface DetailedManga {
id: string
title?: string
cover?: string
status?: Status
readingDirection?: ReadingDirection
latestChapter?: Chapter
description?: string
externalLink?: string
updatedAt?: number // Unix timestamp in milliseconds
authors?: string[]
genres?: Genre[]
chapters?: ChapterGroup[]
remarks?: string
meta?: string
}

The string values recognized by Mankai are:

all, action, romance, yuri, boysLove, otokonoko, schoolLife, adventure,
harem, speculativeFiction, war, suspense, fanFiction, comedy, magic, horror,
historical, sports, mature, mecha
Name Value
any 0
onGoing 1
completed 2
Name Value
leftToRight 1
rightToLeft 2
vertical 3

Mankai injects the following helpers into every callback script.

The runtime provides a fetch implementation that uses the app’s native networking. It supports the following request options:

interface FetchOptions {
method?: string // Defaults to "GET"
headers?: Record<string, string> | Headers
body?: string
}

The returned response has these properties and methods:

interface FetchResponse {
ok: boolean
status: number
statusText: string
url: string
headers: Headers
text(): Promise<string>
json(): Promise<any>
blob(): Promise<Blob>
arrayBuffer(): Promise<ArrayBuffer>
}

console.log is redirected to Mankai’s JavaScript plugin log. It is useful for diagnosing a plugin during development:

console.log('searching', query)

These asynchronous helpers convert Chinese text using OpenCC:

const traditional = await s2t('简体转繁体')
const simplified = await t2s('繁體轉簡體')

s2t converts simplified Chinese to traditional Chinese, while t2s does the reverse.

Returns an array of current values for the configuration fields declared in the manifest:

interface ConfigValue {
key: string
value: unknown
}

Example:

function configValue(key) {
return getConfigs().find((config) => config.key === key)?.value
}
const username = configValue('username')

These helpers store string values in a key-value store scoped to the plugin’s id:

getValue(key: string): Promise<string | null>;
setValue(key: string, value: string): Promise<void>;
removeValue(key: string): Promise<boolean>;

Use await with all three helpers. Values survive app restarts and plugin updates, and are removed when the plugin is deleted.

const token = await getValue('token')
if (!token) {
await setValue('token', 'new-token')
}
await removeValue('temporary-value')

A configuration entry has this shape:

interface Config {
key: string
name: string
description?: string
type:
'text' | 'password' | 'number' | 'slider' | 'boolean' | 'select' | 'color'
defaultValue: unknown
options?: string[]
min?: number
max?: number
step?: number
supportsOpacity?: boolean
}

options is used for select fields. min, max, and step configure the range and increment for slider fields. The app initializes each configuration field with defaultValue, persists changes made in source settings, and exposes the current values through getConfigs().

Example:

{
"configs": [
{
"key": "language",
"name": "Language",
"description": "Language used when requesting titles.",
"type": "select",
"defaultValue": "en",
"options": ["en", "zh-Hans", "zh-Hant"]
},
{
"key": "imageQuality",
"name": "Image quality",
"type": "slider",
"defaultValue": 80,
"min": 10,
"max": 100,
"step": 5
},
{
"key": "includeMature",
"name": "Include mature content",
"type": "boolean",
"defaultValue": false
}
]
}

Cooldown values are in milliseconds:

interface Cooldown {
default?: number
getImage?: number
getImageConcurrency?: number
}
Field Description
default Minimum spacing between non-image operations such as search, list, details, and chapter requests.
getImage Minimum spacing between image requests. Images use a separate schedule from other operations.
getImageConcurrency Maximum number of image requests in flight at once. Values less than 1 do not set a limit.

Use these fields to respect the source’s rate limits. For example:

{
"cooldown": {
"default": 500,
"getImage": 100,
"getImageConcurrency": 2
}
}