wikiparser-node
Version:
A Node.js parser for MediaWiki markup with AST
500 lines (499 loc) • 23.1 kB
JavaScript
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.AttributeToken = void 0;
const lint_1 = require("../util/lint");
const string_1 = require("../util/string");
const constants_1 = require("../util/constants");
const sharable_1 = require("../util/sharable");
const rect_1 = require("../lib/rect");
const index_1 = __importDefault(require("../index"));
const index_2 = require("./index");
const atom_1 = require("./atom");
/* NOT FOR BROWSER */
const debug_1 = require("../util/debug");
const document_1 = require("../lib/document");
const fixed_1 = require("../mixin/fixed");
const cached_1 = require("../mixin/cached");
const stages = { 'ext-attr': 0, 'html-attr': 2, 'table-attr': 3 }, ariaAttrs = new Set(['aria-describedby', 'aria-flowto', 'aria-labelledby', 'aria-owns']);
const insecureStyle = /expression|(?:accelerator|-o-link(?:-source)?|-o-replace)\s*:|(?:url|src|image(?:-set)?)\s*\(|attr\s*\([^)]+[\s,]url/u, evil = /(?:^|\s|\*\/)(?:javascript|vbscript)(?:\W|$)/iu, complexTypes = new Set(['ext', 'arg', 'magic-word', 'template']), urlAttrs = new Set([
'about',
'property',
'resource',
'datatype',
'typeof',
'itemid',
'itemprop',
'itemref',
'itemscope',
'itemtype',
]);
/**
* attribute of extension and HTML tags
*
* 扩展和HTML标签属性
* @classdesc `{childNodes: [AtomToken, Token|AtomToken]}`
*/
let AttributeToken = (() => {
let _classDecorators = [fixed_1.fixedToken];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
let _classSuper = index_2.Token;
let _instanceExtraInitializers = [];
let _toHtmlInternal_decorators;
var AttributeToken = class extends _classSuper {
static { _classThis = this; }
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_toHtmlInternal_decorators = [(0, cached_1.cached)()];
__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);
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
AttributeToken = _classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
__runInitializers(_classThis, _classExtraInitializers);
}
#type = __runInitializers(this, _instanceExtraInitializers);
#tag;
#equal;
#quotes;
/* NOT FOR BROWSER END */
get type() {
return this.#type;
}
/** tag name / 标签名 */
get tag() {
return this.#tag;
}
/** whether the quotes are balanced / 引号是否匹配 */
get balanced() {
return !this.#equal || this.#quotes[0] === this.#quotes[1];
}
/* NOT FOR BROWSER */
set balanced(value) {
if (value) {
this.close();
}
}
/** attribute value / 属性值 */
get value() {
return this.getValue();
}
set value(value) {
this.setValue(value);
}
/* NOT FOR BROWSER END */
/**
* @param type 标签类型
* @param tag 标签名
* @param key 属性名
* @param equal 等号
* @param value 属性值
* @param quotes 引号
*/
constructor(type, tag, key, equal = '', value, quotes = [], config = index_1.default.getConfig(), accum = []) {
const keyToken = new atom_1.AtomToken(key, 'attr-key', config, accum, type === 'ext-attr' ? { AstText: ':' } : { 'Stage-2': ':', '!ExtToken': '', '!HeadingToken': '' });
let valueToken;
if (key === 'title' || tag === 'img' && key === 'alt') {
valueToken = new index_2.Token(value, config, accum, {
[`Stage-${stages[type]}`]: ':', ConverterToken: ':',
});
valueToken.type = 'attr-value';
valueToken.setAttribute('stage', constants_1.MAX_STAGE - 1);
}
else if (tag === 'gallery' && key === 'caption'
|| tag === 'choose' && (key === 'before' || key === 'after')) {
const newConfig = {
...config,
excludes: [...config.excludes, 'heading', 'html', 'table', 'hr', 'list'],
};
valueToken = new index_2.Token(value, newConfig, accum, {
AstText: ':',
ArgToken: ':',
TranscludeToken: ':',
LinkToken: ':',
FileToken: ':',
CategoryToken: ':',
QuoteToken: ':',
ExtLinkToken: ':',
MagicLinkToken: ':',
ConverterToken: ':',
});
valueToken.type = 'attr-value';
valueToken.setAttribute('stage', 1);
}
else {
valueToken = new atom_1.AtomToken(value, 'attr-value', config, accum, {
[`Stage-${stages[type]}`]: ':',
});
}
super(undefined, config, accum);
this.#type = type;
this.append(keyToken, valueToken);
this.#equal = equal;
this.#quotes = [...quotes];
this.#tag = tag;
this.setAttribute('name', (0, string_1.trimLc)((0, string_1.removeComment)(key)));
}
/** @private */
afterBuild() {
if (this.#equal.includes('\0')) {
this.#equal = this.buildFromStr(this.#equal, constants_1.BuildMethod.String);
}
if (this.parentNode) {
this.#tag = this.parentNode.name;
}
this.setAttribute('name', (0, string_1.trimLc)(this.firstChild.text()));
super.afterBuild();
}
/** @private */
toString(skip) {
const [quoteStart = '', quoteEnd = ''] = this.#quotes;
return this.#equal ? super.toString(skip, this.#equal + quoteStart) + quoteEnd : this.firstChild.toString(skip);
}
/** @private */
text() {
return this.#equal ? `${super.text(`${this.#equal.trim()}"`)}"` : this.firstChild.text();
}
/** @private */
getGaps() {
return this.#equal ? this.#equal.length + (this.#quotes[0]?.length ?? 0) : 0;
}
#lint(start, rect) {
const { firstChild, lastChild, type, name, tag, parentNode } = this, simple = !lastChild.childNodes.some(({ type: t }) => complexTypes.has(t)), value = this.getValue(), attrs = sharable_1.extAttrs[tag], attrs2 = sharable_1.htmlAttrs[tag], { length } = this.toString();
let rule = 'illegal-attr', lintConfig, computeEditInfo;
LINT: { // eslint-disable-line no-unused-labels
({ lintConfig } = index_1.default);
({ computeEditInfo } = lintConfig);
}
if (!attrs?.has(name)
&& !attrs2?.has(name)
// 不是未定义的扩展标签或包含嵌入的HTML标签
&& (type === 'ext-attr' ? attrs || attrs2 : !/\{\{[^{]+\}\}/u.test(name))
&& (type === 'ext-attr' && !attrs2
|| !/^(?:xmlns:[\w:.-]+|data-(?!ooui|mw|parsoid)[^:]*)$/u.test(name)
&& (tag === 'meta' || tag === 'link' || !sharable_1.commonHtmlAttrs.has(name)))
|| (name === 'itemtype' || name === 'itemid' || name === 'itemref')
&& !parentNode?.hasAttr('itemscope')) {
/* PRINT ONLY */
if (start === undefined) {
return true;
}
/* PRINT ONLY END */
LINT: { // eslint-disable-line no-unused-labels
const s = lintConfig.getSeverity(rule, 'unknown');
if (s) {
const e = (0, lint_1.generateForChild)(firstChild, rect, rule, 'illegal-attribute-name', s);
if (computeEditInfo) {
e.suggestions = [(0, lint_1.fixByRemove)(start, length)];
}
return e;
}
}
}
else if (name === 'style' && typeof value === 'string' && insecureStyle.test(value)) {
/* PRINT ONLY */
if (start === undefined) {
return true;
}
/* PRINT ONLY END */
LINT: { // eslint-disable-line no-unused-labels
rule = 'insecure-style';
const s = lintConfig.getSeverity(rule);
return s && (0, lint_1.generateForChild)(lastChild, rect, rule, 'insecure-style', s);
}
}
else if (name === 'tabindex' && typeof value === 'string' && value !== '0') {
/* PRINT ONLY */
if (start === undefined) {
return true;
}
/* PRINT ONLY END */
LINT: { // eslint-disable-line no-unused-labels
const s = lintConfig.getSeverity(rule, 'tabindex');
if (s) {
const e = (0, lint_1.generateForChild)(lastChild, rect, rule, 'nonzero-tabindex', s);
if (computeEditInfo) {
e.suggestions = [
(0, lint_1.fixByRemove)(start, length),
(0, lint_1.fixBy)(e, '0 tabindex', '0'),
];
}
return e;
}
}
}
else if (typeof value === 'string' && ((/^xmlns:[\w:.-]+$/u.test(name) || urlAttrs.has(name)) && evil.test(value)
|| simple
&& (name === 'href' || tag === 'img' && name === 'src')
&& !new RegExp(String.raw `^(?:${this.getAttribute('config').protocol}|//)\S+$`, 'iu')
.test(value))) {
/* PRINT ONLY */
if (start === undefined) {
return true;
}
/* PRINT ONLY END */
LINT: { // eslint-disable-line no-unused-labels
const s = lintConfig.getSeverity(rule, 'value');
return s && (0, lint_1.generateForChild)(lastChild, rect, rule, 'illegal-attribute-value', s);
}
}
else if (simple && type !== 'ext-attr') {
const data = (0, lint_1.provideValues)(tag, name), v = String(value).toLowerCase();
if (data.length > 0 && data.every(n => n !== v)) {
/* PRINT ONLY */
if (start === undefined) {
return true;
}
/* PRINT ONLY END */
LINT: { // eslint-disable-line no-unused-labels
const s = lintConfig.getSeverity(rule, 'value');
return s && (0, lint_1.generateForChild)(lastChild, rect, rule, 'illegal-attribute-value', s);
}
}
}
return false;
}
/** @private */
lint(start = this.getAbsoluteIndex(), re) {
LINT: { // eslint-disable-line no-unused-labels
const errors = super.lint(start, re), { balanced, firstChild, lastChild, name, tag } = this, rect = new rect_1.BoundingRect(this, start), rules = ['unclosed-quote', 'obsolete-attr'], { lintConfig } = index_1.default, s = rules.map(rule => lintConfig.getSeverity(rule, name));
if (s[0] && !balanced) {
const e = (0, lint_1.generateForChild)(lastChild, rect, rules[0], 'unclosed-quotes', s[0]);
e.startIndex--;
e.startCol--;
if (lintConfig.computeEditInfo) {
e.suggestions = [(0, lint_1.fixByClose)(e.endIndex, this.#quotes[0])];
}
errors.push(e);
}
const e = this.#lint(start, rect);
if (e) {
errors.push(e);
}
if (s[1] && sharable_1.obsoleteAttrs[tag]?.has(name)) {
errors.push((0, lint_1.generateForChild)(firstChild, rect, rules[1], 'obsolete-attribute', s[1]));
}
return errors;
}
}
/**
* Get the attribute value
*
* 获取属性值
*/
getValue() {
return this.#equal ? this.lastChild.text().trim() : this.type === 'ext-attr' || '';
}
escape() {
LSP: { // eslint-disable-line no-unused-labels
this.#equal = '{{=}}';
this.lastChild.escape();
}
}
/* PRINT ONLY */
/** @private */
getAttribute(key) {
return key === 'invalid' ? this.#lint() : super.getAttribute(key);
}
/** @private */
print() {
const [quoteStart = '', quoteEnd = ''] = this.#quotes;
return this.#equal ? super.print({ sep: (0, string_1.escape)(this.#equal) + quoteStart, post: quoteEnd }) : super.print();
}
/** @private */
json(_, start = this.getAbsoluteIndex()) {
const json = super.json(undefined, start);
LSP: { // eslint-disable-line no-unused-labels
json['tag'] = this.tag;
return json;
}
}
/* PRINT ONLY END */
/* NOT FOR BROWSER */
cloneNode() {
const [key, value] = this.cloneChildNodes(), k = key.toString(), config = this.getAttribute('config');
return debug_1.Shadow.run(() => {
// @ts-expect-error abstract class
const token = new AttributeToken(this.type, this.tag, k, this.#equal, '', this.#quotes, config);
token.firstChild.safeReplaceWith(key);
token.lastChild.safeReplaceWith(value);
return token;
});
}
/**
* Close the quote
*
* 闭合引号
*/
close() {
const [opening] = this.#quotes;
if (opening) {
this.#quotes[1] = opening;
}
}
/**
* Set the attribute value
*
* 设置属性值
* @param value attribute value / 属性值
* @throws `RangeError` 扩展标签属性不能包含 ">"
* @throws `RangeError` 同时包含单引号和双引号
*/
setValue(value) {
if (value === false) {
this.remove();
return;
}
else if (value === true) {
this.#equal = '';
return;
}
const { type, lastChild } = this;
if (type === 'ext-attr' && value.includes('>')) {
throw new RangeError('Attributes of an extension tag cannot contain ">"!');
}
else if (value.includes('"') && value.includes(`'`)) {
throw new RangeError('Attribute values cannot contain single and double quotes simultaneously!');
}
const config = this.getAttribute('config'), { childNodes } = index_1.default.parse(value, this.getAttribute('include'), stages[type] + 1, config);
lastChild.safeReplaceChildren(childNodes);
if (value.includes('"')) {
this.#quotes = [`'`, `'`];
}
else if (value.includes(`'`) || !this.#quotes[0]) {
this.#quotes = ['"', '"'];
}
else {
this.close();
}
}
/**
* Rename the attribute
*
* 修改属性名
* @param key new attribute name / 新属性名
* @throws `Error` title和alt属性不能更名
*/
rename(key) {
if (this.name === 'title' || this.name === 'alt' && this.tag === 'img') {
throw new Error(`${this.name} attribute cannot be renamed!`);
}
const config = this.getAttribute('config'), { childNodes } = index_1.default.parse(key, this.getAttribute('include'), stages[this.type] + 1, config);
this.firstChild.safeReplaceChildren(childNodes);
}
/** @private */
toHtmlInternal() {
const { type, name, tag, lastChild } = this;
if (type === 'ext-attr' && sharable_1.extAttrs[tag]?.has(name) || this.#lint()) {
return '';
}
const value = lastChild.toHtmlInternal().trim();
if (name === 'id' && !value) {
return '';
}
const sanitized = ariaAttrs.has(name)
? value.split(/\s+/u).filter(Boolean).map(v => (0, string_1.sanitizeAttr)(v, true)).join(' ')
: (0, string_1.sanitizeAttr)(value, name === 'id');
return `${name}="${sanitized}"`;
}
/**
* Get or set the value of a style property
*
* 获取或设置某一样式属性的值
* @param key style property / 样式属性
* @param value style property value / 样式属性值
* @throws `Error` 不是style属性
* @throws `Error` 复杂的style属性
* @throws `Error` 无CSS语言服务
* @since v1.17.1
*/
css(key, value) {
const { name, lastChild } = this;
if (name !== 'style') {
throw new Error('Not a style attribute!');
}
else if (lastChild.length !== 1 || lastChild.firstChild.type !== 'text') {
throw new Error('Complex style attribute!');
}
else if (!document_1.cssLSP) {
throw new Error('CSS language service is not available!');
}
const doc = new document_1.EmbeddedCSSDocument(this.getRootNode(), lastChild), styleSheet = doc.styleSheet, { children: [{ declarations }] } = styleSheet, declaration = declarations.children?.filter(({ property }) => property.getText() === key) ?? [];
if (value === undefined) {
return declaration.at(-1)?.value.getText();
}
else if (typeof value === 'number') {
value = String(value);
}
const style = styleSheet.getText().slice(0, -1);
if (!value) {
if (declaration.length === declarations.children?.length) {
this.setValue('');
}
else if (declaration.length > 0) {
let output = '', start = doc.pre.length;
for (const { offset, length } of declaration) {
output += style.slice(start, offset);
start = offset + length;
}
output += style.slice(start);
this.setValue(output.replace(/^\s*;\s*|;\s*(?=;)/gu, ''));
}
return undefined;
}
const hasQuote = value.includes('"');
if (this.#quotes[0] && value.includes(this.#quotes[0]) || hasQuote && value.includes(`'`)) {
const quote = this.#quotes[0] || '"';
throw new RangeError(`Please consider replacing \`${quote}\` with \`${quote === '"' ? `'` : '"'}\`!`);
}
else if (declaration.length > 0) {
const { value: { offset, length } } = declaration.at(-1);
this.setValue(style.slice(doc.pre.length, offset) + value + style.slice(offset + length));
}
else {
this.setValue(`${style.slice(doc.pre.length)}${/;\s*$/u.test(style) ? '' : '; '}${key}: ${value}`);
}
return undefined;
}
};
return AttributeToken = _classThis;
})();
exports.AttributeToken = AttributeToken;
constants_1.classes['AttributeToken'] = __filename;
;