UNPKG

wikiparser-node

Version:

A Node.js parser for MediaWiki markup with AST

528 lines (527 loc) 24 kB
"use strict"; 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.FileToken = void 0; const lint_1 = require("../../util/lint"); const rect_1 = require("../../lib/rect"); const index_1 = __importDefault(require("../../index")); const base_1 = require("./base"); const imageParameter_1 = require("../imageParameter"); /* NOT FOR BROWSER */ const string_1 = require("../../util/string"); const debug_1 = require("../../util/debug"); const constants_1 = require("../../util/constants"); const title_1 = require("../../lib/title"); const cached_1 = require("../../mixin/cached"); const frame = new Map([ ['manualthumb', 'Thumb'], ['frameless', 'Frameless'], ['framed', 'Frame'], ['thumbnail', 'Thumb'], ]), argTypes = new Set(['arg']), transclusion = new Set(['template', 'magic-word']), horizAlign = new Set(['left', 'right', 'center', 'none']), vertAlign = new Set(['baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom']), extensions = new Set(['tiff', 'tif', 'png', 'gif', 'jpg', 'jpeg', 'webp', 'xcf', 'pdf', 'svg', 'djvu']); /** * a more sophisticated string-explode function * @param str string to be exploded */ const explode = (str) => { if (str === undefined) { return []; } const regex = /-\{|\}-|\|/gu, exploded = []; let mt = regex.exec(str), depth = 0, lastIndex = 0; while (mt) { const { 0: match, index } = mt; if (match !== '|') { depth += match === '-{' ? 1 : -1; } else if (depth === 0) { exploded.push(str.slice(lastIndex, index)); ({ lastIndex } = regex); } mt = regex.exec(str); } exploded.push(str.slice(lastIndex)); return exploded; }; /** * filter out the image parameters that are not of the specified type * @param args image parameter tokens * @param types token types to be filtered */ const filterArgs = (args, types) => args.filter(({ childNodes }) => { const visibleNodes = childNodes.filter(node => node.text().trim()); return visibleNodes.length !== 1 || !types.has(visibleNodes[0].type); }); /** * image * * 图片 * @classdesc `{childNodes: [AtomToken, ...ImageParameterToken[]]}` */ let FileToken = (() => { let _classSuper = base_1.LinkBaseToken; let _instanceExtraInitializers = []; let _toHtmlInternal_decorators; return class FileToken extends _classSuper { 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); if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); } /* NOT FOR BROWSER END */ get type() { return 'file'; } /** * file extension * * 扩展名 * @since v1.5.3 */ get extension() { LSP: return this.getAttribute('title').extension; // eslint-disable-line no-unused-labels } /* NOT FOR BROWSER */ /** image link / 图片链接 */ get link() { return this.getArg('link')?.link ?? super.link; } set link(value) { this.setValue('link', value); } /** image size / 图片大小 */ get size() { const fr = this.getFrame(); return fr === 'framed' || fr instanceof title_1.Title ? undefined : this.getArg('width')?.size; } set size(size) { this.setValue('width', size && size.width + (size.height && 'x') + size.height); } /** image width / 图片宽度 */ get width() { return this.is('gallery-image') ? this.parentNode?.widths.toString() : this.size?.width; } set width(width) { const arg = this.getArg('width'); if (arg) { arg.width = width; } else { this.setValue('width', width); } } /** image height / 图片高度 */ get height() { return this.is('gallery-image') ? this.parentNode?.heights.toString() : this.size?.height; } set height(height) { const arg = this.getArg('width'); if (arg) { arg.height = height; } else { this.setValue('width', height && `x${height}`); } } /* NOT FOR BROWSER END */ /** * @param link 文件名 * @param text 图片参数 * @param delimiter `|` */ constructor(link, text, config, accum = [], delimiter = '|') { super(link, undefined, config, accum, delimiter); __runInitializers(this, _instanceExtraInitializers); /* NOT FOR BROWSER */ this.setAttribute('acceptable', { AtomToken: 0, ImageParameterToken: '1:' }); /* NOT FOR BROWSER END */ const { extension } = this.getTitle(true, true); /-\{|\}-|\|/gu; // eslint-disable-line @typescript-eslint/no-unused-expressions this.safeAppend(explode(text).map( // @ts-expect-error abstract class (part) => new imageParameter_1.ImageParameterToken(part, extension, config, accum))); } /** @private */ lint(start = this.getAbsoluteIndex(), re) { LINT: { // eslint-disable-line no-unused-labels const errors = super.lint(start, re), args = filterArgs(this.getAllArgs(), argTypes), keys = [...new Set(args.map(({ name }) => name))], frameKeys = keys.filter(key => frame.has(key)), horizAlignKeys = keys.filter(key => horizAlign.has(key)), vertAlignKeys = keys.filter(key => vertAlign.has(key)), [fr] = frameKeys, unscaled = fr === 'framed' || fr === 'manualthumb', rect = new rect_1.BoundingRect(this, start), { lintConfig } = index_1.default, { computeEditInfo, fix } = lintConfig, { ns, extension, /* NOT FOR BROWSER */ interwiki, } = this.getAttribute('title'), { firstChild } = this; let rule = 'nested-link', s = lintConfig.getSeverity(rule, 'file'); if (s && extensions.has(extension) && this.closest('ext-link-text') && this.getValue('link')?.trim() !== '') { const e = (0, lint_1.generateForSelf)(this, rect, rule, 'link-in-extlink', s); if (computeEditInfo || fix) { const link = this.getArg('link'); if (link) { const from = start + link.getRelativeIndex(); e.fix = { desc: index_1.default.msg('delink'), range: [from, from + link.toString().length], text: 'link=', }; } else { e.fix = (0, lint_1.fixByInsert)(e.endIndex - 2, 'delink', '|link='); } } errors.push(e); } rule = 'invalid-gallery'; s = lintConfig.getSeverity(rule, 'extension'); if (s && ns === 6 && !extension && !firstChild.querySelector('arg,magic-word,template') && !interwiki) { errors.push((0, lint_1.generateForSelf)(this, rect, rule, 'missing-extension', s)); } s = lintConfig.getSeverity(rule, 'parameter'); if (s && unscaled) { for (const arg of args.filter(({ name }) => name === 'width')) { const e = (0, lint_1.generateForChild)(arg, rect, rule, 'invalid-image-parameter', s); if (computeEditInfo || fix) { e.fix = (0, lint_1.fixByRemove)(e, -1); } errors.push(e); } } if (args.length === keys.length && frameKeys.length < 2 && horizAlignKeys.length < 2 && vertAlignKeys.length < 2) { return errors; } rule = 'no-duplicate'; const severities = ['unknownImageParameter', 'imageParameter'].map(k => lintConfig.getSeverity(rule, k)); /** * 图片参数到语法错误的映射 * @param tokens 图片参数节点 * @param msg 消息键 * @param p1 替换$1 * @param severity 错误等级 */ const generate = (tokens, msg, p1, severity = true) => tokens.map(arg => { s = severities[Number(typeof severity === 'function' ? severity(arg) : severity)]; if (!s) { return false; } /** `conflicting-image-parameter`或`duplicate-image-parameter` */ const e = (0, lint_1.generateForChild)(arg, rect, rule, index_1.default.msg(`${msg}-image-parameter`, p1), s); if (computeEditInfo) { e.suggestions = [(0, lint_1.fixByRemove)(e, -1)]; } return e; }).filter((e) => e !== false); for (const key of keys) { if (key === 'invalid' || key === 'width' && unscaled) { continue; } const isCaption = key === 'caption'; let relevantArgs = args.filter(({ name }) => name === key); if (isCaption) { relevantArgs = [ ...relevantArgs.slice(0, -1).filter(arg => arg.text()), ...relevantArgs.slice(-1), ]; } if (relevantArgs.length > 1) { let severity = !isCaption || !extension || extensions.has(extension); if (isCaption && severity) { const plainArgs = filterArgs(relevantArgs, transclusion); severity = plainArgs.length > 1 && ((arg) => plainArgs.includes(arg)); } errors.push(...generate(relevantArgs, 'duplicate', key, severity)); } } if (frameKeys.length > 1) { errors.push(...generate(args.filter(({ name }) => frame.has(name)), 'conflicting', 'frame')); } if (horizAlignKeys.length > 1) { errors.push(...generate(args.filter(({ name }) => horizAlign.has(name)), 'conflicting', 'horizontal-alignment')); } if (vertAlignKeys.length > 1) { errors.push(...generate(args.filter(({ name }) => vertAlign.has(name)), 'conflicting', 'vertical-alignment')); } return errors; } } /** * Get all image parameter tokens * * 获取所有图片参数节点 */ getAllArgs() { return this.childNodes.slice(1); } /** * Get image parameters with the specified name * * 获取指定图片参数 * @param key parameter name / 参数名 */ getArgs(key) { return this.getAllArgs().filter(({ name }) => key === name); } /** * Get the effective image parameter with the specified name * * 获取生效的指定图片参数 * @param key parameter name / 参数名 */ getArg(key) { const args = this.getArgs(key); return args[key === 'manualthumb' ? 0 : args.length - 1]; } /** * Get the effective image parameter value * * 获取生效的指定图片参数值 * @param key parameter name / 参数名 */ getValue(key) { return this.getArg(key)?.getValue(); } /** @private */ json(_, start = this.getAbsoluteIndex()) { const json = super.json(undefined, start); LSP: { // eslint-disable-line no-unused-labels const { extension } = this; if (extension) { json['extension'] = extension; } return json; } } /* NOT FOR BROWSER */ /** * 获取特定类型的图片属性参数节点 * @param keys 接受的参数名 * @param type 类型名 */ #getTypedArgs(keys, type) { const args = this.getAllArgs().filter(({ name }) => keys.has(name)); if (args.length > 1) { index_1.default.warn(`The image ${this.name} has ${args.length} ${type} parameters. Only the last ${args[0].name} will take effect!`); } return args; } /** * Get image frame parameter tokens * * 获取图片框架属性参数节点 */ getFrameArgs() { return this.#getTypedArgs(frame, 'frame'); } /** * Get image horizontal alignment parameter tokens * * 获取图片水平对齐参数节点 */ getHorizAlignArgs() { return this.#getTypedArgs(horizAlign, 'horizontal-align'); } /** * Get image vertical alignment parameter tokens * * 获取图片垂直对齐参数节点 */ getVertAlignArgs() { return this.#getTypedArgs(vertAlign, 'vertical-align'); } /** * Get the effective image frame paremter value * * 获取生效的图片框架属性参数 * @since v1.11.0 */ getFrame() { const [arg] = this.getFrameArgs(), val = arg?.name; return val === 'manualthumb' ? this.normalizeTitle(arg.getValue(), 6) : val; } /** * Get the effective image horizontal alignment parameter value * * 获取生效的图片水平对齐参数 * @since v1.11.0 */ getHorizAlign() { return this.getHorizAlignArgs()[0]?.name; } /** * Get the effective image vertical alignment parameter value * * 获取生效的图片垂直对齐参数 * @since v1.11.0 */ getVertAlign() { return this.getVertAlignArgs()[0]?.name; } /** * Check if the image contains the specified parameter * * 是否具有指定图片参数 * @param key parameter name / 参数名 */ hasArg(key) { return this.getArgs(key).length > 0; } /** * Remove the specified image parameter * * 移除指定图片参数 * @param key parameter name / 参数名 */ removeArg(key) { for (const token of this.getArgs(key)) { this.removeChild(token); } } /** * Get all image parameter names * * 获取图片参数名 */ getKeys() { return new Set(this.getAllArgs().map(({ name }) => name)); } /** * Get the image parameter values with the specified name * * 获取指定的图片参数值 * @param key parameter name / 参数名 */ getValues(key) { return this.getArgs(key).map(token => token.getValue()); } /** * Set the image parameter * * 设置图片参数 * @param key parameter name / 参数名 * @param value parameter value / 参数值 * @throws `RangeError` 未定义的图片参数 */ setValue(key, value = false) { if (value === false) { this.removeArg(key); return; } const token = this.getArg(key); if (token) { token.setValue(value); return; } const config = this.getAttribute('config'), syntax = key === 'caption' ? '$1' : Object.entries(config.img).find(([, name]) => name === key)?.[0]; /* istanbul ignore if */ if (syntax === undefined) { throw new RangeError(`Unknown image parameter: ${key}`); } const free = syntax.includes('$1'); /* istanbul ignore if */ if (value === true && free) { this.typeError('setValue', 'String'); } const parameter = debug_1.Shadow.run(() => // @ts-expect-error abstract class new imageParameter_1.ImageParameterToken(syntax.replace('$1', key === 'width' ? '1' : ''), this.extension, config)); if (free) { const { childNodes } = index_1.default .parse(value, this.getAttribute('include'), undefined, config); parameter.safeReplaceChildren(childNodes); } this.insertAt(parameter); } /* istanbul ignore next */ /** * @override * @throws `Error` 不适用于图片 */ setLinkText() { throw new Error('LinkBaseToken.setLinkText method is not applicable to images!'); } /** @private */ toHtmlInternal(opt) { /** @ignore */ const isInteger = (n) => Boolean(n && !/\D/u.test(n)); const { link, width, height, type } = this, file = this.getAttribute('title'), fr = this.getFrame(), manual = fr instanceof title_1.Title, visibleCaption = manual || fr === 'thumbnail' || fr === 'framed' || type === 'gallery-image', caption = this.getArg('caption')?.toHtmlInternal({ ...opt, nowrap: true, }) ?? '', titleFromCaption = visibleCaption && type !== 'gallery-image' ? '' : (0, string_1.sanitizeAlt)(caption), hasLink = manual || link !== file, title = titleFromCaption || (hasLink && typeof link !== 'string' ? link.getTitleAttr() : ''), titleAttr = title && ` title="${title}"`, alt = (0, string_1.sanitizeAlt)(this.getArg('alt')?.toHtmlInternal({ ...opt, nowrap: true, })) ?? titleFromCaption, horiz = this.getHorizAlign() ?? '', vert = this.getVertAlign() ?? '', className = `${horiz ? `mw-halign-${horiz}` : vert && `mw-valign-${vert}`}${this.getValue('border') ? ' mw-image-border' : ''} ${(0, string_1.sanitizeAlt)(this.getValue('class')) ?? ''}`.trim(), classAttr = className && ` class="${className}"`, img = `<img${alt && ` alt="${alt}"`} src="${(manual ? fr : file).getUrl()}" decoding="async" class="mw-file-element"${isInteger(width) ? ` width="${width}"` : ''}${isInteger(height) ? ` height="${height}"` : ''}>`; let href = ''; if (link) { try { href = typeof link === 'string' ? this.getArg('link').getUrl() : link.getUrl(); if (link === file) { const lang = this.getValue('lang'), page = this.getValue('page'); if (lang) { href += `?lang=${lang}`; } else if (page) { href += `?page=${page}`; } } } catch { } } const a = link ? `<a${href && ` href="${href}"`}${hasLink ? '' : ' class="mw-file-description"'}${titleAttr}${typeof link === 'string' ? ' rel="nofollow"' : ''}>${img}</a>` : `<span${titleAttr}>${img}</span>`; if (type !== 'gallery-image') { return horiz || vert || visibleCaption ? `<figure${classAttr} typeof="mw:File${fr ? `/${manual ? 'Thumb' : frame.get(fr)}` : ''}">${a}<figcaption>${caption}</figcaption></figure>` : `<span${classAttr}>${a}</span>`; } const parent = this.parentNode, mode = parent?.parentNode?.getAttr('mode'), nolines = typeof mode === 'string' && mode.toLowerCase() === 'nolines', padding = nolines ? 0 : 30; return `\t<li class="gallerybox" style="width: ${Number(width) + padding + 5}px">\n\t\t<div class="thumb" style="width: ${Number(width) + padding}px${nolines ? '' : `; height: ${Number(height) + padding}px`}"><span>${a}</span></div>\n\t\t<div class="gallerytext">${parent?.parentNode?.hasAttr('showfilename') ? `<a href="${file.getUrl()}" class="galleryfilename galleryfilename-truncate" title="${file.title}">${file.main}</a>\n` : ''}${caption}</div>\n\t</li>`; } }; })(); exports.FileToken = FileToken; constants_1.classes['FileToken'] = __filename;