mwn
Advanced tools
Comparing version 0.11.0 to 0.11.1
@@ -27,3 +27,3 @@ /** | ||
import { MwnTitle } from './title'; | ||
import { MwnPage, ApiPage, ApiRevision } from './page'; | ||
import { MwnPage } from './page'; | ||
import { MwnWikitext } from './wikitext'; | ||
@@ -34,9 +34,10 @@ import { MwnUser } from './user'; | ||
import { MwnStream } from './eventstream'; | ||
export { MwnDate, MwnTitle, MwnPage, MwnFile, MwnCategory, MwnWikitext, MwnUser, MwnStream, ApiPage, ApiRevision }; | ||
import { RawRequestParams } from './core'; | ||
import { log, updateLoggingConfig } from './log'; | ||
import { MwnError } from './error'; | ||
import { link, template, table } from './static_utils'; | ||
import { link, table, template } from './static_utils'; | ||
import { sleep } from './utils'; | ||
import type { ApiDeleteParams, ApiEditPageParams, ApiMoveParams, ApiParseParams, ApiPurgeParams, ApiQueryAllMessagesParams, ApiQueryAllPagesParams, ApiQueryCategoryMembersParams, ApiQuerySearchParams, ApiQueryUserInfoParams, ApiRollbackParams, ApiUndeleteParams, ApiUploadParams } from './api_params'; | ||
import { ApiEditResponse, ApiPage, ApiResponse, ApiRevision, ApiSearchResult } from './api_response_types'; | ||
export { MwnDate, MwnTitle, MwnPage, MwnFile, MwnCategory, MwnWikitext, MwnUser, MwnStream, ApiPage, ApiRevision }; | ||
export interface MwnOptions { | ||
@@ -73,3 +74,3 @@ silent?: boolean; | ||
export declare type ApiParams = { | ||
[param: string]: string | string[] | boolean | number | number[] | Date | { | ||
[param: string]: string | string[] | boolean | number | number[] | Date | File | { | ||
stream: ReadableStream; | ||
@@ -79,16 +80,2 @@ name: string; | ||
}; | ||
export interface ApiResponse { | ||
query?: Record<string, any>; | ||
[prop: string]: any; | ||
} | ||
declare type ApiEditResponse = { | ||
result: string; | ||
pageid: number; | ||
title: string; | ||
contentmodel: string; | ||
nochange?: boolean; | ||
oldrevid: number; | ||
newrevid: number; | ||
newtimestamp: string; | ||
}; | ||
export declare class mwn { | ||
@@ -161,31 +148,39 @@ /** | ||
/** | ||
* Title class associated with the bot instance | ||
* Title class associated with the bot instance. | ||
* See {@link MwnTitle} interface for methods on title objects. | ||
*/ | ||
title: import("./title").MwnTitleStatic; | ||
/** | ||
* Page class associated with the bot instance | ||
* Page class associated with the bot instance. | ||
* See {@link MwnPage} interface for methods on page objects. | ||
*/ | ||
page: import("./page").MwnPageStatic; | ||
/** | ||
* Category class associated with the bot instance | ||
* Category class associated with the bot instance. | ||
* See {@link MwnCategory} interface for methods on category objects. | ||
*/ | ||
category: import("./category").MwnCategoryStatic; | ||
/** | ||
* File class associated with the bot instance | ||
* File class associated with the bot instance. | ||
* See {@link MwnFile} interface for methods on file objects. | ||
*/ | ||
file: import("./file").MwnFileStatic; | ||
/** | ||
* User class associated with the bot instance | ||
* User class associated with the bot instance. | ||
* See {@link MwnUser} interface for methods on user objects. | ||
*/ | ||
user: import("./user").MwnUserStatic; | ||
/** | ||
* Wikitext class associated with the bot instance | ||
* Wikitext class associated with the bot instance. | ||
* See {@link MwnWikitext} interface for methods on wikitext objects. | ||
*/ | ||
wikitext: import("./wikitext").MwnWikitextStatic; | ||
/** | ||
* Stream class associated with the bot instance | ||
* Stream class associated with the bot instance. | ||
* See {@link MwnStream} interface for methods on stream objects. | ||
*/ | ||
stream: import("./eventstream").MwnStreamStatic; | ||
/** | ||
* Date class associated with the bot instance | ||
* Date class associated with the bot instance. | ||
* See {@link MwnDate} interface for methods on date objects. | ||
*/ | ||
@@ -262,2 +257,3 @@ date: import("./date").MwnDateStatic; | ||
request(params: ApiParams, customRequestOptions?: RawRequestParams): Promise<ApiResponse>; | ||
query(params: ApiParams, customRequestOptions?: RawRequestParams): Promise<ApiResponse>; | ||
/************** CORE FUNCTIONS *******************/ | ||
@@ -557,3 +553,3 @@ /** | ||
*/ | ||
search(searchTerm: string, limit: number, props: ('size' | 'timestamp' | 'wordcount' | 'snippet' | 'redirectitle' | 'sectiontitle' | 'redirectsnippet' | 'titlesnippet' | 'sectionsnippet' | 'categorysnippet')[], otherParams?: ApiQuerySearchParams): Promise<ApiResponse>; | ||
search(searchTerm: string, limit?: number, props?: ApiQuerySearchParams['srprop'], otherParams?: ApiQuerySearchParams): Promise<ApiSearchResult[]>; | ||
/************* BULK PROCESSING FUNCTIONS ************/ | ||
@@ -560,0 +556,0 @@ /** |
148
build/bot.js
@@ -45,3 +45,2 @@ "use strict"; | ||
const axios_cookiejar_support_1 = require("axios-cookiejar-support"); | ||
axios_cookiejar_support_1.default(axios_1.default); | ||
// Nested classes of mwn | ||
@@ -61,2 +60,3 @@ const date_1 = require("./date"); | ||
const utils_1 = require("./utils"); | ||
axios_cookiejar_support_1.default(axios_1.default); | ||
class mwn { | ||
@@ -161,31 +161,39 @@ /** | ||
/** | ||
* Title class associated with the bot instance | ||
* Title class associated with the bot instance. | ||
* See {@link MwnTitle} interface for methods on title objects. | ||
*/ | ||
this.title = title_1.default(); | ||
/** | ||
* Page class associated with the bot instance | ||
* Page class associated with the bot instance. | ||
* See {@link MwnPage} interface for methods on page objects. | ||
*/ | ||
this.page = page_1.default(this); | ||
/** | ||
* Category class associated with the bot instance | ||
* Category class associated with the bot instance. | ||
* See {@link MwnCategory} interface for methods on category objects. | ||
*/ | ||
this.category = category_1.default(this); | ||
/** | ||
* File class associated with the bot instance | ||
* File class associated with the bot instance. | ||
* See {@link MwnFile} interface for methods on file objects. | ||
*/ | ||
this.file = file_1.default(this); | ||
/** | ||
* User class associated with the bot instance | ||
* User class associated with the bot instance. | ||
* See {@link MwnUser} interface for methods on user objects. | ||
*/ | ||
this.user = user_1.default(this); | ||
/** | ||
* Wikitext class associated with the bot instance | ||
* Wikitext class associated with the bot instance. | ||
* See {@link MwnWikitext} interface for methods on wikitext objects. | ||
*/ | ||
this.wikitext = wikitext_1.default(this); | ||
/** | ||
* Stream class associated with the bot instance | ||
* Stream class associated with the bot instance. | ||
* See {@link MwnStream} interface for methods on stream objects. | ||
*/ | ||
this.stream = eventstream_1.default(this); | ||
/** | ||
* Date class associated with the bot instance | ||
* Date class associated with the bot instance. | ||
* See {@link MwnDate} interface for methods on date objects. | ||
*/ | ||
@@ -319,3 +327,3 @@ this.date = date_1.default(this); | ||
*/ | ||
rawRequest(requestOptions) { | ||
async rawRequest(requestOptions) { | ||
if (!requestOptions.url) { | ||
@@ -354,2 +362,5 @@ return error_1.rejectWithError({ | ||
} | ||
async query(params, customRequestOptions = {}) { | ||
return this.request(Object.assign({ action: 'query' }, params), customRequestOptions); | ||
} | ||
/************** CORE FUNCTIONS *******************/ | ||
@@ -438,3 +449,3 @@ /** | ||
*/ | ||
logout() { | ||
async logout() { | ||
if (this.usingOAuth) { | ||
@@ -492,3 +503,3 @@ throw new Error("Can't use logout() while using OAuth"); | ||
*/ | ||
userinfo(options = {}) { | ||
async userinfo(options = {}) { | ||
return this.request({ | ||
@@ -506,3 +517,3 @@ action: 'query', | ||
*/ | ||
getSiteInfo() { | ||
async getSiteInfo() { | ||
return this.request({ | ||
@@ -520,3 +531,3 @@ action: 'query', | ||
*/ | ||
getTokens() { | ||
async getTokens() { | ||
return this.getTokensAndSiteInfo(); | ||
@@ -811,4 +822,5 @@ } | ||
save(title, content, summary, options) { | ||
return this.request(utils_1.merge({ | ||
return this.request({ | ||
action: 'edit', | ||
...utils_1.makeTitle(title), | ||
text: content, | ||
@@ -818,3 +830,4 @@ summary: summary, | ||
token: this.csrfToken, | ||
}, utils_1.makeTitle(title), options)).then((data) => data.edit); | ||
...options, | ||
}).then((data) => data.edit); | ||
} | ||
@@ -832,3 +845,3 @@ /** | ||
create(title, content, summary, options) { | ||
return this.request(utils_1.merge({ | ||
return this.request({ | ||
action: 'edit', | ||
@@ -841,3 +854,4 @@ title: String(title), | ||
token: this.csrfToken, | ||
}, options)).then((data) => data.edit); | ||
...options, | ||
}).then((data) => data.edit); | ||
} | ||
@@ -853,4 +867,5 @@ /** | ||
newSection(title, header, message, additionalParams) { | ||
return this.request(utils_1.merge({ | ||
return this.request({ | ||
action: 'edit', | ||
...utils_1.makeTitle(title), | ||
section: 'new', | ||
@@ -861,3 +876,4 @@ summary: header, | ||
token: this.csrfToken, | ||
}, utils_1.makeTitle(title), additionalParams)).then((data) => data.edit); | ||
...additionalParams, | ||
}).then((data) => data.edit); | ||
} | ||
@@ -873,7 +889,9 @@ /** | ||
delete(title, summary, options) { | ||
return this.request(utils_1.merge({ | ||
return this.request({ | ||
action: 'delete', | ||
...utils_1.makeTitle(title), | ||
reason: summary, | ||
token: this.csrfToken, | ||
}, utils_1.makeTitle(title), options)).then((data) => data.delete); | ||
...options, | ||
}).then((data) => data.delete); | ||
} | ||
@@ -890,3 +908,3 @@ /** | ||
undelete(title, summary, options) { | ||
return this.request(utils_1.merge({ | ||
return this.request({ | ||
action: 'undelete', | ||
@@ -896,3 +914,4 @@ title: String(title), | ||
token: this.csrfToken, | ||
}, options)).then((data) => data.undelete); | ||
...options, | ||
}).then((data) => data.undelete); | ||
} | ||
@@ -908,3 +927,3 @@ /** | ||
move(fromtitle, totitle, summary, options) { | ||
return this.request(utils_1.merge({ | ||
return this.request({ | ||
action: 'move', | ||
@@ -916,3 +935,4 @@ from: fromtitle, | ||
token: this.csrfToken, | ||
}, options)).then((data) => data.move); | ||
...options, | ||
}).then((data) => data.move); | ||
} | ||
@@ -927,9 +947,12 @@ /** | ||
*/ | ||
parseWikitext(content, additionalParams) { | ||
return this.request(utils_1.merge({ | ||
async parseWikitext(content, additionalParams) { | ||
return this.request({ | ||
action: 'parse', | ||
text: String(content), | ||
contentmodel: 'wikitext', | ||
disablelimitreport: true, | ||
disableeditsection: true, | ||
formatversion: 2, | ||
action: 'parse', | ||
contentmodel: 'wikitext', | ||
}, additionalParams)).then(function (data) { | ||
...additionalParams, | ||
}).then(function (data) { | ||
return data.parse.text; | ||
@@ -946,4 +969,4 @@ }); | ||
*/ | ||
parseTitle(title, additionalParams) { | ||
return this.request(utils_1.merge({ | ||
async parseTitle(title, additionalParams) { | ||
return this.request({ | ||
page: String(title), | ||
@@ -953,3 +976,4 @@ formatversion: 2, | ||
contentmodel: 'wikitext', | ||
}, additionalParams)).then(function (data) { | ||
...additionalParams, | ||
}).then(function (data) { | ||
return data.parse.text; | ||
@@ -967,3 +991,3 @@ }); | ||
*/ | ||
upload(filepath, title, text, options) { | ||
async upload(filepath, title, text, options) { | ||
return this.request({ | ||
@@ -1002,4 +1026,4 @@ action: 'upload', | ||
*/ | ||
uploadFromUrl(url, title, text, options) { | ||
return this.request(utils_1.merge({ | ||
async uploadFromUrl(url, title, text, options) { | ||
return this.request({ | ||
action: 'upload', | ||
@@ -1011,3 +1035,4 @@ url: url, | ||
token: this.csrfToken, | ||
}, options)).then((data) => { | ||
...options, | ||
}).then((data) => { | ||
if (data.upload.warnings) { | ||
@@ -1030,8 +1055,9 @@ log_1.log('[W] The API returned warnings while uploading to ' + title + ':'); | ||
*/ | ||
download(file, localname) { | ||
return this.request(utils_1.merge({ | ||
async download(file, localname) { | ||
return this.request({ | ||
action: 'query', | ||
...utils_1.makeTitles(file), | ||
prop: 'imageinfo', | ||
iiprop: 'url', | ||
}, utils_1.makeTitles(file))).then((data) => { | ||
}).then((data) => { | ||
const url = data.query.pages[0].imageinfo[0].url; | ||
@@ -1049,3 +1075,3 @@ const name = new this.title(data.query.pages[0].title).getMainText(); | ||
*/ | ||
downloadFromUrl(url, localname) { | ||
async downloadFromUrl(url, localname) { | ||
return this.rawRequest({ | ||
@@ -1067,6 +1093,6 @@ method: 'get', | ||
} | ||
saveOption(option, value) { | ||
async saveOption(option, value) { | ||
return this.saveOptions({ [option]: value }); | ||
} | ||
saveOptions(options) { | ||
async saveOptions(options) { | ||
return this.request({ | ||
@@ -1086,8 +1112,10 @@ action: 'options', | ||
*/ | ||
rollback(page, user, params) { | ||
return this.request(utils_1.merge({ | ||
async rollback(page, user, params) { | ||
return this.request({ | ||
action: 'rollback', | ||
...utils_1.makeTitle(page), | ||
user: user, | ||
token: this.state.rollbacktoken, | ||
}, utils_1.makeTitle(page), params)).then((data) => { | ||
...params, | ||
}).then((data) => { | ||
return data.rollback; | ||
@@ -1103,6 +1131,8 @@ }); | ||
*/ | ||
purge(titles, options) { | ||
return this.request(utils_1.merge({ | ||
async purge(titles, options) { | ||
return this.request({ | ||
action: 'purge', | ||
}, utils_1.makeTitles(titles), options)).then((data) => data.purge); | ||
...utils_1.makeTitles(titles), | ||
...options, | ||
}).then((data) => data.purge); | ||
} | ||
@@ -1116,3 +1146,3 @@ /** | ||
*/ | ||
getPagesByPrefix(prefix, otherParams) { | ||
async getPagesByPrefix(prefix, otherParams) { | ||
const title = this.title.newFromText(prefix); | ||
@@ -1122,3 +1152,3 @@ if (!title) { | ||
} | ||
return this.request(utils_1.merge({ | ||
return this.request({ | ||
action: 'query', | ||
@@ -1129,3 +1159,4 @@ list: 'allpages', | ||
aplimit: 'max', | ||
}, otherParams)).then((data) => { | ||
...otherParams, | ||
}).then((data) => { | ||
return data.query.allpages.map((pg) => pg.title); | ||
@@ -1140,3 +1171,3 @@ }); | ||
*/ | ||
getPagesInCategory(category, otherParams) { | ||
async getPagesInCategory(category, otherParams) { | ||
const title = this.title.newFromText(category, 14); | ||
@@ -1162,4 +1193,4 @@ return this.request({ | ||
*/ | ||
search(searchTerm, limit, props, otherParams) { | ||
return this.request(utils_1.merge({ | ||
async search(searchTerm, limit = 50, props, otherParams) { | ||
return this.request({ | ||
action: 'query', | ||
@@ -1169,4 +1200,5 @@ list: 'search', | ||
srlimit: limit, | ||
srprop: props || 'size|wordcount|timestamp', | ||
}, otherParams)).then((data) => { | ||
srprop: props || ['size', 'wordcount', 'timestamp'], | ||
...otherParams, | ||
}).then((data) => { | ||
return data.query.search; | ||
@@ -1173,0 +1205,0 @@ }); |
@@ -8,3 +8,4 @@ /** | ||
import * as OAuth from 'oauth-1.0a'; | ||
import type { mwn, ApiParams, ApiResponse } from './bot'; | ||
import type { mwn, ApiParams } from './bot'; | ||
import { ApiResponse } from './api_response_types'; | ||
export interface RawRequestParams extends AxiosRequestConfig { | ||
@@ -11,0 +12,0 @@ retryNumber?: number; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const wikitext_1 = require("./wikitext"); | ||
/** | ||
@@ -153,5 +154,3 @@ * Wrapper around the native JS Date() for ease of | ||
}; | ||
// as long as only unbind() and rebind() methods of bot.wikitext are used, | ||
// there shouldn't be problems from not having called getSiteInfo() on the bot object | ||
let unbinder = new bot.wikitext(formatstr); // escape stuff between [...] | ||
let unbinder = new wikitext_1.Unbinder(formatstr); // escape stuff between [...] | ||
unbinder.unbind('\\[', '\\]'); | ||
@@ -158,0 +157,0 @@ unbinder.text = unbinder.text.replace( |
import type { mwn, MwnTitle } from './bot'; | ||
import type { ApiDeleteParams, ApiEditPageParams, ApiMoveParams, ApiPurgeParams, ApiQueryAllPagesParams, ApiQueryLogEventsParams, ApiQueryRevisionsParams, ApiUndeleteParams } from './api_params'; | ||
import type { LogEvent } from './user'; | ||
export declare type revisionprop = 'content' | 'timestamp' | 'user' | 'comment' | 'parsedcomment' | 'ids' | 'flags' | 'size' | 'tags' | 'userid' | 'contentmodel'; | ||
export declare type logprop = 'type' | 'user' | 'comment' | 'details' | 'timestamp' | 'title' | 'parsedcomment' | 'ids' | 'tags' | 'userid'; | ||
export interface PageViewOptions { | ||
access?: 'all-access' | 'desktop' | 'mobile-app' | 'mobile-web'; | ||
agent?: 'all-agents' | 'user' | 'spider' | 'automated'; | ||
granularity?: 'daily' | 'monthly'; | ||
start?: Date; | ||
end?: Date; | ||
} | ||
export interface PageViewData { | ||
project: string; | ||
article: string; | ||
granularity: string; | ||
timestamp: string; | ||
access: string; | ||
agent: string; | ||
views: number; | ||
} | ||
export interface AuthorshipData { | ||
totalBytes: number; | ||
users: Array<{ | ||
id: number; | ||
name: string; | ||
bytes: number; | ||
percent: number; | ||
}>; | ||
} | ||
export interface ApiPage { | ||
pageid: number; | ||
ns: number; | ||
title: string; | ||
missing?: true; | ||
invalid?: true; | ||
revisions?: ApiRevision[]; | ||
} | ||
export interface ApiRevision extends ApiRevisionSlot { | ||
revid?: number; | ||
parentid?: number; | ||
minor?: boolean; | ||
userhidden?: true; | ||
anon?: true; | ||
user?: string; | ||
userid?: number; | ||
timestamp?: string; | ||
roles?: string[]; | ||
commenthidden?: true; | ||
comment?: string; | ||
parsedcomment?: string; | ||
slots?: { | ||
main: ApiRevisionSlot; | ||
[slotname: string]: ApiRevisionSlot; | ||
}; | ||
} | ||
export interface ApiRevisionSlot { | ||
size?: number; | ||
sha1?: string; | ||
contentmodel?: string; | ||
contentformat?: string; | ||
content?: string; | ||
} | ||
import { ApiParseResponse, ApiRevision, LogEvent } from './api_response_types'; | ||
export interface MwnPageStatic { | ||
@@ -72,17 +12,5 @@ new (title: MwnTitle | string, namespace?: number): MwnPage; | ||
text(): Promise<string>; | ||
categories(): Promise<{ | ||
sortkey: string; | ||
category: string; | ||
hidden: boolean; | ||
}[]>; | ||
templates(): Promise<{ | ||
ns: number; | ||
title: string; | ||
exists: boolean; | ||
}[]>; | ||
links(): Promise<{ | ||
ns: number; | ||
title: string; | ||
exists: boolean; | ||
}[]>; | ||
categories(): Promise<ApiParseResponse['categories']>; | ||
templates(): Promise<ApiParseResponse['templates']>; | ||
links(): Promise<ApiParseResponse['links']>; | ||
backlinks(): Promise<string[]>; | ||
@@ -98,6 +26,6 @@ transclusions(): Promise<string[]>; | ||
getDescription(customOptions?: any): Promise<string>; | ||
history(props: revisionprop[] | revisionprop, limit: number, customOptions?: ApiQueryRevisionsParams): Promise<ApiRevision[]>; | ||
historyGen(props: revisionprop[] | revisionprop, customOptions?: ApiQueryRevisionsParams): AsyncGenerator<ApiRevision>; | ||
logs(props: logprop | logprop[], limit?: number, type?: string, customOptions?: ApiQueryLogEventsParams): Promise<LogEvent[]>; | ||
logsGen(props: logprop | logprop[], type?: string, customOptions?: ApiQueryLogEventsParams): AsyncGenerator<LogEvent>; | ||
history(props: ApiQueryRevisionsParams['rvprop'], limit: number, customOptions?: ApiQueryRevisionsParams): Promise<ApiRevision[]>; | ||
historyGen(props: ApiQueryRevisionsParams['rvprop'], customOptions?: ApiQueryRevisionsParams): AsyncGenerator<ApiRevision>; | ||
logs(props: ApiQueryLogEventsParams['leprop'], limit?: number, type?: string, customOptions?: ApiQueryLogEventsParams): Promise<LogEvent[]>; | ||
logsGen(props: ApiQueryLogEventsParams['leprop'], type?: string, customOptions?: ApiQueryLogEventsParams): AsyncGenerator<LogEvent>; | ||
pageViews(options?: PageViewOptions): Promise<PageViewData[]>; | ||
@@ -117,1 +45,26 @@ queryAuthors(): Promise<AuthorshipData>; | ||
export default function (bot: mwn): MwnPageStatic; | ||
export interface PageViewOptions { | ||
access?: 'all-access' | 'desktop' | 'mobile-app' | 'mobile-web'; | ||
agent?: 'all-agents' | 'user' | 'spider' | 'automated'; | ||
granularity?: 'daily' | 'monthly'; | ||
start?: Date; | ||
end?: Date; | ||
} | ||
export interface PageViewData { | ||
project: string; | ||
article: string; | ||
granularity: string; | ||
timestamp: string; | ||
access: string; | ||
agent: string; | ||
views: number; | ||
} | ||
export interface AuthorshipData { | ||
totalBytes: number; | ||
users: Array<{ | ||
id: number; | ||
name: string; | ||
bytes: number; | ||
percent: number; | ||
}>; | ||
} |
@@ -278,3 +278,3 @@ "use strict"; | ||
* Get the edit history of the page | ||
* @param {revisionprop[]} props - revision properties to fetch, by default content is | ||
* @param {string|string[]} props - revision properties to fetch, by default content is | ||
* excluded | ||
@@ -322,3 +322,3 @@ * @param {number} [limit=50] - number of revisions to fetch data about | ||
* Get the page logs. | ||
* @param {logprop[]} props - data about log entries to fetch | ||
* @param {string|string[]} props - data about log entries to fetch | ||
* @param {number} limit - max number of log entries to fetch | ||
@@ -325,0 +325,0 @@ * @param {string} type - type of log to fetch, can either be an letype or leaction |
import type { mwn, MwnPage } from './bot'; | ||
import type { ApiBlockParams, ApiEmailUserParams, ApiQueryLogEventsParams, ApiQueryUserContribsParams, ApiUnblockParams } from './api_params'; | ||
import type { ApiBlockParams, ApiEmailUserParams, ApiQueryGlobalUserInfoParams, ApiQueryLogEventsParams, ApiQueryUserContribsParams, ApiQueryUsersParams, ApiUnblockParams } from './api_params'; | ||
import { ApiBlockResponse, ApiEditResponse, ApiEmailUserResponse, ApiQueryGlobalUserInfoResponse, ApiQueryUsersResponse, ApiUnblockResponse, LogEvent, UserContribution } from './api_response_types'; | ||
export interface MwnUserStatic { | ||
@@ -14,37 +15,9 @@ new (username: string): MwnUser; | ||
logsGen(options?: ApiQueryLogEventsParams): AsyncGenerator<LogEvent>; | ||
info(props?: string | string[]): Promise<any>; | ||
globalinfo(props?: ('groups' | 'rights' | 'merged' | 'unattached' | 'editcount')[]): Promise<any>; | ||
sendMessage(header: string, message: string): Promise<any>; | ||
email(subject: string, message: string, options?: ApiEmailUserParams): Promise<any>; | ||
block(options: ApiBlockParams): Promise<any>; | ||
unblock(options: ApiUnblockParams): Promise<any>; | ||
info(props?: ApiQueryUsersParams['usprop']): Promise<ApiQueryUsersResponse>; | ||
globalinfo(props?: ApiQueryGlobalUserInfoParams['guiprop']): Promise<ApiQueryGlobalUserInfoResponse>; | ||
sendMessage(header: string, message: string): Promise<ApiEditResponse>; | ||
email(subject: string, message: string, options?: ApiEmailUserParams): Promise<ApiEmailUserResponse>; | ||
block(options: ApiBlockParams): Promise<ApiBlockResponse>; | ||
unblock(options: ApiUnblockParams): Promise<ApiUnblockResponse>; | ||
} | ||
export declare type UserContribution = { | ||
userid: number; | ||
user: string; | ||
pageid: number; | ||
revid: number; | ||
parentid: number; | ||
ns: number; | ||
title: string; | ||
timestamp: string; | ||
new: boolean; | ||
minor: boolean; | ||
top: boolean; | ||
comment: string; | ||
size: number; | ||
}; | ||
export declare type LogEvent = { | ||
logid: number; | ||
ns: number; | ||
title: string; | ||
pageid: number; | ||
logpage: number; | ||
params: any; | ||
type: string; | ||
action: string; | ||
user: string; | ||
timestamp: string; | ||
comment: string; | ||
}; | ||
export default function (bot: mwn): MwnUserStatic; |
@@ -84,3 +84,3 @@ "use strict"; | ||
* Get public information about the user | ||
* @param {Array} props - properties to fetch | ||
* @param {string|string[]} props - properties to fetch | ||
* @returns {Promise<Object>} | ||
@@ -102,3 +102,3 @@ */ | ||
* Get global user info for wikis with CentralAuth | ||
* @param {("groups"|"rights"|"merged"|"unattached"|"editcount")[]} props | ||
* @param {string|string[]} props | ||
*/ | ||
@@ -105,0 +105,0 @@ globalinfo(props) { |
@@ -25,4 +25,3 @@ /** | ||
} | ||
export interface MwnWikitext { | ||
text: string; | ||
export interface MwnWikitext extends Unbinder { | ||
links: Array<PageLink>; | ||
@@ -37,5 +36,2 @@ templates: Array<Template>; | ||
parseSections(): Section[]; | ||
unbind(prefix: string, postfix: string): void; | ||
rebind(): string; | ||
getText(): string; | ||
apiParse(options: ApiParseParams): Promise<string>; | ||
@@ -69,3 +65,2 @@ } | ||
/** | ||
* @class | ||
* Represents the wikitext of template transclusion. Used by #parseTemplates. | ||
@@ -101,2 +96,55 @@ * @prop {string} name Name of the template | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
export declare function parseTemplates(wikitext: string, config: TemplateConfig): Template[]; | ||
/** | ||
* Simple table parser. | ||
* Parses tables provided: | ||
* 1. It doesn't have any merged or joined cells. | ||
* 2. It doesn't use any templates to produce any table markup. | ||
* 3. Further restrictions may apply. | ||
* | ||
* Tables generated via mwn.table() class are intended to be parsable. | ||
* | ||
* This method throws when it finds an inconsistency (rather than silently | ||
* cause undesired behaviour). | ||
* | ||
* @param {string} text | ||
* @returns {Object[]} - each object in the returned array represents a row, | ||
* with its keys being column names, and values the cell content | ||
*/ | ||
export declare function parseTable(text: string): { | ||
[column: string]: string; | ||
}[]; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
export declare function parseSections(text: string): Section[]; | ||
export declare class Unbinder { | ||
text: string; | ||
constructor(text: string); | ||
private unbinder; | ||
/** | ||
* Temporarily hide a part of the string while processing the rest of it. | ||
* | ||
* eg. let u = new bot.wikitext("Hello world <!-- world --> world"); | ||
* u.unbind('<!--','-->'); | ||
* u.content = u.content.replace(/world/g, 'earth'); | ||
* u.rebind(); // gives "Hello earth <!-- world --> earth" | ||
* | ||
* Text within the 'unbinded' part (in this case, the HTML comment) remains intact | ||
* unbind() can be called multiple times to unbind multiple parts of the string. | ||
* | ||
* @param {string} prefix | ||
* @param {string} postfix | ||
*/ | ||
unbind(prefix: string, postfix: string): void; | ||
/** | ||
* Rebind after unbinding. | ||
*/ | ||
rebind(): string; | ||
/** Get the updated text */ | ||
getText(): string; | ||
} | ||
export default function (bot: mwn): MwnWikitextStatic; |
@@ -17,3 +17,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Parameter = exports.Template = void 0; | ||
exports.Unbinder = exports.parseSections = exports.parseTable = exports.parseTemplates = exports.Parameter = exports.Template = void 0; | ||
// Adapted from https://en.wikipedia.org/wiki/MediaWiki:Gadget-libExtraUtil.js | ||
@@ -23,3 +23,2 @@ // by Evad37 (cc-by-sa-3.0/GFDL) | ||
/** | ||
* @class | ||
* Represents the wikitext of template transclusion. Used by #parseTemplates. | ||
@@ -70,4 +69,322 @@ * @prop {string} name Name of the template | ||
exports.Parameter = Parameter; | ||
// parseTemplates() and processTemplateText() are adapted from | ||
// https://en.wikipedia.org/wiki/MediaWiki:Gadget-libExtraUtil.js written by Evad37 | ||
// which was in turn adapted from https://en.wikipedia.org/wiki/User:SD0001/parseAllTemplates.js | ||
// written by me. (cc-by-sa/GFDL) | ||
/** | ||
* @inheritdoc | ||
*/ | ||
function parseTemplates(wikitext, config) { | ||
config = config || { | ||
recursive: false, | ||
namePredicate: null, | ||
templatePredicate: null, | ||
count: null, | ||
}; | ||
const result = []; | ||
const n = wikitext.length; | ||
// number of unclosed braces | ||
let numUnclosed = 0; | ||
// are we inside a comment, or between nowiki tags, or in a {{{parameter}}}? | ||
let inComment = false; | ||
let inNowiki = false; | ||
let inParameter = false; | ||
let startIdx, endIdx; | ||
for (let i = 0; i < n; i++) { | ||
if (!inComment && !inNowiki && !inParameter) { | ||
if (wikitext[i] === '{' && wikitext[i + 1] === '{' && wikitext[i + 2] === '{' && wikitext[i + 3] !== '{') { | ||
inParameter = true; | ||
i += 2; | ||
} | ||
else if (wikitext[i] === '{' && wikitext[i + 1] === '{') { | ||
if (numUnclosed === 0) { | ||
startIdx = i + 2; | ||
} | ||
numUnclosed += 2; | ||
i++; | ||
} | ||
else if (wikitext[i] === '}' && wikitext[i + 1] === '}') { | ||
if (numUnclosed === 2) { | ||
endIdx = i; | ||
let templateWikitext = wikitext.slice(startIdx, endIdx); // without braces | ||
let processed = processTemplateText(templateWikitext, config.namePredicate, config.templatePredicate); | ||
if (processed) { | ||
result.push(processed); | ||
} | ||
if (config.count && result.length === config.count) { | ||
return result; | ||
} | ||
} | ||
numUnclosed -= 2; | ||
i++; | ||
} | ||
else if (wikitext[i] === '|' && numUnclosed > 2) { | ||
// swap out pipes in nested templates with \x01 character | ||
wikitext = strReplaceAt(wikitext, i, '\x01'); | ||
} | ||
else if (/^<!--/.test(wikitext.slice(i, i + 4))) { | ||
inComment = true; | ||
i += 3; | ||
} | ||
else if (/^<nowiki ?>/.test(wikitext.slice(i, i + 9))) { | ||
inNowiki = true; | ||
i += 7; | ||
} | ||
} | ||
else { | ||
// we are in a comment or nowiki or {{{parameter}}} | ||
if (wikitext[i] === '|') { | ||
// swap out pipes with \x01 character | ||
wikitext = strReplaceAt(wikitext, i, '\x01'); | ||
} | ||
else if (/^-->/.test(wikitext.slice(i, i + 3))) { | ||
inComment = false; | ||
i += 2; | ||
} | ||
else if (/^<\/nowiki ?>/.test(wikitext.slice(i, i + 10))) { | ||
inNowiki = false; | ||
i += 8; | ||
} | ||
else if (wikitext[i] === '}' && wikitext[i + 1] === '}' && wikitext[i + 2] === '}') { | ||
inParameter = false; | ||
i += 2; | ||
} | ||
} | ||
} | ||
if (config.recursive) { | ||
let subtemplates = result | ||
.map((template) => { | ||
return template.wikitext.slice(2, -2); | ||
}) | ||
.filter((templateWikitext) => { | ||
return /\{\{.*\}\}/s.test(templateWikitext); | ||
}) | ||
.map((templateWikitext) => { | ||
return parseTemplates(templateWikitext, config); | ||
}); | ||
return result.concat(...subtemplates); | ||
} | ||
return result; | ||
} | ||
exports.parseTemplates = parseTemplates; | ||
/** | ||
* @param {string} text - template wikitext without braces, with the pipes in | ||
* nested templates replaced by \x01 | ||
* @param {Function} [namePredicate] | ||
* @param {Function} [templatePredicate] | ||
* @returns {Template} | ||
*/ | ||
function processTemplateText(text, namePredicate, templatePredicate) { | ||
// eslint-disable-next-line no-control-regex | ||
const template = new Template('{{' + text.replace(/\x01/g, '|') + '}}'); | ||
// swap out pipe in links with \x01 control character | ||
// [[File: ]] can have multiple pipes, so might need multiple passes | ||
while (/(\[\[[^\]]*?)\|(.*?\]\])/g.test(text)) { | ||
text = text.replace(/(\[\[[^\]]*?)\|(.*?\]\])/g, '$1\x01$2'); | ||
} | ||
const [name, ...parameterChunks] = text.split('|').map((chunk) => { | ||
// change '\x01' control characters back to pipes | ||
// eslint-disable-next-line no-control-regex | ||
return chunk.replace(/\x01/g, '|'); | ||
}); | ||
template.setName(name); | ||
if (namePredicate && !namePredicate(template.name)) { | ||
return null; | ||
} | ||
let unnamedIdx = 1; | ||
parameterChunks.forEach(function (chunk) { | ||
let indexOfEqualTo = chunk.indexOf('='); | ||
let indexOfOpenBraces = chunk.indexOf('{{'); | ||
let isWithoutEquals = !chunk.includes('='); | ||
let hasBracesBeforeEquals = chunk.includes('{{') && indexOfOpenBraces < indexOfEqualTo; | ||
let isUnnamedParam = isWithoutEquals || hasBracesBeforeEquals; | ||
let pName, pNum, pVal; | ||
if (isUnnamedParam) { | ||
// Get the next number not already used by either an unnamed parameter, | ||
// or by a named parameter like `|1=val` | ||
while (template.getParam(unnamedIdx)) { | ||
unnamedIdx++; | ||
} | ||
pNum = unnamedIdx; | ||
pVal = chunk.trim(); | ||
} | ||
else { | ||
pName = chunk.slice(0, indexOfEqualTo).trim(); | ||
pVal = chunk.slice(indexOfEqualTo + 1).trim(); | ||
} | ||
template.addParam(pName || pNum, pVal, chunk); | ||
}); | ||
if (templatePredicate && !templatePredicate(template)) { | ||
return null; | ||
} | ||
return template; | ||
} | ||
/** | ||
* Simple table parser. | ||
* Parses tables provided: | ||
* 1. It doesn't have any merged or joined cells. | ||
* 2. It doesn't use any templates to produce any table markup. | ||
* 3. Further restrictions may apply. | ||
* | ||
* Tables generated via mwn.table() class are intended to be parsable. | ||
* | ||
* This method throws when it finds an inconsistency (rather than silently | ||
* cause undesired behaviour). | ||
* | ||
* @param {string} text | ||
* @returns {Object[]} - each object in the returned array represents a row, | ||
* with its keys being column names, and values the cell content | ||
*/ | ||
function parseTable(text) { | ||
text = text.trim(); | ||
const indexOfRawPipe = function (text) { | ||
// number of unclosed brackets | ||
let tlevel = 0, llevel = 0; | ||
let n = text.length; | ||
for (let i = 0; i < n; i++) { | ||
if (text[i] === '{' && text[i + 1] === '{') { | ||
tlevel++; | ||
i++; | ||
} | ||
else if (text[i] === '[' && text[i + 1] === '[') { | ||
llevel++; | ||
i++; | ||
} | ||
else if (text[i] === '}' && text[i + 1] === '}') { | ||
tlevel--; | ||
i++; | ||
} | ||
else if (text[i] === ']' && text[i + 1] === ']') { | ||
llevel--; | ||
i++; | ||
} | ||
else if (text[i] === '|' && tlevel === 0 && llevel === 0) { | ||
return i; | ||
} | ||
} | ||
}; | ||
if (!text.startsWith('{|') || !text.endsWith('|}')) { | ||
throw new Error('failed to parse table. Unexpected starting or ending'); | ||
} | ||
// remove front matter and final matter | ||
// including table attributes and caption, and unnecessary |- at the top | ||
text = text.replace(/^\{\|.*$((\n\|-)?\n\|\+.*$)?(\n\|-)?/m, '').replace(/^\|\}$/m, ''); | ||
let [header, ...rows] = text.split(/^\|-/m).map((r) => r.trim()); | ||
// remove cell attributes, extracts data | ||
const extractData = (cell) => { | ||
return cell.slice(indexOfRawPipe(cell) + 1).trim(); | ||
}; | ||
// XXX: handle the case where there are is no header row | ||
let cols = header.split('\n').map((e) => e.replace(/^!/, '')); | ||
if (cols.length === 1) { | ||
// non-multilined table? | ||
cols = cols[0].split('!!'); | ||
} | ||
cols = cols.map(extractData); | ||
let numcols = cols.length; | ||
let output = new Array(rows.length); | ||
rows.forEach((row, idx) => { | ||
let cells = row.split(/^\|/m).slice(1); // slice(1) removes the emptiness or the row styles if present | ||
if (cells.length === 1) { | ||
// non-multilined | ||
// cells are separated by || | ||
cells = cells[0].replace(/^\|/, '').split('||'); | ||
} | ||
cells = cells.map(extractData); | ||
if (cells.length !== numcols) { | ||
throw new Error(`failed to parse table: found ${cells.length} cells on row ${idx}, expected ${numcols}`); | ||
} | ||
output[idx] = {}; // output[idx] represents a row | ||
for (let i = 0; i < numcols; i++) { | ||
output[idx][cols[i]] = cells[i]; | ||
} | ||
}); | ||
return output; | ||
} | ||
exports.parseTable = parseTable; | ||
// XXX: fix jsdocs | ||
/** | ||
* @inheritdoc | ||
*/ | ||
function parseSections(text) { | ||
const rgx = /^(=+)(.*?)\1/gm; | ||
let sections = [ | ||
{ | ||
level: 1, | ||
header: null, | ||
index: 0, | ||
}, | ||
]; | ||
let match; | ||
while ((match = rgx.exec(text))) { | ||
// eslint-disable-line no-cond-assign | ||
sections.push({ | ||
level: match[1].length, | ||
header: match[2].trim(), | ||
index: match.index, | ||
}); | ||
} | ||
let n = sections.length; | ||
for (let i = 0; i < n - 1; i++) { | ||
sections[i].content = text.slice(sections[i].index, sections[i + 1].index); | ||
} | ||
sections[n - 1].content = text.slice(sections[n - 1].index); | ||
return sections; | ||
} | ||
exports.parseSections = parseSections; | ||
// Attribution: https://en.wikipedia.org/wiki/MediaWiki:Gadget-morebits.js (cc-by-sa 3.0/GFDL) | ||
class Unbinder { | ||
constructor(text) { | ||
this.text = text; | ||
} | ||
/** | ||
* Temporarily hide a part of the string while processing the rest of it. | ||
* | ||
* eg. let u = new bot.wikitext("Hello world <!-- world --> world"); | ||
* u.unbind('<!--','-->'); | ||
* u.content = u.content.replace(/world/g, 'earth'); | ||
* u.rebind(); // gives "Hello earth <!-- world --> earth" | ||
* | ||
* Text within the 'unbinded' part (in this case, the HTML comment) remains intact | ||
* unbind() can be called multiple times to unbind multiple parts of the string. | ||
* | ||
* @param {string} prefix | ||
* @param {string} postfix | ||
*/ | ||
unbind(prefix, postfix) { | ||
if (!this.unbinder) { | ||
this.unbinder = { | ||
counter: 0, | ||
history: {}, | ||
prefix: '%UNIQ::' + Math.random() + '::', | ||
postfix: '::UNIQ%', | ||
}; | ||
} | ||
let re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g'); | ||
this.text = this.text.replace(re, (match) => { | ||
let current = this.unbinder.prefix + this.unbinder.counter + this.unbinder.postfix; | ||
this.unbinder.history[current] = match; | ||
++this.unbinder.counter; | ||
return current; | ||
}); | ||
} | ||
/** | ||
* Rebind after unbinding. | ||
*/ | ||
rebind() { | ||
let content = this.text; | ||
for (let [current, replacement] of Object.entries(this.unbinder.history)) { | ||
content = content.replace(current, replacement); | ||
} | ||
this.text = content; | ||
return this.text; | ||
} | ||
/** Get the updated text */ | ||
getText() { | ||
return this.text; | ||
} | ||
} | ||
exports.Unbinder = Unbinder; | ||
function default_1(bot) { | ||
class Wikitext { | ||
class Wikitext extends Unbinder { | ||
constructor(wikitext) { | ||
@@ -77,3 +394,3 @@ if (typeof wikitext !== 'string') { | ||
} | ||
this.text = wikitext; | ||
super(wikitext); | ||
} | ||
@@ -131,107 +448,5 @@ /** Parse links, file usages and categories from the wikitext */ | ||
parseTemplates(config) { | ||
return (this.templates = Wikitext.parseTemplates(this.text, config)); | ||
return (this.templates = parseTemplates(this.text, config)); | ||
} | ||
// parseTemplates() and processTemplateText() are adapted from | ||
// https://en.wikipedia.org/wiki/MediaWiki:Gadget-libExtraUtil.js written by Evad37 | ||
// which was in turn adapted from https://en.wikipedia.org/wiki/User:SD0001/parseAllTemplates.js | ||
// written by me. (cc-by-sa/GFDL) | ||
/** | ||
* @inheritdoc | ||
*/ | ||
static parseTemplates(wikitext, config) { | ||
config = config || { | ||
recursive: false, | ||
namePredicate: null, | ||
templatePredicate: null, | ||
count: null, | ||
}; | ||
const result = []; | ||
const n = wikitext.length; | ||
// number of unclosed braces | ||
let numUnclosed = 0; | ||
// are we inside a comment, or between nowiki tags, or in a {{{parameter}}}? | ||
let inComment = false; | ||
let inNowiki = false; | ||
let inParameter = false; | ||
let startIdx, endIdx; | ||
for (let i = 0; i < n; i++) { | ||
if (!inComment && !inNowiki && !inParameter) { | ||
if (wikitext[i] === '{' && | ||
wikitext[i + 1] === '{' && | ||
wikitext[i + 2] === '{' && | ||
wikitext[i + 3] !== '{') { | ||
inParameter = true; | ||
i += 2; | ||
} | ||
else if (wikitext[i] === '{' && wikitext[i + 1] === '{') { | ||
if (numUnclosed === 0) { | ||
startIdx = i + 2; | ||
} | ||
numUnclosed += 2; | ||
i++; | ||
} | ||
else if (wikitext[i] === '}' && wikitext[i + 1] === '}') { | ||
if (numUnclosed === 2) { | ||
endIdx = i; | ||
let templateWikitext = wikitext.slice(startIdx, endIdx); // without braces | ||
let processed = processTemplateText(templateWikitext, config.namePredicate, config.templatePredicate); | ||
if (processed) { | ||
result.push(processed); | ||
} | ||
if (config.count && result.length === config.count) { | ||
return result; | ||
} | ||
} | ||
numUnclosed -= 2; | ||
i++; | ||
} | ||
else if (wikitext[i] === '|' && numUnclosed > 2) { | ||
// swap out pipes in nested templates with \x01 character | ||
wikitext = strReplaceAt(wikitext, i, '\x01'); | ||
} | ||
else if (/^<!--/.test(wikitext.slice(i, i + 4))) { | ||
inComment = true; | ||
i += 3; | ||
} | ||
else if (/^<nowiki ?>/.test(wikitext.slice(i, i + 9))) { | ||
inNowiki = true; | ||
i += 7; | ||
} | ||
} | ||
else { | ||
// we are in a comment or nowiki or {{{parameter}}} | ||
if (wikitext[i] === '|') { | ||
// swap out pipes with \x01 character | ||
wikitext = strReplaceAt(wikitext, i, '\x01'); | ||
} | ||
else if (/^-->/.test(wikitext.slice(i, i + 3))) { | ||
inComment = false; | ||
i += 2; | ||
} | ||
else if (/^<\/nowiki ?>/.test(wikitext.slice(i, i + 10))) { | ||
inNowiki = false; | ||
i += 8; | ||
} | ||
else if (wikitext[i] === '}' && wikitext[i + 1] === '}' && wikitext[i + 2] === '}') { | ||
inParameter = false; | ||
i += 2; | ||
} | ||
} | ||
} | ||
if (config.recursive) { | ||
let subtemplates = result | ||
.map((template) => { | ||
return template.wikitext.slice(2, -2); | ||
}) | ||
.filter((templateWikitext) => { | ||
return /\{\{.*\}\}/s.test(templateWikitext); | ||
}) | ||
.map((templateWikitext) => { | ||
return Wikitext.parseTemplates(templateWikitext, config); | ||
}); | ||
return result.concat(...subtemplates); | ||
} | ||
return result; | ||
} | ||
/** | ||
* Remove a template, link, file or category from the text | ||
@@ -247,49 +462,2 @@ * CAUTION: If an entity with the very same wikitext exists earlier in the text, | ||
/** | ||
* Temporarily hide a part of the string while processing the rest of it. | ||
* | ||
* eg. let u = new bot.wikitext("Hello world <!-- world --> world"); | ||
* u.unbind('<!--','-->'); | ||
* u.content = u.content.replace(/world/g, 'earth'); | ||
* u.rebind(); // gives "Hello earth <!-- world --> earth" | ||
* | ||
* Text within the 'unbinded' part (in this case, the HTML comment) remains intact | ||
* unbind() can be called multiple times to unbind multiple parts of the string. | ||
* | ||
* Attribution: https://en.wikipedia.org/wiki/MediaWiki:Gadget-morebits.js (cc-by-sa 3.0/GFDL) | ||
* @param {string} prefix | ||
* @param {string} postfix | ||
*/ | ||
unbind(prefix, postfix) { | ||
if (!this.unbinder) { | ||
this.unbinder = { | ||
counter: 0, | ||
history: {}, | ||
prefix: '%UNIQ::' + Math.random() + '::', | ||
postfix: '::UNIQ%', | ||
}; | ||
} | ||
let re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g'); | ||
this.text = this.text.replace(re, (match) => { | ||
let current = this.unbinder.prefix + this.unbinder.counter + this.unbinder.postfix; | ||
this.unbinder.history[current] = match; | ||
++this.unbinder.counter; | ||
return current; | ||
}); | ||
} | ||
/** | ||
* Rebind after unbinding. | ||
*/ | ||
rebind() { | ||
let content = this.text; | ||
for (let [current, replacement] of Object.entries(this.unbinder.history)) { | ||
content = content.replace(current, replacement); | ||
} | ||
this.text = content; | ||
return this.text; | ||
} | ||
/** Get the updated text */ | ||
getText() { | ||
return this.text; | ||
} | ||
/** | ||
* Parse the text using the API. | ||
@@ -304,84 +472,2 @@ * @see https://www.mediawiki.org/wiki/API:Parsing_wikitext | ||
/** | ||
* Simple table parser. | ||
* Parses tables provided: | ||
* 1. It doesn't have any merged or joined cells. | ||
* 2. It doesn't use any templates to produce any table markup. | ||
* 3. Further restrictions may apply. | ||
* | ||
* Tables generated via mwn.table() class are intended to be parsable. | ||
* | ||
* This method throws when it finds an inconsistency (rather than silently | ||
* cause undesired behaviour). | ||
* | ||
* @param {string} text | ||
* @returns {Object[]} - each object in the returned array represents a row, | ||
* with its keys being column names, and values the cell content | ||
*/ | ||
static parseTable(text) { | ||
text = text.trim(); | ||
const indexOfRawPipe = function (text) { | ||
// number of unclosed brackets | ||
let tlevel = 0, llevel = 0; | ||
let n = text.length; | ||
for (let i = 0; i < n; i++) { | ||
if (text[i] === '{' && text[i + 1] === '{') { | ||
tlevel++; | ||
i++; | ||
} | ||
else if (text[i] === '[' && text[i + 1] === '[') { | ||
llevel++; | ||
i++; | ||
} | ||
else if (text[i] === '}' && text[i + 1] === '}') { | ||
tlevel--; | ||
i++; | ||
} | ||
else if (text[i] === ']' && text[i + 1] === ']') { | ||
llevel--; | ||
i++; | ||
} | ||
else if (text[i] === '|' && tlevel === 0 && llevel === 0) { | ||
return i; | ||
} | ||
} | ||
}; | ||
if (!text.startsWith('{|') || !text.endsWith('|}')) { | ||
throw new Error('failed to parse table. Unexpected starting or ending'); | ||
} | ||
// remove front matter and final matter | ||
// including table attributes and caption, and unnecessary |- at the top | ||
text = text.replace(/^\{\|.*$((\n\|-)?\n\|\+.*$)?(\n\|-)?/m, '').replace(/^\|\}$/m, ''); | ||
let [header, ...rows] = text.split(/^\|-/m).map((r) => r.trim()); | ||
// remove cell attributes, extracts data | ||
const extractData = (cell) => { | ||
return cell.slice(indexOfRawPipe(cell) + 1).trim(); | ||
}; | ||
// XXX: handle the case where there are is no header row | ||
let cols = header.split('\n').map((e) => e.replace(/^!/, '')); | ||
if (cols.length === 1) { | ||
// non-multilined table? | ||
cols = cols[0].split('!!'); | ||
} | ||
cols = cols.map(extractData); | ||
let numcols = cols.length; | ||
let output = new Array(rows.length); | ||
rows.forEach((row, idx) => { | ||
let cells = row.split(/^\|/m).slice(1); // slice(1) removes the emptiness or the row styles if present | ||
if (cells.length === 1) { | ||
// non-multilined | ||
// cells are separated by || | ||
cells = cells[0].replace(/^\|/, '').split('||'); | ||
} | ||
cells = cells.map(extractData); | ||
if (cells.length !== numcols) { | ||
throw new Error(`failed to parse table: found ${cells.length} cells on row ${idx}, expected ${numcols}`); | ||
} | ||
output[idx] = {}; // output[idx] represents a row | ||
for (let i = 0; i < numcols; i++) { | ||
output[idx][cols[i]] = cells[i]; | ||
} | ||
}); | ||
return output; | ||
} | ||
/** | ||
* Parse sections from wikitext | ||
@@ -397,40 +483,9 @@ * CAUTION: section header syntax in comments, nowiki tags, | ||
parseSections() { | ||
return (this.sections = Wikitext.parseSections(this.text)); | ||
return (this.sections = parseSections(this.text)); | ||
} | ||
// XXX: fix jsdocs | ||
/** | ||
* @inheritdoc | ||
*/ | ||
static parseSections(text) { | ||
const rgx = /^(=+)(.*?)\1/gm; | ||
let sections = [ | ||
{ | ||
level: 1, | ||
header: null, | ||
index: 0, | ||
}, | ||
]; | ||
let match; | ||
while ((match = rgx.exec(text))) { | ||
// eslint-disable-line no-cond-assign | ||
sections.push({ | ||
level: match[1].length, | ||
header: match[2].trim(), | ||
index: match.index, | ||
}); | ||
} | ||
let n = sections.length; | ||
for (let i = 0; i < n - 1; i++) { | ||
sections[i].content = text.slice(sections[i].index, sections[i + 1].index); | ||
} | ||
sections[n - 1].content = text.slice(sections[n - 1].index); | ||
return sections; | ||
} | ||
} | ||
Wikitext.parseTemplates = parseTemplates; | ||
Wikitext.parseTable = parseTable; | ||
Wikitext.parseSections = parseSections; | ||
/**** Private members *****/ | ||
class Stack extends Array { | ||
top() { | ||
return this[this.length - 1]; | ||
} | ||
} | ||
function processLink(self, startIdx, endIdx) { | ||
@@ -472,60 +527,13 @@ let linktext = self.text.slice(startIdx, endIdx + 1); | ||
} | ||
/** | ||
* @param {string} text - template wikitext without braces, with the pipes in | ||
* nested templates replaced by \x01 | ||
* @param {Function} [namePredicate] | ||
* @param {Function} [templatePredicate] | ||
* @returns {Template} | ||
*/ | ||
function processTemplateText(text, namePredicate, templatePredicate) { | ||
// eslint-disable-next-line no-control-regex | ||
const template = new Template('{{' + text.replace(/\x01/g, '|') + '}}'); | ||
// swap out pipe in links with \x01 control character | ||
// [[File: ]] can have multiple pipes, so might need multiple passes | ||
while (/(\[\[[^\]]*?)\|(.*?\]\])/g.test(text)) { | ||
text = text.replace(/(\[\[[^\]]*?)\|(.*?\]\])/g, '$1\x01$2'); | ||
} | ||
const [name, ...parameterChunks] = text.split('|').map((chunk) => { | ||
// change '\x01' control characters back to pipes | ||
// eslint-disable-next-line no-control-regex | ||
return chunk.replace(/\x01/g, '|'); | ||
}); | ||
template.setName(name); | ||
if (namePredicate && !namePredicate(template.name)) { | ||
return null; | ||
} | ||
let unnamedIdx = 1; | ||
parameterChunks.forEach(function (chunk) { | ||
let indexOfEqualTo = chunk.indexOf('='); | ||
let indexOfOpenBraces = chunk.indexOf('{{'); | ||
let isWithoutEquals = !chunk.includes('='); | ||
let hasBracesBeforeEquals = chunk.includes('{{') && indexOfOpenBraces < indexOfEqualTo; | ||
let isUnnamedParam = isWithoutEquals || hasBracesBeforeEquals; | ||
let pName, pNum, pVal; | ||
if (isUnnamedParam) { | ||
// Get the next number not already used by either an unnamed parameter, | ||
// or by a named parameter like `|1=val` | ||
while (template.getParam(unnamedIdx)) { | ||
unnamedIdx++; | ||
} | ||
pNum = unnamedIdx; | ||
pVal = chunk.trim(); | ||
} | ||
else { | ||
pName = chunk.slice(0, indexOfEqualTo).trim(); | ||
pVal = chunk.slice(indexOfEqualTo + 1).trim(); | ||
} | ||
template.addParam(pName || pNum, pVal, chunk); | ||
}); | ||
if (templatePredicate && !templatePredicate(template)) { | ||
return null; | ||
} | ||
return template; | ||
} | ||
function strReplaceAt(string, index, char) { | ||
return string.slice(0, index) + char + string.slice(index + 1); | ||
} | ||
return Wikitext; | ||
} | ||
exports.default = default_1; | ||
class Stack extends Array { | ||
top() { | ||
return this[this.length - 1]; | ||
} | ||
} | ||
function strReplaceAt(string, index, char) { | ||
return string.slice(0, index) + char + string.slice(index + 1); | ||
} | ||
//# sourceMappingURL=wikitext.js.map |
{ | ||
"name": "mwn", | ||
"version": "0.11.0", | ||
"version": "0.11.1", | ||
"description": "JavaScript & TypeScript MediaWiki bot framework for Node.js", | ||
@@ -8,3 +8,3 @@ "main": "./build/bot.js", | ||
"scripts": { | ||
"bump": "node bump-version.js", | ||
"bump": "node scripts/bump-version.js", | ||
"format": "prettier --write .", | ||
@@ -21,3 +21,3 @@ "build": "tsc || echo", | ||
"test:ts": "ts-mocha -p tsconfig.json tests/ts/*", | ||
"docs": "typedoc src/bot.ts --out docs --ignoreCompilerErrors" | ||
"docs": "node scripts/generate-docs.js" | ||
}, | ||
@@ -71,2 +71,4 @@ "engines": { | ||
"mocha": "^8.2.1", | ||
"mocha-chai-jest-snapshot": "^1.1.2", | ||
"nock": "^13.1.0", | ||
"nyc": "^15.1.0", | ||
@@ -77,3 +79,3 @@ "prettier": "^2.2.1", | ||
"ts-mocha": "^8.0.0", | ||
"typedoc": "^0.19.2", | ||
"typedoc": "^0.20.36", | ||
"typescript": "^4.1.3" | ||
@@ -80,0 +82,0 @@ }, |
@@ -9,3 +9,3 @@ # mwn | ||
**Quick links: [Getting Started](#user-content-getting-started) — [GitHub](https://github.com/siddharthvp/mwn) — [NPM](https://www.npmjs.com/package/mwn) — [API Documentation](https://mwn.toolforge.org/docs/classes/_bot_.mwn.html)** | ||
**Quick links: [Getting Started](#user-content-getting-started) — [GitHub](https://github.com/siddharthvp/mwn) — [NPM](https://www.npmjs.com/package/mwn) — [API Documentation](https://mwn.toolforge.org/docs/classes/mwn.html)** | ||
@@ -20,3 +20,3 @@ **Mwn** is a modern and comprehensive MediaWiki bot framework for Node.js, originally adapted from [mwbot](https://github.com/Fannon/mwbot). | ||
Complete API documentation is available **[here](https://tools-static.wmflabs.org/mwn/docs/classes/_bot_.mwn.html)** ([alternative link](https://mwn.toolforge.org/docs/classes/_bot_.mwn.html)). In addition to the MediaWiki Action API, the library also provides methods to talk to the Wikimedia EventStreams API, the ORES API, Pageviews API and WikiWho API. | ||
Complete API documentation is available **[here](https://mwn.toolforge.org/docs/classes/mwn.html)** ([alternative link](https://tools-static.wmflabs.org/mwn/docs/classes/mwn.html)). In addition to the MediaWiki Action API, the library also provides methods to talk to the Wikimedia EventStreams API, the ORES API, Pageviews API and WikiWho API. | ||
@@ -46,3 +46,3 @@ Amongst the major highlights are `batchOperation` and `seriesBatchOperation` which allow you run a large number of tasks with control over concurrency and sleep time between tasks. Failing actions are automatically retried. | ||
If your bot is hosted on [Toolforge](https://tools.wmflabs.org/), note that the system version of node there is v8.11.1. You can install a more recent version of node to your home directory, using: | ||
If your bot is hosted on [Toolforge](https://tools.wmflabs.org/), note that the system version of node there is v8.11.1. You can install a more recent version of node to your home directory using a version manager like [nvm](https://github.com/nvm-sh/nvm) or [n](http://npmjs.com/package/n). Like: | ||
@@ -113,15 +113,12 @@ ```sh | ||
bot.enableEmergencyShutoff({ | ||
page: 'User:ExampleBot/shutoff', // The name of the page to check | ||
intervalDuration: 5000, // check shutoff page every 5 seconds | ||
page: 'User:ExampleBot/shutoff', // The name of the page to check | ||
intervalDuration: 5000, // check shutoff page every 5 seconds | ||
condition: function (pagetext) { | ||
// function to determine whether the bot should continue to run or not | ||
if (pagetext !== 'running') { | ||
// Example implementation: if some one changes the text to something | ||
return false; // other than "running", let's decide to stop! | ||
} else return true; | ||
if (pagetext !== 'running') { // function to determine whether the bot should continue to run or not | ||
return false; // Example implementation: if some one changes the text to something | ||
} else return true; // other than "running", let's decide to stop! | ||
}, | ||
onShutoff: function (pagetext) { | ||
// function to trigger when shutoff is activated | ||
process.exit(); // let's just exit, though we could also terminate | ||
} // any open connections, close files, etc. | ||
onShutoff: function (pagetext) { // function to trigger when shutoff is activated | ||
process.exit(); // let's just exit, though we could also terminate | ||
} // any open connections, close files, etc. | ||
}); | ||
@@ -231,5 +228,5 @@ ``` | ||
bot.setOptions({ | ||
silent: false, // suppress messages (except error messages) | ||
retryPause: 5000, // pause for 5000 milliseconds (5 seconds) on maxlag error. | ||
maxRetries: 3 // attempt to retry a failing requests upto 3 times | ||
silent: false, // suppress messages (except error messages) | ||
retryPause: 5000, // pause for 5000 milliseconds (5 seconds) on maxlag error. | ||
maxRetries: 3 // attempt to retry a failing requests upto 3 times | ||
}); | ||
@@ -358,5 +355,5 @@ ``` | ||
See [list of methods available on page object](https://mwn.toolforge.org/docs/interfaces/_page_.mwnpage.html). | ||
See [list of methods available on page object](https://mwn.toolforge.org/docs/interfaces/mwnpage.html). | ||
[Files](https://mwn.toolforge.org/docs/interfaces/_file_.mwnfile.html#text) and [categories](https://mwn.toolforge.org/docs/interfaces/_category_.mwncategory.html) have their own subclasses that add a few additional methods. | ||
[Files](https://mwn.toolforge.org/docs/interfaces/mwnfile.html) and [categories](https://mwn.toolforge.org/docs/interfaces/mwncategory.html) have their own subclasses that add a few additional methods. | ||
@@ -383,3 +380,3 @@ ### Working with titles | ||
The API of this class is based on that of [mw.Title](https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Title) in the on-site JS interface. See [full list of methods](https://mwn.toolforge.org/docs/interfaces/_title_.mwntitle.html). | ||
The API of this class is based on that of [mw.Title](https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Title) in the on-site JS interface. See [full list of methods](https://mwn.toolforge.org/docs/interfaces/mwntitle.html). | ||
@@ -386,0 +383,0 @@ ### Working with wikitext |
273614
36
6963
25
463