wikiparser-node
Version:
A Node.js parser for MediaWiki markup with AST
952 lines (951 loc) • 39.7 kB
JavaScript
"use strict";
// PHP解析器的步骤:
// -2. 替换签名和`{{subst:}}`,参见Parser::preSaveTransform;这在revision中不可能保留,可以跳过
// -1. 移除特定字符`\0`和`\x7F`,参见Parser::parse
// 0. 重定向,参见WikitextContentHandler::extractRedirectTargetAndText
// 1. 注释/扩展标签('<'相关),参见Preprocessor_Hash::buildDomTreeArrayFromText和Sanitizer::decodeTagAttributes
// 2. 模板/模板变量/标题,注意rightmost法则,以及`-{`和`[[`可以破坏`{{`或`{{{`语法,
// 参见Preprocessor_Hash::buildDomTreeArrayFromText
// 3. HTML标签(允许不匹配),参见Sanitizer::internalRemoveHtmlTags
// 4. 表格,参见Parser::handleTables
// 5. 水平线、状态开关和余下的标题,参见Parser::internalParse
// 6. 内链,含文件和分类,参见Parser::handleInternalLinks2
// 7. `'`,参见Parser::doQuotes
// 8. 外链,参见Parser::handleExternalLinks
// 9. ISBN、RFC和自由外链,参见Parser::handleMagicLinks
// 10. 段落和列表,参见BlockLevelPass::execute
// 11. 转换,参见LanguageConverter::recursiveConvertTopLevel
// \0\d+.\x7F标记Token:
// !: `{{!}}`专用
// {: `{{(!}}`专用
// }: `{{!)}}`专用
// -: `{{!-}}`专用
// +: `{{!!}}`专用
// ~: `{{=}}`专用
// a: AttributeToken
// b: TableToken
// c: CommentToke
// d: ListToken
// e: ExtToken或OnlyincludeToken
// f: ImageParameterToken内的MagicLinkToken
// g: TranslateToken
// h: HeadingToken
// i: RFC/PMID/ISBN
// l: LinkToken
// m: `{{server}}`、`{{fullurl:}}`、`{{canonicalurl:}}`或`{{filepath:}}`
// n: NoIncludeToken、IncludeToken、DoubleUnderscoreToken或`{{#vardefine:}}`
// o: RedirectToken
// q: QuoteToken
// r: HrToken
// s: `{{{|subst:}}}`
// t: ArgToken或TranscludeToken
// u: `__toc__`
// v: ConverterToken
// w: ExtLinkToken或free-ext-link
// x: HtmlToken
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Token = void 0;
const string_1 = require("../util/string");
const constants_1 = require("../util/constants");
const lint_1 = require("../util/lint");
const debug_1 = require("../util/debug");
const index_1 = __importDefault(require("../index"));
const element_1 = require("../lib/element");
const text_1 = require("../lib/text");
/* NOT FOR BROWSER */
const strict_1 = __importDefault(require("assert/strict"));
const html_1 = require("../util/html");
const ranges_1 = require("../lib/ranges");
const range_1 = require("../lib/range");
const readOnly_1 = require("../mixin/readOnly");
const cached_1 = require("../mixin/cached");
/* NOT FOR BROWSER END */
/* NOT FOR BROWSER ONLY */
const document_1 = require("../lib/document");
const lsp_1 = require("../lib/lsp");
const lintSelectors = ['category', 'html-attr#id,ext-attr#id,table-attr#id'];
/* NOT FOR BROWSER */
/**
* 可接受的Token类型
* @param value 可接受的Token类型
*/
const getAcceptable = (value) => {
const acceptable = {};
for (const [k, v] of Object.entries(value)) {
if (k.startsWith('Stage-')) {
for (let i = 0; i <= Number(k.slice(6)); i++) {
for (const type of constants_1.aliases[i]) {
acceptable[type] = new ranges_1.Ranges(v);
}
}
}
else if (k.startsWith('!')) { // `!`项必须放在最后
delete acceptable[k.slice(1)];
}
else {
acceptable[k] = new ranges_1.Ranges(v);
}
}
return acceptable;
};
/* NOT FOR BROWSER END */
/**
* base class for all tokens
*
* 所有节点的基类
* @classdesc `{childNodes: (AstText|Token)[]}`
*/
let Token = (() => {
let _classSuper = element_1.AstElement;
let _instanceExtraInitializers = [];
let _lint_decorators;
let _safeReplaceWith_decorators;
let _toHtmlInternal_decorators;
return class Token extends _classSuper {
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_lint_decorators = [(0, readOnly_1.readOnly)(true)];
_safeReplaceWith_decorators = [(0, readOnly_1.readOnly)()];
_toHtmlInternal_decorators = [(0, cached_1.cached)()];
__esDecorate(this, null, _lint_decorators, { kind: "method", name: "lint", static: false, private: false, access: { has: obj => "lint" in obj, get: obj => obj.lint }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _safeReplaceWith_decorators, { kind: "method", name: "safeReplaceWith", static: false, private: false, access: { has: obj => "safeReplaceWith" in obj, get: obj => obj.safeReplaceWith }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _toHtmlInternal_decorators, { kind: "method", name: "toHtmlInternal", static: false, private: false, access: { has: obj => "toHtmlInternal" in obj, get: obj => obj.toHtmlInternal }, metadata: _metadata }, null, _instanceExtraInitializers);
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
#type = (__runInitializers(this, _instanceExtraInitializers), 'plain');
/** 解析阶段,参见顶部注释。只对plain Token有意义。 */
#stage = 0;
#config;
/** 这个数组起两个作用:1. 数组中的Token会在build时替换`/\0\d+.\x7F/`标记;2. 数组中的Token会依次执行parseOnce和build方法。 */
#accum;
#include;
#built = false;
#string;
/* NOT FOR BROWSER */
#acceptable;
#protectedChildren = new ranges_1.Ranges();
/* NOT FOR BROWSER END */
get type() {
return this.#type;
}
set type(value) {
/* NOT FOR BROWSER */
const plainTypes = [
'plain',
'root',
'table-inter',
'arg-default',
'attr-value',
'ext-link-text',
'heading-title',
'parameter-key',
'parameter-value',
'link-text',
'td-inner',
'ext-inner',
'list-range',
'translate-inner',
];
if (!plainTypes.includes(value)) {
throw new RangeError(`"${value}" is not a valid type for ${this.constructor.name}!`);
}
/* NOT FOR BROWSER END */
this.#type = value;
}
/** @class */
constructor(wikitext, config = index_1.default.getConfig(), accum = [], acceptable) {
super();
if (typeof wikitext === 'string') {
this.insertAt(wikitext);
}
this.#config = config;
this.#accum = accum;
accum.push(this);
/* NOT FOR BROWSER */
this.setAttribute('acceptable', acceptable);
}
/** @private */
parseOnce(n = this.#stage, include = false, tidy) {
if (n < this.#stage || this.length === 0 || !this.isPlain()) {
return this;
}
else if (this.#stage >= constants_1.MAX_STAGE) {
/* NOt FOR BROWSER */
if (this.type === 'root') {
index_1.default.error('Fully parsed!');
}
/* NOT FOR BROWSER END */
return this;
}
switch (n) {
case 0:
if (this.type === 'root') {
this.#accum.pop();
const isRedirect = this.#parseRedirect();
include &&= !isRedirect;
}
this.#include = include;
this.#parseCommentAndExt(include);
break;
case 1:
this.#parseBraces();
break;
case 2:
this.#parseHtml();
break;
case 3:
this.#parseTable();
break;
case 4:
this.#parseHrAndDoubleUnderscore();
break;
case 5:
this.#parseLinks(tidy);
break;
case 6:
this.#parseQuotes(tidy);
break;
case 7:
this.#parseExternalLinks();
break;
case 8:
this.#parseMagicLinks();
break;
case 9:
this.#parseList();
break;
case 10:
this.#parseConverter();
// no default
}
if (this.type === 'root') {
for (const token of this.#accum) {
token?.parseOnce(n, include, tidy); // eslint-disable-line @typescript-eslint/no-unnecessary-condition
}
}
this.#stage++;
return this;
}
/** @private */
buildFromStr(str, type) {
const nodes = str.split(/[\0\x7F]/u).map((s, i) => {
if (i % 2 === 0) {
return new text_1.AstText(s);
}
else if (isNaN(s.slice(-1))) {
return this.#accum[Number(s.slice(0, -1))];
}
throw new Error(`Failed to build! Unrecognized token: ${s}`);
});
if (type === constants_1.BuildMethod.String) {
return nodes.map(String).join('');
}
else if (type === constants_1.BuildMethod.Text) {
return (0, string_1.text)(nodes);
}
return nodes;
}
/** @private */
build() {
this.#stage = constants_1.MAX_STAGE;
const { length, firstChild } = this, str = firstChild?.toString();
if (length === 1 && firstChild.type === 'text' && str.includes('\0')) {
(0, debug_1.setChildNodes)(this, 0, 1, this.buildFromStr(str));
this.normalize();
if (this.type === 'root') {
for (const token of this.#accum) {
token?.build(); // eslint-disable-line @typescript-eslint/no-unnecessary-condition
}
}
}
}
/** @private */
afterBuild() {
if (this.type === 'root') {
for (const token of this.#accum) {
token?.afterBuild(); // eslint-disable-line @typescript-eslint/no-unnecessary-condition
}
}
this.#built = true;
}
/** @private */
parse(n = constants_1.MAX_STAGE, include, tidy) {
n = Math.min(n, constants_1.MAX_STAGE);
while (this.#stage < n) {
this.parseOnce(this.#stage, include, tidy);
}
if (n) {
this.build();
this.afterBuild();
}
return this;
}
/** 解析重定向 */
#parseRedirect() {
const { parseRedirect } = require('../parser/redirect');
const wikitext = this.firstChild.toString(), parsed = parseRedirect(wikitext, this.#config, this.#accum);
if (parsed) {
this.setText(parsed);
}
return Boolean(parsed);
}
/**
* 解析HTML注释和扩展标签
* @param includeOnly 是否嵌入
*/
#parseCommentAndExt(includeOnly) {
const { parseCommentAndExt } = require('../parser/commentAndExt');
this.setText(parseCommentAndExt(this.firstChild.toString(), this.#config, this.#accum, includeOnly));
}
/** 解析花括号 */
#parseBraces() {
const { parseBraces } = require('../parser/braces');
const str = this.type === 'root' ? this.firstChild.toString() : `\0${this.firstChild.toString()}`, parsed = parseBraces(str, this.#config, this.#accum);
this.setText(this.type === 'root' ? parsed : parsed.slice(1));
}
/** 解析HTML标签 */
#parseHtml() {
if (this.#config.excludes.includes('html')) {
return;
}
const { parseHtml } = require('../parser/html');
this.setText(parseHtml(this.firstChild.toString(), this.#config, this.#accum));
}
/** 解析表格 */
#parseTable() {
if (this.#config.excludes.includes('table')) {
return;
}
const { parseTable } = require('../parser/table');
this.setText(parseTable(this, this.#config, this.#accum));
}
/** 解析`<hr>`和状态开关 */
#parseHrAndDoubleUnderscore() {
if (this.#config.excludes.includes('hr')) {
return;
}
const { parseHrAndDoubleUnderscore } = require('../parser/hrAndDoubleUnderscore');
this.setText(parseHrAndDoubleUnderscore(this, this.#config, this.#accum));
}
/**
* 解析内部链接
* @param tidy 是否整理
*/
#parseLinks(tidy) {
const { parseLinks } = require('../parser/links');
this.setText(parseLinks(this.firstChild.toString(), this.#config, this.#accum, tidy));
}
/**
* 解析单引号
* @param tidy 是否整理
*/
#parseQuotes(tidy) {
if (this.#config.excludes.includes('quote')) {
return;
}
const { parseQuotes } = require('../parser/quotes');
const lines = this.firstChild.toString().split('\n');
for (let i = 0; i < lines.length; i++) {
lines[i] = parseQuotes(lines[i], this.#config, this.#accum, tidy);
}
this.setText(lines.join('\n'));
}
/** 解析外部链接 */
#parseExternalLinks() {
if (this.#config.excludes.includes('extLink')) {
return;
}
const { parseExternalLinks } = require('../parser/externalLinks');
this.setText(parseExternalLinks(this.firstChild.toString(), this.#config, this.#accum));
}
/** 解析自由外链 */
#parseMagicLinks() {
if (this.#config.excludes.includes('magicLink')) {
return;
}
const { parseMagicLinks } = require('../parser/magicLinks');
this.setText(parseMagicLinks(this.firstChild.toString(), this.#config, this.#accum));
}
/** 解析列表 */
#parseList() {
if (this.#config.excludes.includes('list')) {
return;
}
const { parseList } = require('../parser/list');
const { firstChild, type, name } = this, lines = firstChild.toString().split('\n'), state = { lastPrefix: '' };
let i = type === 'root' || type === 'ext-inner' && name === 'poem' ? 0 : 1;
for (; i < lines.length; i++) {
lines[i] = parseList(lines[i], state, this.#config, this.#accum);
}
this.setText(lines.join('\n'));
}
/** 解析语言变体转换 */
#parseConverter() {
if (this.#config.variants.length > 0) {
const { parseConverter } = require('../parser/converter');
this.setText(parseConverter(this.firstChild.toString(), this.#config, this.#accum));
}
}
/** @private */
isPlain() {
return this.constructor === Token;
}
/** @private */
getAttribute(key) {
switch (key) {
case 'config':
return this.#config;
case 'include':
return (this.#include ?? Boolean(this.getRootNode().#include));
case 'accum':
return this.#accum;
case 'built':
return this.#built;
case 'stage':
return this.#stage;
/* PRINT ONLY */
case 'invalid':
return (this.type === 'table-inter' && (0, lint_1.isFostered)(this) === 2);
/* PRINT ONLY END */
/* NOT FOR BROWSER */
case 'protectedChildren':
return this.#protectedChildren;
/* NOT FOR BROWSER END */
default:
return super.getAttribute(key);
}
}
/** @private */
setAttribute(key, value) {
switch (key) {
case 'stage':
if (this.#stage === 0 && this.type === 'root') {
this.#accum.shift();
}
this.#stage = value;
break;
/* NOT FOR BROWSER */
case 'acceptable':
this.#acceptable = value
&& (() => getAcceptable(value));
break;
case 'include':
this.#include = value;
break;
/* NOT FOR BROWSER END */
default:
super.setAttribute(key, value);
}
}
insertAt(child, i = this.length) {
const token = typeof child === 'string' ? new text_1.AstText(child) : child;
/* NOT FOR BROWSER */
const acceptable = this.getAcceptable();
if (!debug_1.Shadow.running && acceptable) {
const { length, childNodes } = this, nodesAfter = childNodes.slice(i), insertedName = token.constructor.name;
i += i < 0 ? length : 0;
if (!acceptable[insertedName]?.has(i, length + 1)) {
this.constructorError(`cannot insert a ${insertedName} at position ${i}`);
}
else if (nodesAfter.some(({ constructor: { name } }, j) => !acceptable[name]?.has(i + j + 1, length + 1))) {
this.constructorError(`violates the order of acceptable nodes by inserting a child node at position ${i}`);
}
}
/* NOT FOR BROWSER END */
super.insertAt(token, i);
const { type,
/* NOT FOR BROWSER */
constructor, } = token;
/* NOT FOR BROWSER */
const e = new Event('insert', { bubbles: true });
this.dispatchEvent(e, { type: 'insert', position: i < 0 ? i + this.length - 1 : i });
if (type !== 'list-range' && constructor === Token && this.isPlain()) {
index_1.default.warn('You are inserting a plain token as a child of another plain token. '
+ 'Consider calling Token.flatten method afterwards.');
}
/* NOT FOR BROWSER END */
if (type === 'root') {
token.type = 'plain';
}
return token;
}
/** @private */
normalizeTitle(title, defaultNs = 0, opt) {
return index_1.default.normalizeTitle(title, defaultNs, this.#include, this.#config, opt);
}
/** @private */
inTableAttrs() {
return this.closest('table-attrs,ext')?.type === 'table-attrs' && (this.closest('table-attrs,arg,magic-word,template')?.is('table-attrs')
? 2
: 1);
}
/** @private */
inHtmlAttrs() {
return this.closest('html-attrs,ext')?.is('html-attrs')
? 2
: this.inTableAttrs();
}
/** @private */
lint(start = this.getAbsoluteIndex(), re) {
LINT: { // eslint-disable-line no-unused-labels
const { lintConfig } = index_1.default, { computeEditInfo, fix: needFix, ignoreDisables, configurationComment } = lintConfig;
let errors = super.lint(start, re);
if (this.type === 'root') {
const record = new Map(), r = 'no-duplicate', s = ['category', 'id'].map(key => lintConfig.getSeverity(r, key)), wikitext = this.toString(), selector = lintSelectors.filter((_, i) => s[i]).join();
if (selector) {
for (const cat of this.querySelectorAll(selector)) {
let key;
if (cat.is('category')) {
key = cat.name;
}
else {
const value = cat.getValue();
if (value && value !== true) {
key = `#${value}`;
}
}
if (key) {
const thisCat = record.get(key);
if (thisCat) {
thisCat.add(cat);
}
else {
record.set(key, new Set([cat]));
}
}
}
for (const [key, value] of record) {
if (value.size > 1 && !key.startsWith('#mw-customcollapsible-')) {
const isCat = !key.startsWith('#'), msg = `duplicate-${isCat ? 'category' : 'id'}`, severity = s[isCat ? 0 : 1];
errors.push(...[...value].map(cat => {
const e = (0, lint_1.generateForSelf)(cat, { start: cat.getAbsoluteIndex() }, r, msg, severity);
if (computeEditInfo && isCat) {
e.suggestions = [(0, lint_1.fixByRemove)(e)];
}
return e;
}));
}
}
}
if (!ignoreDisables) {
const regex = new RegExp(String.raw `<!--\s*${configurationComment}-(disable(?:(?:-next)?-line)?|enable)(\s[\sa-z,-]*)?-->`, 'gu'), ignores = [];
let mt = regex.exec(wikitext);
while (mt) {
const { 1: type, index } = mt, detail = mt[2]?.trim();
ignores.push({
line: this.posFromIndex(index).top + (type === 'disable-line' ? 0 : 1),
from: type === 'disable' ? regex.lastIndex : undefined,
to: type === 'enable' ? regex.lastIndex : undefined,
rules: detail ? new Set(detail.split(',').map(rule => rule.trim())) : undefined,
});
mt = regex.exec(wikitext);
}
errors = errors.filter(({ rule, startLine, startIndex }) => {
const nearest = { pos: 0 };
for (const { line, from, to, rules } of ignores) {
if (line > startLine + 1) {
break;
}
else if (rules && !rules.has(rule)) {
continue;
}
else if (line === startLine && from === undefined && to === undefined) {
return false;
}
else if (from <= startIndex && from > nearest.pos) {
nearest.pos = from;
nearest.type = 'from';
}
else if (to <= startIndex && to > nearest.pos) {
nearest.pos = to;
nearest.type = 'to';
}
}
return nearest.type !== 'from';
});
}
if (needFix && errors.some(({ fix }) => fix)) {
// 倒序修复,跳过嵌套的修复
const fixable = errors.map(({ fix }) => fix).filter(Boolean).sort(({ range: [aFrom, aTo] }, { range: [bFrom, bTo] }) => aTo === bTo ? bFrom - aFrom : bTo - aTo);
let i = Infinity, output = wikitext;
for (const { range: [from, to], text: t } of fixable) {
if (to <= i) {
output = output.slice(0, from) + t + output.slice(to);
i = from;
}
}
Object.assign(errors, { output });
}
if (!computeEditInfo) {
for (const e of errors) {
delete e.fix;
delete e.suggestions;
}
}
/* NOT FOR BROWSER ONLY */
}
else if ((0, lsp_1.isAttr)(this, true)) {
const rule = 'invalid-css', s = lintConfig.getSeverity(rule), sWarn = lintConfig.getSeverity(rule, 'warn');
if (s || sWarn) {
const root = this.getRootNode(), textDoc = new document_1.EmbeddedCSSDocument(root, this);
errors.push(...document_1.cssLSP.doValidation(textDoc, textDoc.styleSheet)
.filter(({ code, severity }) => code !== 'css-ruleorselectorexpected' && code !== 'emptyRules'
&& (severity === 1 ? s : sWarn))
.map(({ range: { start: { line, character }, end }, message, severity, code }) => ({
code: code,
rule,
message,
severity: (severity === 1 ? s : sWarn),
startLine: line,
startCol: character,
startIndex: root.indexFromPos(line, character),
endLine: end.line,
endCol: end.character,
endIndex: root.indexFromPos(end.line, end.character),
})));
}
/* NOT FOR BROWSER ONLY END */
}
return errors;
}
}
/** @private */
toString(skip, separator) {
return skip
? super.toString(true, separator)
: (0, lint_1.cache)(this.#string, () => super.toString(false, separator), value => {
const root = this.getRootNode();
if (root.type === 'root' && root.#built) {
this.#string = value;
}
});
}
/* NOT FOR BROWSER */
/** @private */
print(opt) {
return this.is('list-range') ? (0, string_1.print)(this.childNodes) : super.print(opt);
}
/** @private */
getAcceptable() {
if (typeof this.#acceptable === 'function') {
this.#acceptable = this.#acceptable();
}
return this.#acceptable;
}
/** @private */
dispatchEvent(e, data) {
if (this.#built) {
super.dispatchEvent(e, data);
}
}
/** @private */
protectChildren(args) {
this.#protectedChildren.push(...new ranges_1.Ranges(args));
}
/** @private */
concat(elements) {
if (elements.length === 0) {
return;
}
const { childNodes, lastChild } = this, first = elements[0], last = elements.at(-1), parent = first.parentNode, nodes = parent.getChildNodes();
nodes.splice(nodes.indexOf(first), elements.length);
parent.setAttribute('childNodes', nodes);
first.previousSibling?.setAttribute('nextSibling', last.nextSibling);
last.nextSibling?.setAttribute('previousSibling', first.previousSibling);
for (const element of elements) {
element.setAttribute('parentNode', this);
}
lastChild?.setAttribute('nextSibling', first);
first.setAttribute('previousSibling', lastChild);
last.setAttribute('nextSibling', undefined);
this.setAttribute('childNodes', [...childNodes, ...elements]);
}
/**
* @override
* @param i position of the child node / 移除位置
*/
removeAt(i) {
const { length, childNodes } = this;
i += i < 0 ? length : 0;
if (!debug_1.Shadow.running) {
if (this.#protectedChildren.has(i, length)) {
this.constructorError(`cannot remove the child node at position ${i}`);
}
const acceptable = this.getAcceptable();
if (acceptable) {
const nodesAfter = childNodes.slice(i + 1);
if (nodesAfter.some(({ constructor: { name } }, j) => !acceptable[name]?.has(i + j, length - 1))) {
this.constructorError(`violates the order of acceptable nodes by removing the child node at position ${i}`);
}
}
}
const node = super.removeAt(i);
const e = new Event('remove', { bubbles: true });
this.dispatchEvent(e, { type: 'remove', position: i, removed: node });
return node;
}
/**
* Replace with a token of the same type
*
* 替换为同类节点
* @param token token to be replaced with / 待替换的节点
* @throws `Error` 不存在父节点
*/
safeReplaceWith(token) {
const { parentNode } = this;
if (!parentNode) {
throw new Error('The node does not have a parent node!');
}
else if (token.constructor !== this.constructor) {
this.typeError('safeReplaceWith', this.constructor.name);
}
try {
strict_1.default.deepEqual(token.getAcceptable(), this.getAcceptable());
}
catch (e) {
if (e instanceof strict_1.default.AssertionError) {
this.constructorError('has a different #acceptable property');
}
throw e;
}
const i = parentNode.childNodes.indexOf(this);
super.removeAt.call(parentNode, i);
super.insertAt.call(parentNode, token, i);
if (token.type === 'root') {
token.type = 'plain';
}
const e = new Event('replace', { bubbles: true });
token.dispatchEvent(e, { type: 'replace', position: i, oldToken: this });
}
/**
* Create an HTML comment
*
* 创建HTML注释
* @param data comment content / 注释内容
*/
createComment(data) {
require('../addon/token');
return this.createComment(data);
}
/**
* Create a tag
*
* 创建标签
* @param tagName tag name / 标签名
* @param options options / 选项
* @param options.selfClosing whether to be a self-closing tag / 是否自封闭
* @param options.closing whether to be a closing tag / 是否是闭合标签
* @throws `RangeError` 非法的标签名
*/
createElement(tagName, options) {
require('../addon/token');
return this.createElement(tagName, options);
}
/**
* Create a text node
*
* 创建纯文本节点
* @param data text content / 文本内容
*/
createTextNode(data = '') {
return new text_1.AstText(data);
}
/**
* Create an AstRange object
*
* 创建AstRange对象
*/
createRange() {
return new range_1.AstRange();
}
/**
* Check if a title is an interwiki link
*
* 判断标题是否是跨维基链接
* @param title title / 标题
*/
isInterwiki(title) {
return index_1.default.isInterwiki(title, this.#config);
}
/** @private */
cloneChildNodes() {
return this.childNodes.map(child => child.cloneNode());
}
/**
* Deep clone the node
*
* 深拷贝节点
*/
cloneNode() {
if (this.constructor !== Token) {
this.constructorError('does not specify a cloneNode method');
}
const cloned = this.cloneChildNodes();
return debug_1.Shadow.run(() => {
const token = new Token(undefined, this.#config, [], this.getAcceptable());
token.type = this.type;
token.setAttribute('stage', this.#stage);
token.setAttribute('include', Boolean(this.#include));
token.setAttribute('name', this.name);
token.safeAppend(cloned);
token.protectChildren(this.#protectedChildren);
return token;
});
}
/**
* Get all sections
*
* 获取全部章节
*/
sections() {
require('../addon/token');
return this.sections();
}
/**
* Get a section
*
* 获取指定章节
* @param n rank of the section / 章节序号
*/
section(n) {
return this.sections()?.[n];
}
/**
* Get the enclosing HTML tags
*
* 获取指定的外层HTML标签
* @param tag HTML tag name / HTML标签名
* @throws `RangeError` 非法的标签或空标签
*/
findEnclosingHtml(tag) {
require('../addon/token');
return this.findEnclosingHtml(tag);
}
/**
* Get all categories
*
* 获取全部分类
*/
getCategories() {
return this.querySelectorAll('category').map(({ name, sortkey }) => [name, sortkey]);
}
/**
* Expand templates
*
* 展开模板
* @since v1.10.0
*/
expand() {
require('../addon/token');
return this.expand();
}
/**
* Parse some magic words
*
* 解析部分魔术字
*/
solveConst() {
require('../addon/token');
return this.solveConst();
}
/**
* Merge plain child tokens of a plain token
*
* 合并普通节点的普通子节点
*/
flatten() {
if (this.isPlain()) {
for (const child of this.childNodes) {
if (child.type !== 'text' && child.isPlain()) {
child.insertAdjacent(child.childNodes, 1);
child.remove();
}
}
}
}
/**
* Generate HTML
*
* 生成HTML
* @since v1.10.0
*/
toHtml() {
require('../addon/token');
return this.toHtml();
}
/**
* 构建列表
* @param recursive 是否递归
*/
#buildLists(recursive) {
for (let i = 0; i < this.length; i++) {
const child = this.childNodes[i];
if (child.is('list') || child.is('dd')) {
child.getRange();
}
else if (recursive && child.type !== 'text') {
child.#buildLists(true);
}
}
}
/**
* Build lists
*
* 构建列表
* @since v1.17.1
*/
buildLists() {
this.#buildLists(true);
}
/** @private */
toHtmlInternal(opt) {
for (const child of this.childNodes) {
if (child.type === 'text') {
child.removeBlankLines();
}
}
this.#buildLists();
this.normalize();
return (0, html_1.html)(this.childNodes, '', opt);
}
};
})();
exports.Token = Token;
constants_1.classes['Token'] = __filename;