wikiparser-node v1.6.2
目录
- Parser
- AstElement
- Token
- CommentToken
- ExtToken
- AttributeToken
- HeadingToken
- ArgToken
- TranscludeToken
- ParameterToken
- HtmlToken
- TableToken
- 原型方法
- getRowCount
- getNthRow
- getNthCell
- getFullRow
- getFullCol
- formatTableRow
- formatTableCol
- insertTableRow
- insertTableCol
- insertTableCell
- removeTableRow
- removeTableCol
- mergeCells
- splitIntoRows
- splitIntoCols
- splitIntoCells
- replicateTableRow
- replicateTableCol
- moveTableRowBefore
- moveTableRowAfter
- moveTableColBefore
- moveTableColAfter
- 原型方法
- TdToken
- DoubleUnderscoreToken
- LinkToken
- CategoryToken
- FileToken
- ImageParameterToken
- ExtLinkToken
- MagicLinkToken
- 选择器
- $ (TokenCollection)
Parser
这是解析工具的入口。
var Parser = require('wikiparser-node');
方法
parse(wikitext: string, include?: boolean = false): Token
- 解析维基文本。
var wikitext = '<includeonly>include</includeonly><noinclude>noinclude</noinclude>',
include = Parser.parse(wikitext, true),
noinclude = Parser.parse(wikitext);
assert(include.text() === 'include'); // Token.text()方法只保留有效部分,详见后文Token章节
assert(noinclude.text() === 'noinclude');
isInterwiki(title: string): RegExpMatchArray
- 指定的标题是否是跨维基。
assert(Boolean(Parser.isInterwiki('zhwiki:首页')));
normalizeTitle(title: string, defaultNs?: number = 0): string
- 规范化页面标题。
assert(Parser.normalizeTitle('lj', 10) === 'Template:Lj');
getTool(): typeof $
- 加载批量操作工具。
属性
config: string
- 指定解析设置JSON文件的相对或绝对路径。
assert(Parser.config === './config/default'); // 这是默认设置的相对路径
AstElement
语法树的节点均为字符串或一个仿 HTMLElement 的类 AstElement,这里仅列举这些方法和属性。
原型方法
isEqualNode(node: this): boolean
cloneNode(): this
hasAttribute(key: PropertyKey): boolean
getAttribute(key: PropertyKey): string|undefined
getAttributeNames(): string[]
hasAttributes(): boolean
setAttribte(key: PropertyKey, value: any): this
removeAttribute(key: PropertyKey): void
toggleAttribute(key: PropertyKey, force?: boolean): void
hasChildNodes(): boolean
contains(node: this): boolean
removeChild(node: this): this
appendChild(node: string|this): string|this
append(...elements: string|this): void
insertBefore(node: string|this, reference: this): string|this
prepend(...elements: string|this): void
replaceChild(newChild: string|this, oldChild: this): this
replaceChildren(...elements: string|this): void
after(...element: string|this): void
before(...element: string|this): void
remove(): void
replaceWith(...elements: string|this): void
normalize(): void
getRootNode(): this
addEventListener(type: string, listener: (e: event, data: any) => void, options?: {once: boolean}): void
removeEventListener(type: string, listener: (e: event, data: any) => void): void
dispatchEvent(e: event, data: any): void
matches(selector: string): boolean
comparePosition(other: this): number
closest(selector: string): this|undefined
querySelector(selector: string): this|undefined
querySelectorAll(selector: string): this[]
getBoundingClientRect(): {height: number, width: number, top: number, left: number}
splitText(i: number, offset: number): string
实例属性
childNodes: (string|this)[]
原型属性
children: this[]
firstChild: string|this|undefined
firstElementChild: this|undefined
lastChild string|this|undefined
lastElementChild: this|undefined
isConnected: boolean
parentNode: this|undefined
parentElement: this|undefined
nextSibling: string|this|undefined
nextElementSibling: this|undefined
previousSibling: string|this|undefined
previousElementSibling: this|undefined
hidden: boolean
offsetHeight: number
offsetWidth: number
offsetTop: number
offsetLeft: number
style: {top: number, left: number, height: number, width: number, padding: number}
Token
这是所有解析后的维基文本的基础类。
原型方法
destroy(): void
- 销毁节点,只能对根节点使用。
getAncestors(): Token[]
- 获取所有祖先节点。
var root = Parser.parse('<ref/>'),
attr = root.querySelector('ext-attr');
assert.deepStrictEqual(attr.getAncestors(), [attr.parentElement, root]);
isPlain(): boolean
- 是否是基础类(即未拓展的 Token)。
var root = Parser.parse(wikitext);
assert(root.isPlain() === true); // 根节点总是基础类
toString(): string
- 还原为完整的维基文本。
var root = Parser.parse(wikitext);
assert(root.toString() === wikitext); // 解析是可逆的
text(): string
- 移除不可见的维基文本,包括 HTML 注释、仅用于嵌入的文字(即
<includeonly>
或<onlyinclude>
标签内部)、无效的标签属性等。
var root = Parser.parse('<!-- a -->{{{||b}}}<br */>');
assert(root.text() === '{{{|}}}<br/>');
setText(text: string, i: number): void
- 只能通过这个方法修改指定位置上的纯字符串节点。
var root = Parser.parse('');
root.setText('string', 0);
assert(root.toString() === 'string');
safeReplaceWith(token: Token): void
- 将节点替换为一个同类节点,相当于replaceWith,但适用于父节点有规定的子节点顺序时。
sections(): Token[][]
- 将页面分割为章节,每个章节对应一个 Token 数组。
var root = Parser.parse('a\n==b==\nc\n===d===\n'),
{childNodes} = root,
sections = root.sections();
assert.deepStrictEqual(sections, [childNodes.slice(0, 1), childNodes.slice(1), childNodes.slice(3)]);
section(n: number): Token[]
- 仅获取指定章节。
var root = Parser.parse('a\n==b==\nc\n===d===\n'),
section = root.section(0); // 序言对应的编号为 0
assert.deepStrictEqual(section, [root.firstChild]);
findEnclosingHtml(tag?: string): Token, Token
- 搜索包裹当前Token的HTML标签对,不指定
tag
参数时会搜索任意HTML标签。
var root = Parser.parse('<p>{{a}}</p>'),
template = root.querySelector('template');
assert.deepStrictEqual(template.findEnclosingHtml('p'), [root.firstChild, root.lastChild]);
getCategories(): string, string
- 获取所有分类和对应的排序关键字。
var root = Parser.parse('[[category:a|*]]');
assert.deepStrictEqual(root.getCategories(), [['Category:A', '*']]);
redoQuotes(): void
- 经过一系列编辑操作后,重新局部解析
'
。注意这个方法会忽略所有 Token,只解析当前节点的直接纯文本子节点。
var root = Parser.parse("'''a'''");
root.lastElementChild.setText("''");
assert(root.toString() === "'''a''");
root.redoQuotes();
assert.deepStrictEqual(root.childNodes, ["'", root.firstElementChild, "a", root.lastElementChild]);
实例属性
type: string
- 根节点的值为
root
,其他的基础类节点一般为plain
。
var root = Parser.parse(wikitext);
assert(root.type === 'root');
原型属性
previousVisibleSibling: string|Token|undefined
nextVisibleSibling: string|Token|undefined
CommentToken
HTML 注释。
实例属性
closed: boolean
- 是否闭合。
var root = Parser.parse('<!-- text'),
comment = root.firstChild;
assert(comment.closed === false);
ExtToken
扩展标签。这个类同时混合了 AttributeToken 类的方法。
实例属性
selfClosing: boolean
- 是否自封闭。
var root = Parser.parse('<ref/>'),
ref = root.firstChild;
assert(ref.selfClosing === true);
name: string
- 小写的标签名。
var root = Parser.parse('<REF/>'),
ref = root.firstChild;
assert(ref.name === 'ref');
AttributeToken
扩展和 HTML 标签及表格的属性。
原型方法
hasAttr(key: string): boolean
- 是否带有指定属性。
var root = Parser.parse('<choose uncached before="a"></choose>'),
attr = root.querySelector('ext-attr'); // 扩展标签属性的 type 值为 'ext-attr'
assert(attr.hasAttr('uncached') === true);
assert(attr.hasAttr('before') === true);
getAttr(key: string): string|boolean
- 获取指定属性。
var root = Parser.parse('<choose uncached before="a"></choose>'),
attr = root.querySelector('ext-attr');
assert(attr.getAttr('uncached') === true);
assert(attr.getAttr('before') === 'a');
getAttrNames(): string[]
- 获取属性名列表。
var root = Parser.parse('<ref name="a"/>'),
attr = root.querySelector('ext-attr');
assert.deepStrictEqual(attr.getAttrNames(), ['name']);
hasAttrs(): boolean
- 是否带有至少一条属性。
var root = Parser.parse('<ref/>'),
attr = root.querySelector('ext-attr');
assert(attr.hasAttrs() === false);
setAttr(key: string, value: string|boolean): boolean
- 设置属性。
var root = Parser.parse('<choose></choose>'),
attr = root.querySelector('ext-attr');
assert(attr.setAttr('before', 'a') === true);
assert(attr.setAttr('uncached', true) === true);
assert(root.toString() === '<choose before="a" uncached></choose>');
removeAttr(key: string): void
- 移除指定属性。
var root = Parser.parse('<ref name="a"/>'),
attr = root.querySelector('ext-attr');
attr.removeAttr('name');
assert(root.toString() === '<ref/>');
toggleAttr(key: string): void
- 切换某 Boolean 属性。
var root = Parser.parse('<choose uncached></choose>'),
attr = root.querySelector('ext-attr');
attr.toggleAttr('uncached');
assert(root.toString() === '<choose></choose>');
attr.toggleAttr('uncached');
assert(root.toString() === '<choose uncached></choose>');
sanitize(): void
- 清理无效属性。
var root = Parser.parse('<p ">'),
attr = root.querySelector('html-attr');
attr.sanitize();
assert(root.toString() === '<p>');
实例属性
name: string
- 小写的标签名。
var root = Parser.parse('<REF/>'),
attr = root.querySelector('ext-attr'); // 即使没有设置属性,扩展和 HTML 标签的第一个子节点也总是 AttributeToken
assert(attr.name === 'ref');
HeadingToken
章节标题。
原型方法
setLevel(n: number): void
- 修改标题层级。
var root = Parser.parse('==a=='),
header = root.firstChild;
header.setLevel(3);
assert(root.toString() === '===a===');
实例属性
name: string
- 字符串格式的标题层级。
var root = Parser.parse('==a=='),
header = root.firstChild;
assert(header.name === '2');
ArgToken
被 {{{}}}
包裹的模板参数。
原型方法
setName(name: any): void
- 修改参数名。
var root = Parser.parse('{{{a}}}'),
arg = root.firstChild;
arg.setName('b');
assert(root.toString() === '{{{b}}}');
setDefault(value: any): void
- 设置或修改参数预设值。
var root = Parser.parse('{{{a}}}'),
arg = root.firstChild;
arg.setDefault('b');
assert(root.toString() === '{{{a|b}}}');
实例属性
name: string
- 参数名。
var root = Parser.parse('{{{a}}}'),
arg = root.firstChild;
assert(arg.name === 'a');
TranscludeToken
模板或魔术字。
原型方法
subst(): void
safesubst(): void
- 将引用方式修改为替换引用。
var root = Parser.parse('{{a}}{{!}}'),
template = root.firstChild,
magicWord = root.lastChild;
template.subst();
magicWord.safesubst();
assert(root.toString() === '{{subst:a}}{{safesubst:!}}');
getAllArgs(): ParameterToken[]
- 获取所有参数。
var root = Parser.parse('{{a|b|c=1}}{{#invoke:d|e|f|g=1}}'),
template = root.firstChild,
invoke = root.lastChild;
assert.deepStrictEqual(template.getAllArgs(), template.children.slice(1));
assert.deepStrictEqual(invoke.getAllArgs(), invoke.children.slice(3));
getAnonArgs(): ParameterToken[]
- 获取所有匿名参数。
var root = Parser.parse('{{a|b|c=1}}{{#invoke:d|e|f|g=1}}{{#if:x|y|z}}'),
[template, invoke, magicWord] = root.children;
assert.deepStrictEqual(template.getAnonArgs(), template.children.slice(1, 2));
assert.deepStrictEqual(invoke.getAnonArgs(), invoke.children.slice(3, 4));
assert.deepStrictEqual(magicWord.getAnonArgs(), magicWord.children.slice(1)); // 除#invoke外的魔术字的参数总是视为匿名参数
getArgs(key: string|number): Set\<ParameterToken>
- 获取指定名称的参数(含重复),注意顺序可能不固定。
var root = Parser.parse('{{a|b|1=c}}{{#invoke:d|e|f|1=g}}'),
template = root.firstChild,
invoke = root.lastChild;
assert.deepStrictEqual(template.getArgs(1), new Set(template.children.slice(1)));
assert.deepStrictEqual(invoke.getArgs(1), new Set(invoke.children.slice(3)));
hasArg(key: string|number): boolean
- 是否带有指定参数。
var root = Parser.parse('{{a|b|c=1}}{{#invoke:d|e|f|g=1}}'),
template = root.firstChild,
invoke = root.lastChild;
assert(template.hasArg(1) === true);
assert(template.hasArg('c') === true);
assert(invoke.hasArg(1) === true);
assert(invoke.hasArg('g') === true);
getArg(key: string|number): ParameterToken
- 获取指定名称的有效参数(即最后一个)。
var root = Parser.parse('{{a|b|1=c}}{{#invoke:d|e|1=f|g}}'),
template = root.firstChild,
invoke = root.lastChild;
assert(template.getArg(1) === template.lastChild);
assert(invoke.getArg(1) === invoke.lastChild);
removeArg(key: string|number): void
- 移除指定名称的参数(含重复)。
var root = Parser.parse('{{a|b|1=c}}{{#invoke:d|e|f|1=g}}'),
template = root.firstChild,
invoke = root.lastChild;
template.removeArg(1);
invoke.removeArg(1);
assert(root.toString() === '{{a}}{{#invoke:d|e}}');
getKeys(): string[]
- 获取所有参数名。
var root = Parser.parse('{{a|b=1|c=2}}{{#invoke:d|e|f=1|g=2}}'),
template = root.firstChild,
invoke = root.lastChild;
assert.deepStrictEqual(template.getKeys(), ['b', 'c']);
assert.deepStrictEqual(invoke.getKeys(), ['f', 'g']);
getValues(key: string|number): string[]
- 获取指定名称的参数值(含重复)。
var root = Parser.parse('{{a|b|1=c}}{{#invoke:d|e|f|1=g}}'),
template = root.firstChild,
invoke = root.lastChild;
assert.deepStrictEqual(template.getValues(1), ['b', 'c']);
assert.deepStrictEqual(invoke.getValues(1), ['f', 'g']);
getValue(key: string|number): string
- 获取指定名称的有效参数值(即最后一个)。
var root = Parser.parse('{{a|b|1= c }}{{#invoke:d|e|1=f| g }}'),
template = root.firstChild,
invoke = root.lastChild;
assert(template.getValue(1) === 'c'); // 模板的命名参数不保留首尾的空白字符
assert(invoke.getValue(1) === ' g '); // #invoke魔术字保留匿名参数首位的空白字符
newAnonArg(val: any): void
- 在末尾添加新的匿名参数。
var root = Parser.parse('{{a|b}}{{#invoke:d|e}}'),
template = root.firstChild,
invoke = root.lastChild;
template.newAnonArg(' c ');
invoke.newAnonArg(' f ');
assert(root.toString() === '{{a|b| c }}{{#invoke:d|e| f }}');
setValue(key: string, value: any): void
- 修改或新增参数。
var root = Parser.parse('{{a|b}}{{#invoke:e|f|g=3}}'),
template = root.firstChild,
invoke = root.lastChild;
template.setValue('1', ' c ');
template.setValue('d', ' 2 ');
invoke.setValue('g', ' 4 ');
assert(root.toString() === '{{a| c |d= 2 }}{{#invoke:e|f|g= 4 }}');
anonToNamed(): void
- 将所有匿名参数修改为对应的命名参数。
var root = Parser.parse('{{a| b | c }}{{#invoke:d|e| f }}'),
template = root.firstChild,
invoke = root.lastChild;
template.anonToNamed();
invoke.anonToNamed();
assert(root.toString() === '{{a|1= b |2= c }}{{#invoke:d|e|1= f }}'); // 注意改成命名参数后会参数值的首尾空白字符失效
replaceTemplate(title: string): void
- 更换模板,但保留参数。
var root = Parser.parse('{{a|b|c=1}}'),
template = root.firstChild;
template.replaceTemplate('aa');
assert(root.toString() === '{{aa|b|c=1}}');
hasDuplicatedArgs(): number
- 重复参数计数。
var root = Parser.parse('{{a||1=}}'),
template = root.firstChild;
assert(template.hasDuplicatedArgs() === 1);
getDuplicatedArgs(): [string, Set\<ParameterToken>][]
- 获取全部重复参数,注意顺序可能不固定。
var root = Parser.parse('{{a||1=}}'),
template = root.firstChild;
assert.deepStrictEqual(template.getDuplicatedArgs(), [['1', new Set(template.getAllArgs())]]);
fixDuplication(): string[]
- 尝试修复重复参数,返回值为无法修复的参数名列表。
var root = Parser.parse('{{a|b|1=|1=c}}'),
template = root.firstChild;
assert.deepStrictEqual(template.fixDuplication(), ['1']);
assert(root.toString() === '{{a|b|1=c}}');
escapeTables(): this
- 如果内部包含疑似未转义的表格语法且因此造成了重复参数,则对这些表格进行转义。
var root = Parser.parse('{{a|b=c\n{|\n|rowspan=2|d\n|rowspan=2|e\n|}}}'),
template = root.firstChild;
template.escapeTables();
assert(root.toString() === '{{a|b=c\n{{(!}}\n{{!}}rowspan=2{{!}}d\n{{!}}rowspan=2{{!}}e\n{{!}}}}}');
实例属性
name: string
- 模板名(含名字空间)或魔术字。
var root = Parser.parse('{{a}}{{!}}'),
template = root.firstChild,
magicWord = root.lastChild;
assert(template.name === 'Template:A');
assert(magicWord.name === '!');
modifier: string
- subst 和 safesubst 等。
var root = Parser.parse('<includeonly>{{subst:REVISIONUSER}}</includeonly>', true),
magicWord = root.querySelector('magic-word');
assert(magicWord.modifier === 'subst');
ParameterToken
模板或魔术字的参数。
原型方法
getValue(): string
- 获取参数值。
var root = Parser.parse('{{a| b | c = 1 }}'),
[anonymous, named] = root.querySelectorAll('parameter');
assert(anonymous.getValue() === ' b '); // 模板的匿名参数保留首尾的空白字符
assert(named.getValue() === '1'); // 模板的命名参数不保留首尾的空白字符
setValue(value: any): void
- 设置参数值。
var root = Parser.parse('{{a|b=1}}'),
param = root.querySelector('parameter');
param.setValue(' 2 ');
assert(root.toString() === '{{a|b= 2 }}'); // setValue方法总是保留空白字符,哪怕是无效的
rename(key: string, force: boolean): void
- 重命名参数,可选是否在导致重复参数时抛出错误。
var root = Parser.parse('{{a|b=1|c=2}}'),
param = root.querySelector('parameter');
try {
param.rename('c');
throw new Error();
} catch (e) {
assert(e.message === '参数更名造成重复参数:c');
}
实例属性
name: string
- 参数名。
var root = Parser.parse('{{a|b| c = 1}}'),
[anonymous, named] = root.querySelectorAll('parameter');
assert(anonymous.name === '1');
assert(named.name === 'c');
anon: boolean
- 是否是匿名参数。
var root = Parser.parse('{{a|b| c = 1}}'),
[anonymous, named] = root.querySelectorAll('parameter');
assert(anonymous.anon === true);
assert(named.anon === false);
HtmlToken
HTML标签,未进行匹配。这个类同时混合了 AttributeToken 类的方法。
原型方法
replaceTag(tag: string): void
- 修改标签。
var root = Parser.parse('<b>'),
html = root.firstChild;
html.replaceTag('i');
assert(root.toString() === '<i>');
findMatchingTag(): HtmlToken
- 搜索匹配的另一个标签,找不到或无效自封闭时会抛出不同错误。
var root = Parser.parse('<p><b/></i><u></u><br>'),
[p, b, i, u, u2, br] = root.children;
try {
p.findMatchingTag();
throw new Error();
} catch (e) {
assert(e.message === '未闭合的标签:<p>');
}
try {
b.findMatchingTag();
throw new Error();
} catch (e) {
assert(e.message === '无效自封闭标签:<b/>');
}
try {
i.findMatchingTag();
throw new Error();
} catch (e) {
assert(e.message === '未匹配的闭合标签:</i>');
}
assert(u.findMatchingTag() === u2);
assert(br.findMatchingTag() === br);
fix(): void
- 尝试修复无效自封闭标签,无法修复时会抛出错误。
var root = Parser.parse('<b>a<b/><div style="height:1em"/>');
for (const html of root.querySelectorAll('html')) {
html.fix();
}
assert(root.toString() === '<b>a</b><div style="height:1em"></div>');
实例属性
name: string
- 小写的标签名。
var root = Parser.parse('<b>'),
html = root.firstChild;
assert(html.name === 'b');
closing: boolean
- 是否是闭合标签。
var root = Parser.parse('</b>'),
html = root.firstChild;
assert(html.closing === true);
selfClosing: boolean
- 是否是自闭合标签(可能不符合HTML5规范)。
var root = Parser.parse('<b/>'),
html = root.firstChild;
assert(html.selfClosing === true);
TableToken
表格。
原型方法
getRowCount(): number
- 表格的有效行数。
var root = Parser.parse('{|\n|\n|-\n|}'),
table = root.firstChild;
assert(table.getRowCount() === 1); // 表格首行可以直接写在 TableToken 下,没有内容的表格行是无效行。
getNthRow(n: number): TrToken
- 第
n
个有效行。
var root = Parser.parse('{|\n|rowspan=2| ||\n|-\n|\n|}'),
table = root.firstChild;
assert(table.getNthRow(0) === table);
assert(table.getNthRow(1) === table.querySelector('tr'));
表格示意 |
---|
第 0 行第 0 行第 1 行 |
getNthCell(coords: {row: number, column: number}|{x: number, y: number}): TdToken
- 获取指定位置的单元格,指定位置可以基于 raw HTML 或渲染后的表格。
var root = Parser.parse('{|\n|rowspan=2 colspan=2| ||\n|-\n|\n|-\n| || ||\n|}'),
table = root.firstChild,
td = table.querySelector('td');
assert(table.getNthCell({row: 0, column: 0}) === td);
assert(table.getNthCell({x: 1, y: 1}) === td); // 该单元格跨了 2 行 2 列
(row, column) | (y, x) |
---|---|
(0, 0)(0, 1)(1, 0)(2, 0)(2, 1)(2, 2) | (0, 0), (0, 1)(1, 0), (1, 1)(0, 2)(1, 2)(2, 0)(2, 1)(2, 2) |
getFullRow(y: number): Map\<TdToken, boolean>
- 获取一行的所有单元格。
var root = Parser.parse('{|\n|rowspan=2| ||\n|-\n|\n|}'),
table = root.firstChild,
[a,, c] = table.querySelectorAll('td');
assert.deepStrictEqual(table.getFullRow(1), new Map([[a, false], [c, true]])); // 起始位置位于该行的单元格值为 `true`
表格 | 选中第 1 行 |
---|---|
false true |
getFullCol(x: number): Map\<TdToken, boolean>
- 获取一列的所有单元格。
var root = Parser.parse('{|\n|colspan=2|\n|-\n| ||\n|}'),
table = root.firstChild,
[a,, c] = table.querySelectorAll('td');
assert.deepStrictEqual(table.getFullCol(1), new Map([[a, false], [c, true]])); // 起始位置位于该列的单元格值为 `true`
表格 | 选中第 1 列 |
---|---|
false true |
formatTableRow(y: number, attr: string|Record\<string, string|boolean>, multiRow?: boolean = false): void
- 批量设置一行单元格的属性。
var root = Parser.parse('{|\n|rowspan=2| ||\n|-\n|\n|}'),
table = root.firstChild;
table.formatTableRow(1, 'th'); // `multiRow` 为假时忽略起始位置不在该行的单元格
assert(root.toString() === '{|\n|rowspan=2| ||\n|-\n!\n|}');
原表格 | 将第 1 行设为<th> multiRow = false | 将第 1 行设为<th> multiRow = true |
---|---|---|
tdtdtd | tdtdth | thtdth |
formatTableCol(x: number, attr: string|Record\<string, string|boolean>, multiCol?: boolean = false): void
- 批量设置一列单元格的属性。
var root = Parser.parse('{|\n|colspan=2|\n|-\n| ||\n|}'),
table = root.firstChild;
table.formatTableCol(1, 'th', true); // `multiCol` 为真时包含起始位置不在该列的单元格
assert(root.toString() === '{|\n!colspan=2|\n|-\n| \n!\n|}');
原表格 | 将第 1 列设为<th> multiCol = false | 将第 1 列设为<th> multiCol = true |
---|---|---|
tdtdtd | tdtdth | thtdth |
insertTableRow(row: number, attr: Record\<string, string|boolean>, inner?: string, subtype?: 'td'|'th', innerAttr?: Record\<string, string|boolean>): TrToken
- 插入空行或一行单元格。
var root = Parser.parse('{|\n|a||rowspan=2|b||c\n|-\n|d||e\n|}'),
table = root.firstChild;
table.insertTableRow(1, {class: 'tr'}); // 不填写 `inner` 等后续参数时会插入一个空行,且此时是无效行。
table.insertTableRow(1, {}, 'f', 'th', {class: 'th'});
assert(root.toString() === '{|\n|a|| rowspan="3"|b||c\n|- class="tr"\n|-\n! class="th"|f\n! class="th"|f\n|-\n|d||e\n|}');
原表格 | 插入 1 行 |
---|---|
abcde | abcffde |
insertTableCol(x: number, inner: string, subtype: 'td'|'th', attr: Record\<string, string|boolean>): void
- 插入一列单元格。
var root = Parser.parse('{|\n|colspan=2|a\n|-\n|b||c\n|}'),
table = root.firstChild;
table.insertTableCol(1, 'd', 'th', {class: 'th'});
assert(root.toString() === '{|\n| colspan="3"|a\n|-\n|b\n! class="th"|d\n|c\n|}');
原表格 | 插入 1 列 |
---|---|
abc | abdc |
insertTableCell(inner: string, coords: {row: number, column: number}|{x: number, y: number}, subtype: 'td'|'th', attr: Record\<string, string|boolean>): TdToken
- 插入一个单元格。
var root = Parser.parse('{|\n|rowspan=2 colspan=2|a||b\n|-\n|c\n|-\n|d||e||f\n|}'),
table = root.firstChild;
table.insertTableCell('g', {row: 0, column: 2}, 'th');
table.insertTableCell('h', {x: 2, y: 1}, 'th', {rowspan: 2});
assert(root.toString() === '{|\n|rowspan=2 colspan=2|a||b\n!g\n|-\n! rowspan="2"|h\n|c\n|-\n|d||e||f\n|}');
原表格 | 插入 2 格 |
---|---|
abcdef | abghcdef |
removeTableRow(y: number): TrToken
- 删除一行。
var root = Parser.parse('{|\n|rowspan=2|a||b||c\n|-\n!rowspan=2 class="th"|d||e\n|-\n|f||g\n|}'),
table = root.firstChild;
table.removeTableRow(1);
assert(root.toString() === '{|\n|a||b||c\n|-\n|f\n! class="th"|\n|g\n|}'); // 自动调整跨行单元格的 rowspan
原表格 | 删除第 1 行 |
---|---|
abcdefg | abcfg |
removeTableCol(x: number): void
- 删除一列。
var root = Parser.parse('{|\n|colspan=2|a||b\n|-\n|c\n!colspan=2 class="th"|d\n|-\n|e\n!f\n|g\n|}'),
table = root.firstChild;
table.removeTableCol(1);
assert(root.toString() === '{|\n|a||b\n|-\n|c\n! class="th"|\n|-\n|e\n|g\n|}'); // 自动调整跨列单元格的 colspan
原表格 | 删除第 1 列 |
---|---|
abcdefg | abceg |
mergeCells(xlim: number, number, ylim: number, number): TdToken
- 合并单元格。
var root = Parser.parse('{|\n!a\n|b||c\n|-\n|d||e||f\n|-\n|g||h||i\n|}'),
table = root.firstChild;
table.mergeCells([0, 2], [0, 2]); // 被合并的单元格的属性和内部文本均会丢失
assert(root.toString() === '{|\n! rowspan="2" colspan="2"|a\n|c\n|-\n|f\n|-\n|g||h||i\n|}');
原表格 | 合并单元格 |
---|---|
abcdefghi | acfghi |
splitIntoRows(coords: {row: number, column: number}|{x: number, y: number}): void
- 将单元格分裂到不同行。
var root = Parser.parse('{|\n|a||b\n!rowspan=3|c\n|-\n|d\n|-\n|e||f\n|}'),
table = root.firstChild;
table.splitIntoRows({x: 2, y: 0});
assert(root.toString() === '{|\n|a||b\n!c\n|-\n|d\n|-\n|e||f\n!\n|}'); // 第 1 行由于缺失第 1 列的单元格,分裂后的第 2 列不会保留
原表格 | 按行分裂单元格 |
---|---|
abcdef | abcdef |
splitIntoCols(coords: {row: number, column: number}|{x: number, y: number}): void
- 将单元格分裂到不同列。
var root = Parser.parse('{|\n!colspan=2 class="th"|a\n|}'),
table = root.firstChild;
table.splitIntoCols({x: 1, y: 0});
assert(root.toString() === '{|\n! class="th"|a\n! class="th"|\n|}'); // 分裂继承属性
原表格 | 按列分裂单元格 |
---|---|
a | a |
splitIntoCells(coords: {row: number, column: number}|{x: number, y: number}): void
- 将单元格分裂成最小单元格。
var root = Parser.parse('{|\n!rowspan=2 colspan=2|a\n|b\n|-\n|c\n|-\n|d||e||f\n|}'),
table = root.firstChild;
table.splitIntoCells({x: 0, y: 0});
assert(root.toString() === '{|\n!a\n!\n|b\n|-\n!\n!\n|c\n|-\n|d||e||f\n|}');
原表格 | 分裂单元格 |
---|---|
abcdef | abcdef |
replicateTableRow(row: number): TrToken
- 复制一行并插入该行之前
var root = Parser.parse('{|\n|rowspan=2|a||b||c\n|-\n!rowspan=2|d||e\n|-\n|f||g\n|}'),
table = root.firstChild;
table.replicateTableRow(1);
assert(root.toString() === '{|\n| rowspan="3"|a||b||c\n|-\n!d||e\n|-\n!rowspan=2|d||e\n|-\n|f||g\n|}'); // 复制行内的单元格`rowspan`总是为1
原表格 | 复制第 1 行 |
---|---|
abcdefg | abcdedefg |
replicateTableCol(x: number): TdToken[]
- 复制一列并插入该列之前
var root = Parser.parse('{|\n|colspan=2|a||b\n|-\n|c\n!colspan=2|d\n|-\n|e\n!f\n|g\n|}'),
table = root.firstChild;
table.replicateTableCol(1);
assert(root.toString() === '{|\n| colspan="3"|a||b\n|-\n|c\n!d\n!colspan=2|d\n|-\n|e\n!f\n!f\n|g\n|}'); // 复制列内的单元格`colspan`总是为1
原表格 | 复制第 1 列 |
---|---|
abcdefg | abcddeffg |
moveTableRowBefore(y: number, before: number): TrToken
- 移动表格行。
var root = Parser.parse('{|\n|rowspan=2|a||b||c||d\n|-\n|colspan=2|e||f\n|-\n|rowspan=2|g||h||i||j\n|-\n!rowspan=2|k||colspan=2|l\n|-\n|m||n||o\n|}'),
table = root.firstChild;
table.moveTableRowBefore(3, 1);
assert(root.toString() === '{|\n| rowspan="3"|a||b||c||d\n|-\n!k||colspan=2|l\n|-\n|colspan=2|e||f\n|-\n|g||h||i||j\n|-\n|m\n!\n|n||o\n|}');
原表格 | 移动第 3 行至第 1 行前 |
---|---|
abcdefghijklmno | abcdklefghijmno |
moveTableRowAfter(y: number, after: number): TrToken
- 移动表格行。
var root = Parser.parse('{|\n|rowspan=2|a||colspan=2|b||c\n|-\n|d||e||f\n|-\n|rowspan=2|g||h||i||j\n|-\n!rowspan=2|k||colspan=2|l\n|-\n|m||n||o\n|}'),
table = root.firstChild;
table.moveTableRowAfter(3, 0);
assert(root.toString() === '{|\n| rowspan="3"|a||colspan=2|b||c\n|-\n!k||colspan=2|l\n|-\n|d||e||f\n|-\n|g||h||i||j\n|-\n|m\n!\n|n||o\n|}');
原表格 | 移动第 3 行至第 0 行后 |
---|---|
abcdefghijklmno | abckldefghijmno |
moveTableColBefore(x: number, before: number): void
- 移动表格列。
var root = Parser.parse('{|\n|colspan=2|a||colspan=2|b||c\n|-\n|d||rowspan=2|e||f\n!colspan=2|g\n|-\n|h||i\n!rowspan=2|j\n|k\n|-\n|l||m||n||o\n|}'),
table = root.firstChild;
table.moveTableColBefore(3, 1);
assert(root.toString() === '{|\n| colspan="3"|a||b||c\n|-\n|d\n!g\n|rowspan=2|e||f\n!\n|-\n|h\n!rowspan=2|j\n|i\n|k\n|-\n|l||m||n||o\n|}');
原表格 | 移动第 3 列至第 1 列前 |
---|---|
abcdefghijklmno | abcdgefhjiklmno |
moveTableColAfter(x: number, after: number): void
- 移动表格列。
var root = Parser.parse('{|\n|colspan=2|a||colspan=2|b||c\n|-\n|rowspan=2|d||e||f\n!colspan=2|g\n|-\n|h||i\n!rowspan=2|j\n|k\n|-\n|l||m||n||o\n|}'),
table = root.firstChild;
table.moveTableColAfter(3, 0);
assert(root.toString() === '{|\n| colspan="3"|a||b||c\n|-\n|rowspan=2|d\n!g\n|e||f\n!\n|-\n!rowspan=2|j\n|h||i\n|k\n|-\n|l||m||n||o\n|}');
原表格 | 移动第 3 列至第 0 列后 |
---|---|
abcdefghijklmno | abcdgefjhiklmno |
TdToken
表格单元格。
原型属性
subtype: 'td'|'th'|'caption'
var root = Parser.parse('{|\n|+\n!\n|\n|}'),
[caption, th, td] = root.querySelectorAll('td');
assert(caption.subtype === 'caption');
assert(th.subtype === 'th');
assert(td.subtype === 'td');
rowspan: number
var root = Parser.parse('{|\n|\n|}'),
td = root.querySelector('td');
assert(td.rowspan === 1);
colspan: number
var root = Parser.parse('{|\n|\n|}'),
td = root.querySelector('td');
assert(td.colspan === 1);
DoubleUnderscoreToken
状态开关。
实例属性
name: string
- 小写的状态开关名。
var root = Parser.parse('__NOTOC__'),
doubleUnderscore = root.firstChild;
assert(doubleUnderscore.name === 'notoc');
LinkToken
内链,包括跨维基链接。
原型方法
setTarget(link: string): void
- 修改内链目标。
var root = Parser.parse('[[a]]'),
link = root.firstChild;
link.setTarget('b');
assert(root.toString() === '[[:b]]'); // 自动在开头添加':'
setFragment(fragment: string): void
- 不改变目标页面,仅修改 fragment。
var root = Parser.parse('[[:file:a]]'),
link = root.firstChild;
link.setFragment('b');
assert(root.toString() === '[[:File:A#b]]'); // 这个方法会同时规范化页面名
asSelfLink(fragment?: string): void
- 当原链接带有或指定 fragment 时,将内链修改为 selfLink 格式。
var root = Parser.parse('[[a#b]]'),
link = root.firstChild;
link.asSelfLink();
assert(root.toString() === '[[#b]]');
setLinkText(linkText?: string)
- 修改链接文本。
var root = Parser.parse('[[a]]'),
link = root.firstChild;
link.setLinkText('b');
assert(root.toString() === '[[a|b]]');
link.removeAt(1); // 若要移除链接文本,直接使用removeAt方法即可
assert(root.toString() === '[[a]]');
pipeTrick(): void
- 模拟解析器预转换的 pipe trick。
var root = Parser.parse('[[help:a (b)]]'),
link = root.firstChild;
link.pipeTrick();
assert(root.toString() === '[[help:a (b)|a]]');
实例属性
name: string
- 规范化的目标页面名称。
var root = Parser.parse('[[:文件:a]]'),
link = root.firstChild;
assert(link.name === 'File:A');
selfLink: boolean
- 是否是 selfLink。
var root = Parser.parse('[[#a]]'),
link = root.firstChild;
assert(link.selfLink === true);
fragment: string
- URL 解码后的 fragment。
var root = Parser.parse('[[#.7B.7D]]'), // 兼容 MediaWiki 式的 fragment 编码
link = root.firstChild;
assert(link.fragment === '{}');
interwiki: string
- 跨维基前缀。
var root = Parser.parse('[[zhwiki:a]]'),
link = root.firstChild;
assert(link.interwiki === 'zhwiki');
CategoryToken
分类。这个类继承了 LinkToken 类。
原型方法
setSortkey(text: string): void
- 修改分类关键字,实际上就是 setLinkText 方法的别名。
var root = Parser.parse('[[category:a]]'),
category = root.firstChild;
category.setSortkey('*');
assert(root.toString() === '[[category:a|*]]');
实例属性
sortkey: string
- 分类关键字。
var root = Parser.parse('[[category:a|*]]'),
category = root.firstChild;
assert(category.sortkey === '*');
FileToken
文件。这个类继承了 LinkToken 类。
原型方法
getAllArgs(): ImageParameterToken[]
- 获取所有图片参数,类似 TranscludeToken.getAllArgs 方法。
var root = Parser.parse('[[file:a|thumb|1px|link=b|alt=c|d]]'),
file = root.firstChild;
assert.deepStrictEqual(file.getAllArgs(), file.children.slice(1));
getArgs(key: string): Set\<ImageParameterToken>
- 获取指定的图片参数,类似 TranscludeToken.getArgs 方法。
var root = Parser.parse('[[file:a|link=b|链接=c]]'), // 这里故意使用一个错误语法的例子,请勿模仿
file = root.firstChild;
assert.deepStrictEqual(file.getArgs('link'), new Set(file.children.slice(1)));
hasArg(key: string): boolean
- 是否带有指定的图片参数,类似 TranscludeToken.hasArg 方法。
var root = Parser.parse('[[file:a|b]]'),
file = root.firstChild;
assert(file.hasArg('caption') === true);
getArg(key: string): ImageParameterToken
- 获取最后一个指定的图片参数,类似 TranscludeToken.getArg 方法。
var root = Parser.parse('[[file:a|link=b|链接=c]]'), // 这里故意使用一个错误语法的例子,请勿模仿
file = root.firstChild;
assert(file.getArg('link'), file.lastChild);
removeArg(key: string): void
- 移除指定的图片参数,类似 TranscludeToken.removeArg 方法。
var root = Parser.parse('[[file:a|link=b|链接=c]]'), // 这里故意使用一个错误语法的例子,请勿模仿
file = root.firstChild;
file.removeArg('link');
assert(root.toString() === '[[file:a]]');
getKeys(): string[]
- 获取所有图片参数名,类似 TranscludeToken.getKeys 方法。
var root = Parser.parse('[[file:a|thumb|1px|link=b|alt=c|d]]'),
file = root.firstChild;
assert.deepStrictEqual(file.getKeys(), ['thumbnail', 'width', 'link', 'alt', 'caption']);
getValue(key: string): string|true
- 获取指定的图片参数值,类似 TranscludeToken.getValue 方法。
var root = Parser.parse('[[file:a|thumb|100px]]'),
file = root.firstChild;
assert(file.getValue('thumbnail') === true);
assert(file.getValue('width') === '100');
setValue(key: string, value: string|boolean): void
- 修改或设置指定的图片参数,类似 TranscludeToken.setValue 方法。
var root = Parser.parse('[[file:a|thumb]]'),
file = root.firstChild;
file.setValue('thumbnail', false);
file.setValue('width', '100');
file.setValue('framed', true);
assert(root.toString() === '[[file:a|100px|framed]]');
ImageParameterToken
图片参数。
原型方法
getValue(): string|true
- 获取图片参数值,类似 ParameterToken.getValue 方法。
var root = Parser.parse('[[file:a|thumb|100px]]'),
[thumbnail, width] = root.querySelectorAll('image-parameter');
assert(thumbnail.getValue() === true);
assert(width.getValue() === '100');
setValue(value: string|boolean): void
- 修改或移除图片参数,类似 ParameterToken.setValue 方法。
var root = Parser.parse('[[file:a|thumb|100px]]'),
[thumbnail, width] = root.querySelectorAll('image-parameter');
thumbnail.setValue(false);
width.setValue('x100');
assert(root.toString() === '[[file:a|x100px]]');
ExtLinkToken
[]
内的外部链接。
原型方法
getUrl(): URL
setTarget(url: string|URL): void
setLinkText(text: string): void
- 修改外链文本。
var root = Parser.parse('[//example.org example]'),
extlink = root.firstChild;
extlink.setLinkText(''); // 清空外链文本
assert(root.toString() === '[//example.org]');
原型属性
protocol: string
MagicLinkToken
自由外链。
原型方法
getUrl(): URL
- 生成一个 URL 对象,以方便解析和修改外链目标。
setTarget(url: string|URL): void
- 修改外链目标。可以和 getUrl 方法联合使用(见以下示例)。
var root = Parser.parse('https://www.mediawiki.org/wiki/Manual:Parser.php'),
magiclink = root.firstChild,
url = magiclink.getUrl();
url.searchParams.set('action', 'info');
magiclink.setTarget(url);
assert(root.toString() === 'https://www.mediawiki.org/wiki/Manual:Parser.php?action=info');
原型属性
protocol: string
- 外链协议。
var root = Parser.parse('ftp://example.org'),
magiclink = root.firstChild;
assert(magiclink.protocol === 'ftp://');
magiclink.protocol = 'https://';
assert(root.toString() === 'https://example.org');
选择器
Token 选择器的设计仿照了 CSS 和 jQuery 的选择器。
type
- 类比 CSS tag 选择器。
var root = Parser.parse(wikitext);
assert(root.matches('root') === true)
name
- 类比 CSS id 选择器。
var root = Parser.parse('<ref/>'),
ref = root.firstChild;
assert(ref.matches('#ref') === true);
属性
- 类比 CSS 属性选择器。
var root = Parser.parse('<!-- --><ref name="abc"/>'),
comment = root.firstChild,
attr = root.querySelector('ext-attr');
assert(comment.matches('[closed]') === true); // 非 AttributeToken 的属性选择器对应自身属性
assert(attr.matches('[name^=a]') === true); // AttributeToken 的属性选择器对应维基文本中的属性
assert(attr.matches('[name$=c]') === true);
assert(attr.matches('[name*=b]') === true);
assert(attr.matches('[name!=x]') === true);
assert(attr.matches('[name=abc]') === true);
伪选择器
- 类比 CSS 和 jQuery 伪选择器。
var root = Parser.parse('text <!--'),
comment = root.lastChild;
assert(root.matches(':root') === true);
assert(root.matches(':is(root)') === true);
assert(comment.matches(':not(root)') === true);
assert(comment.matches(':nth-child(2)') === true);
assert(comment.matches(':nth-last-of-type(1)') === true);
assert(comment.matches(':last-child') === true);
assert(comment.matches(':first-of-type') === true);
assert(root.matches(':only-child') === true);
assert(comment.matches(':only-of-type') === true);
assert(root.matches(':contains(text)') === true);
assert(root.matches(':has(comment)') === true);
assert(root.matches(':parent') === true);
assert(comment.matches(':empty') === true);
assert(comment.matches(':hidden') === true);
assert(root.matches(':visible') === true);
$ (TokenCollection)
这是一个仿 jQuery 的批量操作工具,这里仅列举方法和属性。
toArray(): (string|Token)[]
get(num: number): string|Token
each(callback: function(this: string|Token, number, string|Token): void): this
map(callback: function(this: string|Token, number, string|Token): any): any[]|TokenCollection
slice(start: number, end: number): TokenCollection
first(): TokenCollection
last(): TokenCollection
eq(i: number): TokenCollection
toString(): string
text(text?: string): string|this
is(selector: string): boolean
filter(selector: string|function(this: string|Token, number, string|Token): boolean): TokenCollection
not(selector: string|function(this: string|Token, number, string|Token): boolean): TokenCollection
find(selector: string): TokenCollection
has(selector: string): boolean
closest(selector?: string): TokenCollection
index(): number
add(elements: string|Token|TokenCollection|(string|Token)[]): TokenCollection
addBack(selector?: string): TokenCollection
parent(selector?: string): TokenCollection
parents(selector?: string): TokenCollection
parentsUntil(selector?: string, filter?: string): TokenCollection
next(selector?: string): TokenCollection
nextAll(selector?: string): TokenCollection
nextUntil(selector?: string, filter?: string): TokenCollection
prev(selector?: string): TokenCollection
prevAll(selector?: string): TokenCollection
prevUntil(selector?: string, filter?: string): TokenCollection
siblings(selector?: string): TokenCollection
children(selector?: string): TokenCollection
contents(): TokenCollection
data(key: string|Record\<string, any>, value?: any): this|any
removeData(name: string|string[]): this
on(events: string|Record\<string, (e: Event, data: any) => any, selector: string, handler: (e: Event, data: any) => any): this
one(events: string|Record\<string, (e: Event, data: any) => any, selector: string, handler: (e: Event, data: any) => any): this
off(events: string|Record\<string, (e: Event, data: any) => any, selector: string, handler: (e: Event, data: any) => any): this
trigger(event: Event, data?: any): this
tiggerHandler(event: Event, data?: any): any
append(content: string|Token|TokenCollection|(string|Token)[]|function(this: Token, number, string): string|Token|TokenCollection|(string|Token)[]): this
prepend(content: string|Token|TokenCollection|(string|Token)[]|function(this: Token, number, string): string|Token|TokenCollection|(string|Token)[]): this
before(content: string|Token|TokenCollection|(string|Token)[]|function(this: Token, number, string): string|Token|TokenCollection|(string|Token)[]): this
after(content: string|Token|TokenCollection|(string|Token)[]|function(this: Token, number, string): string|Token|TokenCollection|(string|Token)[]): this
html(content: string|Token|TokenCollection|(string|Token)[]|function(this: Token, number, string): string|Token|TokenCollection|(string|Token)[]): this
replaceWith(content: string|Token|TokenCollection|(string|Token)[]|function(this: Token, number, string): string|Token|TokenCollection|(string|Token)[]): this
remove(selector?: string): this
detach(selector?: string): this
empty(): this
appendTo(target: Token|Token[]): this
prependTo(target: Token|Token[]): this
insertBefore(target: Token|Token[]): this
insertAfter(target: Token|Token[]): this
replaceAll(target: Token|Token[]): this
val(value: string|string[]|function(this: Token, number, string): string): this
attr(name: string|Record\<string, string>, value?: string): this|string
removeAttr(name: string): this
prop(name: string|Record\<string, any>, value?: any): this|any
removeProp(name: string): this
wrapAll(wrapper: string[]|function(this: Token, string): string[]): this
wrapInner(wrapper: string[]|function(this: Token, string): string[]): this
wrap(wrapper: string[]|function(this: Token, string): string[]): this
5 days ago
14 days ago
21 days ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
5 months ago
6 months ago
6 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago