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.
Manifest
Section titled “Manifest”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[]}Metadata fields
Section titled “Metadata fields”| 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. |
Script format
Section titled “Script format”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.
Callback scripts
Section titled “Callback scripts”The optional capabilities field accepts the values listed below.
onlineCheck, suggestions, list, listByGenre, listByStatus, search, searchByGenre, searchByStatus, searchByAuthor, mangaDetails, batchMangas, mangaUpdates, chapter, imageThe 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.
isOnline()
Section titled “isOnline()”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.
getSuggestion(query)
Section titled “getSuggestion(query)”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.
getList(page, genre, status)
Section titled “getList(page, genre, status)”Return a paginated list from the source, using the same page, genre, and status arguments as search.
getMangas(ids)
Section titled “getMangas(ids)”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.
getMangaUpdates(mangas)
Section titled “getMangaUpdates(mangas)”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.
getDetailedManga(id)
Section titled “getDetailedManga(id)”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.
getChapter(manga, chapter)
Section titled “getChapter(manga, chapter)”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};getImage(url)
Section titled “getImage(url)”When getImageHeaders is not present, Mankai calls this script for covers and chapter pages. It accepts either of these results:
-
A base64-encoded string containing the raw image bytes. Do not include a
data:image/...;base64,prefix. -
An object that tells Mankai to make a native request:
interface ImageProxyRequest {url: stringheaders: 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" }}Data types
Section titled “Data types”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}Genres
Section titled “Genres”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, mechaStatuses
Section titled “Statuses”| Name | Value |
|---|---|
any |
0 |
onGoing |
1 |
completed |
2 |
Reading directions
Section titled “Reading directions”| Name | Value |
|---|---|
leftToRight |
1 |
rightToLeft |
2 |
vertical |
3 |
Runtime helpers
Section titled “Runtime helpers”Mankai injects the following helpers into every callback script.
fetch(url, options)
Section titled “fetch(url, options)”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(...values)
Section titled “console.log(...values)”console.log is redirected to Mankai’s JavaScript plugin log. It is useful for diagnosing a plugin during development:
console.log('searching', query)s2t(text) and t2s(text)
Section titled “s2t(text) and t2s(text)”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.
getConfigs()
Section titled “getConfigs()”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')Persistent storage
Section titled “Persistent storage”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')Configuration
Section titled “Configuration”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 } ]}Cooldowns
Section titled “Cooldowns”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 }}