Merge remote-tracking branch 'msky/develop' into renovate/major-backend-update-dependencies

This commit is contained in:
kakkokari-gtyih 2025-11-29 22:16:11 +09:00
commit fc43017439
20 changed files with 253 additions and 456 deletions

View File

@ -1,4 +1,4 @@
## Unreleased ## 2025.11.2
### General ### General
- -
@ -7,7 +7,8 @@
- -
### Server ### Server
- - Enhance: メモリ使用量を削減しました
- Enhance: ActivityPubアクティビティを送信する際のパフォーマンス向上
## 2025.11.1 ## 2025.11.1

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.11.1", "version": "2025.11.2-alpha.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -121,16 +121,13 @@
"fluent-ffmpeg": "2.1.3", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.5", "form-data": "4.0.5",
"got": "14.6.4", "got": "14.6.4",
"happy-dom": "20.0.10",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.8.2", "ioredis": "5.8.2",
"ip-cidr": "4.0.2", "ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "6.1.0", "is-svg": "6.1.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"jsdom": "26.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "9.0.0", "jsonld": "9.0.0",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
@ -145,6 +142,7 @@
"nanoid": "5.1.6", "nanoid": "5.1.6",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"node-html-parser": "7.0.1",
"nodemailer": "7.0.10", "nodemailer": "7.0.10",
"nsfwjs": "4.2.0", "nsfwjs": "4.2.0",
"oauth": "0.10.2", "oauth": "0.10.2",
@ -152,7 +150,6 @@
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.4.1", "otpauth": "9.4.1",
"parse5": "7.3.0",
"pg": "8.16.3", "pg": "8.16.3",
"pkce-challenge": "5.0.0", "pkce-challenge": "5.0.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
@ -199,11 +196,9 @@
"@types/color-convert": "2.0.4", "@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.9", "@types/content-disposition": "0.5.9",
"@types/fluent-ffmpeg": "2.1.28", "@types/fluent-ffmpeg": "2.1.28",
"@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.7", "@types/http-link-header": "1.0.7",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.15", "@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.15", "@types/jsrsasign": "10.5.15",
"@types/mime-types": "3.0.1", "@types/mime-types": "3.0.1",

View File

@ -5,9 +5,9 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { JSDOM } from 'jsdom';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import * as htmlParser from 'node-html-parser';
import type { MiInstance } from '@/models/Instance.js'; import type { MiInstance } from '@/models/Instance.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -15,7 +15,6 @@ import { LoggerService } from '@/core/LoggerService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { DOMWindow } from 'jsdom';
type NodeInfo = { type NodeInfo = {
openRegistrations?: unknown; openRegistrations?: unknown;
@ -59,7 +58,7 @@ export class FetchInstanceMetadataService {
return await this.redisClient.set( return await this.redisClient.set(
`fetchInstanceMetadata:mutex:v2:${host}`, '1', `fetchInstanceMetadata:mutex:v2:${host}`, '1',
'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395 'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
'GET' // 古い値を返すなかったらnull 'GET', // 古い値を返すなかったらnull
); );
} }
@ -181,15 +180,14 @@ export class FetchInstanceMetadataService {
} }
@bindThis @bindThis
private async fetchDom(instance: MiInstance): Promise<Document> { private async fetchDom(instance: MiInstance): Promise<htmlParser.HTMLElement> {
this.logger.info(`Fetching HTML of ${instance.host} ...`); this.logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
const html = await this.httpRequestService.getHtml(url); const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html); const doc = htmlParser.parse(html);
const doc = window.document;
return doc; return doc;
} }
@ -206,12 +204,12 @@ export class FetchInstanceMetadataService {
} }
@bindThis @bindThis
private async fetchFaviconUrl(instance: MiInstance, doc: Document | null): Promise<string | null> { private async fetchFaviconUrl(instance: MiInstance, doc: htmlParser.HTMLElement | null): Promise<string | null> {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
if (doc) { if (doc) {
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href; const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.attributes.rel === 'icon')?.attributes.href;
if (href) { if (href) {
return (new URL(href, url)).href; return (new URL(href, url)).href;
@ -232,7 +230,7 @@ export class FetchInstanceMetadataService {
} }
@bindThis @bindThis
private async fetchIconUrl(instance: MiInstance, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> { private async fetchIconUrl(instance: MiInstance, doc: htmlParser.HTMLElement | null, manifest: Record<string, any> | null): Promise<string | null> {
if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
return (new URL(manifest.icons[0].src, url)).href; return (new URL(manifest.icons[0].src, url)).href;
@ -246,9 +244,9 @@ export class FetchInstanceMetadataService {
// https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559
const href = const href =
[ [
links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href, links.find(link => link.attributes.rel?.split(/\s+/).includes('apple-touch-icon-precomposed'))?.attributes.href,
links.find(link => link.relList.contains('apple-touch-icon'))?.href, links.find(link => link.attributes.rel?.split(/\s+/).includes('apple-touch-icon'))?.attributes.href,
links.find(link => link.relList.contains('icon'))?.href, links.find(link => link.attributes.rel?.split(/\s+/).includes('icon'))?.attributes.href,
] ]
.find(href => href); .find(href => href);
@ -261,7 +259,7 @@ export class FetchInstanceMetadataService {
} }
@bindThis @bindThis
private async getThemeColor(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> { private async getThemeColor(info: NodeInfo | null, doc: htmlParser.HTMLElement | null, manifest: Record<string, any> | null): Promise<string | null> {
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color; const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
if (themeColor) { if (themeColor) {
@ -273,7 +271,7 @@ export class FetchInstanceMetadataService {
} }
@bindThis @bindThis
private async getSiteName(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> { private async getSiteName(info: NodeInfo | null, doc: htmlParser.HTMLElement | null, manifest: Record<string, any> | null): Promise<string | null> {
if (info && info.metadata) { if (info && info.metadata) {
if (typeof info.metadata.nodeName === 'string') { if (typeof info.metadata.nodeName === 'string') {
return info.metadata.nodeName; return info.metadata.nodeName;
@ -298,7 +296,7 @@ export class FetchInstanceMetadataService {
} }
@bindThis @bindThis
private async getDescription(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> { private async getDescription(info: NodeInfo | null, doc: htmlParser.HTMLElement | null, manifest: Record<string, any> | null): Promise<string | null> {
if (info && info.metadata) { if (info && info.metadata) {
if (typeof info.metadata.nodeDescription === 'string') { if (typeof info.metadata.nodeDescription === 'string') {
return info.metadata.nodeDescription; return info.metadata.nodeDescription;

View File

@ -5,26 +5,19 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as parse5 from 'parse5'; import * as htmlParser from 'node-html-parser';
import { type Document, type HTMLParagraphElement, Window, XMLSerializer } from 'happy-dom';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { intersperse } from '@/misc/prelude/array.js'; import { intersperse } from '@/misc/prelude/array.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { DefaultTreeAdapterMap } from 'parse5'; import { escapeHtml } from '@/misc/escape-html.js';
import type * as mfm from 'mfm-js'; import type * as mfm from 'mfm-js';
const treeAdapter = parse5.defaultTreeAdapter;
type Node = DefaultTreeAdapterMap['node'];
type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
export type Appender = (document: Document, body: HTMLParagraphElement) => void;
@Injectable() @Injectable()
export class MfmService { export class MfmService {
constructor( constructor(
@ -40,68 +33,68 @@ export class MfmService {
const normalizedHashtagNames = hashtagNames == null ? undefined : new Set<string>(hashtagNames.map(x => normalizeForSearch(x))); const normalizedHashtagNames = hashtagNames == null ? undefined : new Set<string>(hashtagNames.map(x => normalizeForSearch(x)));
const dom = parse5.parseFragment(html); const doc = htmlParser.parse(`<div>${html}</div>`);
let text = ''; let text = '';
for (const n of dom.childNodes) { for (const n of doc.childNodes) {
analyze(n); analyze(n);
} }
return text.trim(); return text.trim();
function getText(node: Node): string { function getText(node: htmlParser.Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (node instanceof htmlParser.TextNode) return node.textContent;
if (!treeAdapter.isElementNode(node)) return ''; if (!(node instanceof htmlParser.HTMLElement)) return '';
if (node.nodeName === 'br') return '\n'; if (node.tagName === 'BR') return '\n';
if (node.childNodes) { if (node.childNodes != null) {
return node.childNodes.map(n => getText(n)).join(''); return node.childNodes.map(n => getText(n)).join('');
} }
return ''; return '';
} }
function appendChildren(childNodes: ChildNode[]): void { function analyzeChildren(childNodes: htmlParser.Node[] | null): void {
if (childNodes) { if (childNodes != null) {
for (const n of childNodes) { for (const n of childNodes) {
analyze(n); analyze(n);
} }
} }
} }
function analyze(node: Node) { function analyze(node: htmlParser.Node) {
if (treeAdapter.isTextNode(node)) { if (node instanceof htmlParser.TextNode) {
text += node.value; text += node.textContent;
return; return;
} }
// Skip comment or document type node // Skip comment or document type node
if (!treeAdapter.isElementNode(node)) { if (!(node instanceof htmlParser.HTMLElement)) {
return; return;
} }
switch (node.nodeName) { switch (node.tagName) {
case 'br': { case 'BR': {
text += '\n'; text += '\n';
break; break;
} }
case 'a': { case 'A': {
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attributes.rel;
const href = node.attrs.find(x => x.name === 'href'); const href = node.attributes.href;
// ハッシュタグ // ハッシュタグ
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) { if (normalizedHashtagNames && href != null && normalizedHashtagNames.has(normalizeForSearch(txt))) {
text += txt; text += txt;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { } else if (txt.startsWith('@') && !(rel != null && rel.startsWith('me '))) {
const part = txt.split('@'); const part = txt.split('@');
if (part.length === 2 && href) { if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する //#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`; const acct = `${txt}@${(new URL(href)).hostname}`;
text += acct; text += acct;
//#endregion //#endregion
} else if (part.length === 3) { } else if (part.length === 3) {
@ -116,17 +109,17 @@ export class MfmService {
if (!href) { if (!href) {
return txt; return txt;
} }
if (!txt || txt === href.value) { // #6383: Missing text node if (!txt || txt === href) { // #6383: Missing text node
if (href.value.match(urlRegexFull)) { if (href.match(urlRegexFull)) {
return href.value; return href;
} else { } else {
return `<${href.value}>`; return `<${href}>`;
} }
} }
if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) { if (href.match(urlRegex) && !href.match(urlRegexFull)) {
return `[${txt}](<${href.value}>)`; // #6846 return `[${txt}](<${href}>)`; // #6846
} else { } else {
return `[${txt}](${href.value})`; return `[${txt}](${href})`;
} }
}; };
@ -135,60 +128,64 @@ export class MfmService {
break; break;
} }
case 'h1': { case 'H1': {
text += '【'; text += '【';
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
text += '】\n'; text += '】\n';
break; break;
} }
case 'b': case 'B':
case 'strong': { case 'STRONG': {
text += '**'; text += '**';
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
text += '**'; text += '**';
break; break;
} }
case 'small': { case 'SMALL': {
text += '<small>'; text += '<small>';
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
text += '</small>'; text += '</small>';
break; break;
} }
case 's': case 'S':
case 'del': { case 'DEL': {
text += '~~'; text += '~~';
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
text += '~~'; text += '~~';
break; break;
} }
case 'i': case 'I':
case 'em': { case 'EM': {
text += '<i>'; text += '<i>';
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
text += '</i>'; text += '</i>';
break; break;
} }
case 'ruby': { case 'RUBY': {
let ruby: [string, string][] = []; let ruby: [string, string][] = [];
for (const child of node.childNodes) { for (const child of node.childNodes) {
if (child.nodeName === 'rp') { if ((child instanceof htmlParser.TextNode) && !/\s|\[|\]/.test(child.textContent)) {
ruby.push([child.textContent, '']);
continue; continue;
} }
if (treeAdapter.isTextNode(child) && !/\s|\[|\]/.test(child.value)) {
ruby.push([child.value, '']); if (!(child instanceof htmlParser.HTMLElement)) continue;
if (child.tagName === 'RP') {
continue; continue;
} }
if (child.nodeName === 'rt' && ruby.length > 0) {
if (child.tagName === 'RT' && ruby.length > 0) {
const rt = getText(child); const rt = getText(child);
if (/\s|\[|\]/.test(rt)) { if (/\s|\[|\]/.test(rt)) {
// If any space is included in rt, it is treated as a normal text // If any space is included in rt, it is treated as a normal text
ruby = []; ruby = [];
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
break; break;
} else { } else {
ruby.at(-1)![1] = rt; ruby.at(-1)![1] = rt;
@ -197,7 +194,7 @@ export class MfmService {
} }
// If any other element is included in ruby, it is treated as a normal text // If any other element is included in ruby, it is treated as a normal text
ruby = []; ruby = [];
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
break; break;
} }
for (const [base, rt] of ruby) { for (const [base, rt] of ruby) {
@ -207,26 +204,30 @@ export class MfmService {
} }
// block code (<pre><code>) // block code (<pre><code>)
case 'pre': { case 'PRE': {
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') { if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.HTMLElement) && node.childNodes[0].tagName === 'CODE') {
text += '\n```\n'; text += '\n```\n';
text += getText(node.childNodes[0]); text += getText(node.childNodes[0]);
text += '\n```\n'; text += '\n```\n';
} else if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.TextNode) && node.childNodes[0].textContent.startsWith('<code>') && node.childNodes[0].textContent.endsWith('</code>')) {
text += '\n```\n';
text += node.childNodes[0].textContent.slice(6, -7);
text += '\n```\n';
} else { } else {
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
} }
break; break;
} }
// inline code (<code>) // inline code (<code>)
case 'code': { case 'CODE': {
text += '`'; text += '`';
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
text += '`'; text += '`';
break; break;
} }
case 'blockquote': { case 'BLOCKQUOTE': {
const t = getText(node); const t = getText(node);
if (t) { if (t) {
text += '\n> '; text += '\n> ';
@ -235,33 +236,33 @@ export class MfmService {
break; break;
} }
case 'p': case 'P':
case 'h2': case 'H2':
case 'h3': case 'H3':
case 'h4': case 'H4':
case 'h5': case 'H5':
case 'h6': { case 'H6': {
text += '\n\n'; text += '\n\n';
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
break; break;
} }
// other block elements // other block elements
case 'div': case 'DIV':
case 'header': case 'HEADER':
case 'footer': case 'FOOTER':
case 'article': case 'ARTICLE':
case 'li': case 'LI':
case 'dt': case 'DT':
case 'dd': { case 'DD': {
text += '\n'; text += '\n';
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
break; break;
} }
default: // includes inline elements default: // includes inline elements
{ {
appendChildren(node.childNodes); analyzeChildren(node.childNodes);
break; break;
} }
} }
@ -269,52 +270,35 @@ export class MfmService {
} }
@bindThis @bindThis
public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], additionalAppenders: Appender[] = []) { public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], extraHtml: string | null = null) {
if (nodes == null) { if (nodes == null) {
return null; return null;
} }
const { happyDOM, window } = new Window(); function toHtml(children?: mfm.MfmNode[]): string {
if (children == null) return '';
const doc = window.document; return children.map(x => handlers[x.type](x)).join('');
const body = doc.createElement('p');
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) {
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
}
} }
function fnDefault(node: mfm.MfmFn) { function fnDefault(node: mfm.MfmFn) {
const el = doc.createElement('i'); return `<i>${toHtml(node.children)}</i>`;
appendChildren(node.children, el);
return el;
} }
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = { const handlers = {
bold: (node) => { bold: (node) => {
const el = doc.createElement('b'); return `<b>${toHtml(node.children)}</b>`;
appendChildren(node.children, el);
return el;
}, },
small: (node) => { small: (node) => {
const el = doc.createElement('small'); return `<small>${toHtml(node.children)}</small>`;
appendChildren(node.children, el);
return el;
}, },
strike: (node) => { strike: (node) => {
const el = doc.createElement('del'); return `<del>${toHtml(node.children)}</del>`;
appendChildren(node.children, el);
return el;
}, },
italic: (node) => { italic: (node) => {
const el = doc.createElement('i'); return `<i>${toHtml(node.children)}</i>`;
appendChildren(node.children, el);
return el;
}, },
fn: (node) => { fn: (node) => {
@ -323,10 +307,7 @@ export class MfmService {
const text = node.children[0].type === 'text' ? node.children[0].props.text : ''; const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
try { try {
const date = new Date(parseInt(text, 10) * 1000); const date = new Date(parseInt(text, 10) * 1000);
const el = doc.createElement('time'); return `<time datetime="${escapeHtml(date.toISOString())}">${escapeHtml(date.toISOString())}</time>`;
el.setAttribute('datetime', date.toISOString());
el.textContent = date.toISOString();
return el;
} catch (err) { } catch (err) {
return fnDefault(node); return fnDefault(node);
} }
@ -336,21 +317,9 @@ export class MfmService {
if (node.children.length === 1) { if (node.children.length === 1) {
const child = node.children[0]; const child = node.children[0];
const text = child.type === 'text' ? child.props.text : ''; const text = child.type === 'text' ? child.props.text : '';
const rubyEl = doc.createElement('ruby');
const rtEl = doc.createElement('rt');
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備りゅうび」となるようにする // ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキストルビテキスト」にフォールバックするようにする
const rpStartEl = doc.createElement('rp'); return `<ruby>${escapeHtml(text.split(' ')[0])}<rp>(</rp><rt>${escapeHtml(text.split(' ')[1])}</rt><rp>)</rp></ruby>`;
rpStartEl.appendChild(doc.createTextNode('('));
const rpEndEl = doc.createElement('rp');
rpEndEl.appendChild(doc.createTextNode(')'));
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
rubyEl.appendChild(rpStartEl);
rubyEl.appendChild(rtEl);
rubyEl.appendChild(rpEndEl);
return rubyEl;
} else { } else {
const rt = node.children.at(-1); const rt = node.children.at(-1);
@ -359,21 +328,9 @@ export class MfmService {
} }
const text = rt.type === 'text' ? rt.props.text : ''; const text = rt.type === 'text' ? rt.props.text : '';
const rubyEl = doc.createElement('ruby');
const rtEl = doc.createElement('rt');
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備りゅうび」となるようにする // ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキストルビテキスト」にフォールバックするようにする
const rpStartEl = doc.createElement('rp'); return `<ruby>${toHtml(node.children.slice(0, node.children.length - 1))}<rp>(</rp><rt>${escapeHtml(text.trim())}</rt><rp>)</rp></ruby>`;
rpStartEl.appendChild(doc.createTextNode('('));
const rpEndEl = doc.createElement('rp');
rpEndEl.appendChild(doc.createTextNode(')'));
appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
rtEl.appendChild(doc.createTextNode(text.trim()));
rubyEl.appendChild(rpStartEl);
rubyEl.appendChild(rtEl);
rubyEl.appendChild(rpEndEl);
return rubyEl;
} }
} }
@ -384,125 +341,98 @@ export class MfmService {
}, },
blockCode: (node) => { blockCode: (node) => {
const pre = doc.createElement('pre'); return `<pre><code>${escapeHtml(node.props.code)}</code></pre>`;
const inner = doc.createElement('code');
inner.textContent = node.props.code;
pre.appendChild(inner);
return pre;
}, },
center: (node) => { center: (node) => {
const el = doc.createElement('div'); return `<div style="text-align: center;">${toHtml(node.children)}</div>`;
appendChildren(node.children, el);
return el;
}, },
emojiCode: (node) => { emojiCode: (node) => {
return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); return `\u200B:${escapeHtml(node.props.name)}:\u200B`;
}, },
unicodeEmoji: (node) => { unicodeEmoji: (node) => {
return doc.createTextNode(node.props.emoji); return node.props.emoji;
}, },
hashtag: (node) => { hashtag: (node) => {
const a = doc.createElement('a'); return `<a href="${escapeHtml(`${this.config.url}/tags/${encodeURIComponent(node.props.hashtag)}`)}" rel="tag">#${escapeHtml(node.props.hashtag)}</a>`;
a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
a.textContent = `#${node.props.hashtag}`;
a.setAttribute('rel', 'tag');
return a;
}, },
inlineCode: (node) => { inlineCode: (node) => {
const el = doc.createElement('code'); return `<code>${escapeHtml(node.props.code)}</code>`;
el.textContent = node.props.code;
return el;
}, },
mathInline: (node) => { mathInline: (node) => {
const el = doc.createElement('code'); return `<code>${escapeHtml(node.props.formula)}</code>`;
el.textContent = node.props.formula;
return el;
}, },
mathBlock: (node) => { mathBlock: (node) => {
const el = doc.createElement('code'); return `<pre><code>${escapeHtml(node.props.formula)}</code></pre>`;
el.textContent = node.props.formula;
return el;
}, },
link: (node) => { link: (node) => {
const a = doc.createElement('a'); try {
a.setAttribute('href', node.props.url); const url = new URL(node.props.url);
appendChildren(node.children, a); return `<a href="${escapeHtml(url.href)}">${toHtml(node.children)}</a>`;
return a; } catch (err) {
return `[${toHtml(node.children)}](${escapeHtml(node.props.url)})`;
}
}, },
mention: (node) => { mention: (node) => {
const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase()); const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
a.setAttribute('href', remoteUserInfo const href = remoteUserInfo
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`); : `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`;
a.className = 'u-url mention'; try {
a.textContent = acct; const url = new URL(href);
return a; return `<a href="${escapeHtml(url.href)}" class="u-url mention">${escapeHtml(acct)}</a>`;
} catch (err) {
return escapeHtml(acct);
}
}, },
quote: (node) => { quote: (node) => {
const el = doc.createElement('blockquote'); return `<blockquote>${toHtml(node.children)}</blockquote>`;
appendChildren(node.children, el);
return el;
}, },
text: (node) => { text: (node) => {
if (!node.props.text.match(/[\r\n]/)) { if (!node.props.text.match(/[\r\n]/)) {
return doc.createTextNode(node.props.text); return escapeHtml(node.props.text);
} }
const el = doc.createElement('span'); let html = '';
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
for (const x of intersperse<FIXME | 'br'>('br', nodes)) { const lines = node.props.text.split(/\r\n|\r|\n/).map(x => escapeHtml(x));
el.appendChild(x === 'br' ? doc.createElement('br') : x);
for (const x of intersperse<FIXME | 'br'>('br', lines)) {
html += x === 'br' ? '<br />' : x;
} }
return el; return html;
}, },
url: (node) => { url: (node) => {
const a = doc.createElement('a'); try {
a.setAttribute('href', node.props.url); const url = new URL(node.props.url);
a.textContent = node.props.url; return `<a href="${escapeHtml(url.href)}">${escapeHtml(node.props.url)}</a>`;
return a; } catch (err) {
return escapeHtml(node.props.url);
}
}, },
search: (node) => { search: (node) => {
const a = doc.createElement('a'); return `<a href="${escapeHtml(`https://www.google.com/search?q=${encodeURIComponent(node.props.query)}`)}">${escapeHtml(node.props.content)}</a>`;
a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
a.textContent = node.props.content;
return a;
}, },
plain: (node) => { plain: (node) => {
const el = doc.createElement('span'); return `<span>${toHtml(node.children)}</span>`;
appendChildren(node.children, el);
return el;
}, },
}; } satisfies { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => string } as { [K in mfm.MfmNode['type']]: (node: mfm.MfmNode) => string };
appendChildren(nodes, body); return `${toHtml(nodes)}${extraHtml ?? ''}`;
for (const additionalAppender of additionalAppenders) {
additionalAppender(doc, body);
}
// Remove the unnecessary namespace
const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*<p xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">/, '<p>');
happyDOM.close().catch(err => {});
return serialized;
} }
} }

View File

@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { MfmService, Appender } from '@/core/MfmService.js'; import { MfmService } from '@/core/MfmService.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { extractApHashtagObjects } from './models/tag.js'; import { extractApHashtagObjects } from './models/tag.js';
@ -25,17 +25,17 @@ export class ApMfmService {
} }
@bindThis @bindThis
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, additionalAppender: Appender[] = []) { public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, extraHtml: string | null = null) {
let noMisskeyContent = false; let noMisskeyContent = false;
const srcMfm = (note.text ?? ''); const srcMfm = (note.text ?? '');
const parsed = mfm.parse(srcMfm); const parsed = mfm.parse(srcMfm);
if (!additionalAppender.length && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) { if (extraHtml == null && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
noMisskeyContent = true; noMisskeyContent = true;
} }
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), additionalAppender); const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), extraHtml);
return { return {
content, content,

View File

@ -19,7 +19,7 @@ import type { MiEmoji } from '@/models/Emoji.js';
import type { MiPoll } from '@/models/Poll.js'; import type { MiPoll } from '@/models/Poll.js';
import type { MiPollVote } from '@/models/PollVote.js'; import type { MiPollVote } from '@/models/PollVote.js';
import { UserKeypairService } from '@/core/UserKeypairService.js'; import { UserKeypairService } from '@/core/UserKeypairService.js';
import { MfmService, type Appender } from '@/core/MfmService.js'; import { MfmService } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { MiUserKeypair } from '@/models/UserKeypair.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js';
@ -28,6 +28,7 @@ import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { escapeHtml } from '@/misc/escape-html.js';
import { JsonLdService } from './JsonLdService.js'; import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js'; import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js'; import { CONTEXT } from './misc/contexts.js';
@ -384,7 +385,7 @@ export class ApRendererService {
inReplyTo = null; inReplyTo = null;
} }
let quote; let quote: string | undefined;
if (note.renoteId) { if (note.renoteId) {
const renote = await this.notesRepository.findOneBy({ id: note.renoteId }); const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
@ -430,29 +431,18 @@ export class ApRendererService {
poll = await this.pollsRepository.findOneBy({ noteId: note.id }); poll = await this.pollsRepository.findOneBy({ noteId: note.id });
} }
const apAppend: Appender[] = []; let extraHtml: string | null = null;
if (quote) { if (quote != null) {
// Append quote link as `<br><br><span class="quote-inline">RE: <a href="...">...</a></span>` // Append quote link as `<br><br><span class="quote-inline">RE: <a href="...">...</a></span>`
// the claas name `quote-inline` is used in non-misskey clients for styling quote notes. // the class name `quote-inline` is used in non-misskey clients for styling quote notes.
// For compatibility, the span part should be kept as possible. // For compatibility, the span part should be kept as possible.
apAppend.push((doc, body) => { extraHtml = `<br><br><span class="quote-inline">RE: <a href="${escapeHtml(quote)}">${escapeHtml(quote)}</a></span>`;
body.appendChild(doc.createElement('br'));
body.appendChild(doc.createElement('br'));
const span = doc.createElement('span');
span.className = 'quote-inline';
span.appendChild(doc.createTextNode('RE: '));
const link = doc.createElement('a');
link.setAttribute('href', quote);
link.textContent = quote;
span.appendChild(link);
body.appendChild(span);
});
} }
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend); const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, extraHtml);
const emojis = await this.getEmojis(note.emojis); const emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));

View File

@ -6,7 +6,7 @@
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import { URL } from 'node:url'; import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Window } from 'happy-dom'; import * as htmlParser from 'node-html-parser';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
@ -215,29 +215,9 @@ export class ApRequestService {
_followAlternate === true _followAlternate === true
) { ) {
const html = await res.text(); const html = await res.text();
const { window, happyDOM } = new Window({
settings: {
disableJavaScriptEvaluation: true,
disableJavaScriptFileLoading: true,
disableCSSFileLoading: true,
disableComputedStyleRendering: true,
handleDisabledFileLoadingAsSuccess: true,
navigation: {
disableMainFrameNavigation: true,
disableChildFrameNavigation: true,
disableChildPageNavigation: true,
disableFallbackToSetURL: true,
},
timer: {
maxTimeout: 0,
maxIntervalTime: 0,
maxIntervalIterations: 0,
},
},
});
const document = window.document;
try { try {
document.documentElement.innerHTML = html; const document = htmlParser.parse(html);
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]'); const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
if (alternate) { if (alternate) {
@ -248,8 +228,6 @@ export class ApRequestService {
} }
} catch (e) { } catch (e) {
// something went wrong parsing the HTML, ignore the whole thing // something went wrong parsing the HTML, ignore the whole thing
} finally {
happyDOM.close().catch(err => {});
} }
} }
//#endregion //#endregion

View File

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

View File

@ -7,7 +7,7 @@ import RE2 from 're2';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms'; import ms from 'ms';
import { JSDOM } from 'jsdom'; import * as htmlParser from 'node-html-parser';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
@ -569,16 +569,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try { try {
const html = await this.httpRequestService.getHtml(url); const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html); const doc = htmlParser.parse(html);
const doc: Document = window.document;
const myLink = `${this.config.url}/@${user.username}`; const myLink = `${this.config.url}/@${user.username}`;
const aEls = Array.from(doc.getElementsByTagName('a')); const aEls = Array.from(doc.getElementsByTagName('a'));
const linkEls = Array.from(doc.getElementsByTagName('link')); const linkEls = Array.from(doc.getElementsByTagName('link'));
const includesMyLink = aEls.some(a => a.href === myLink); const includesMyLink = aEls.some(a => a.attributes.href === myLink);
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink); const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.attributes.rel?.split(/\s+/).includes('me') && link.attributes.href === myLink);
if (includesMyLink || includesRelMeLinks) { if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update() await this.userProfilesRepository.createQueryBuilder('profile').update()
@ -588,8 +587,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}) })
.execute(); .execute();
} }
window.close();
} catch (err) { } catch (err) {
// なにもしない // なにもしない
} }

View File

@ -6,7 +6,7 @@
import dns from 'node:dns/promises'; import dns from 'node:dns/promises';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { JSDOM } from 'jsdom'; import * as htmlParser from 'node-html-parser';
import httpLinkHeader from 'http-link-header'; import httpLinkHeader from 'http-link-header';
import ipaddr from 'ipaddr.js'; import ipaddr from 'ipaddr.js';
import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize'; import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize';
@ -120,9 +120,9 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
} }
const text = await res.text(); const text = await res.text();
const fragment = JSDOM.fragment(text); const fragment = htmlParser.parse(`<div>${text}</div>`);
redirectUris.push(...[...fragment.querySelectorAll<HTMLLinkElement>('link[rel=redirect_uri][href]')].map(el => el.href)); redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
let name = id; let name = id;
let logo: string | null = null; let logo: string | null = null;

View File

@ -15,7 +15,6 @@ import fastifyStatic from '@fastify/static';
import fastifyView from '@fastify/view'; import fastifyView from '@fastify/view';
import fastifyProxy from '@fastify/http-proxy'; import fastifyProxy from '@fastify/http-proxy';
import vary from 'vary'; import vary from 'vary';
import htmlSafeJsonStringify from 'htmlescape';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { getNoteSummary } from '@/misc/get-note-summary.js'; import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -63,6 +62,20 @@ const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`; const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
const tarball = `${_dirname}/../../../../../built/tarball/`; const tarball = `${_dirname}/../../../../../built/tarball/`;
const ESCAPE_LOOKUP = {
'&': '\\u0026',
'>': '\\u003e',
'<': '\\u003c',
'\u2028': '\\u2028',
'\u2029': '\\u2029',
} as Record<string, string>;
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
function htmlSafeJsonStringify(obj: any): string {
return JSON.stringify(obj).replace(ESCAPE_REGEX, x => ESCAPE_LOOKUP[x]);
}
@Injectable() @Injectable()
export class ClientServerService { export class ClientServerService {
private logger: Logger; private logger: Logger;

View File

@ -16,7 +16,7 @@ describe('export-clips', () => {
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
// XXX: Any better way to get the result? // XXX: Any better way to get the result?
async function pollFirstDriveFile() { async function pollFirstDriveFile(): Promise<any> {
while (true) { while (true) {
const files = (await api('drive/files', {}, alice)).body; const files = (await api('drive/files', {}, alice)).body;
if (!files.length) { if (!files.length) {

View File

@ -73,7 +73,7 @@ describe('Webリソース', () => {
}; };
const metaTag = (res: SimpleGetResponse, key: string, superkey = 'name'): string => { const metaTag = (res: SimpleGetResponse, key: string, superkey = 'name'): string => {
return res.body.window.document.querySelector('meta[' + superkey + '="' + key + '"]')?.content; return res.body.querySelector('meta[' + superkey + '="' + key + '"]')?.attributes.content;
}; };
beforeAll(async () => { beforeAll(async () => {

View File

@ -19,7 +19,7 @@ import {
ResourceOwnerPassword, ResourceOwnerPassword,
} from 'simple-oauth2'; } from 'simple-oauth2';
import pkceChallenge from 'pkce-challenge'; import pkceChallenge from 'pkce-challenge';
import { JSDOM } from 'jsdom'; import * as htmlParser from 'node-html-parser';
import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify'; import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify';
import { api, port, sendEnvUpdateRequest, signup } from '../utils.js'; import { api, port, sendEnvUpdateRequest, signup } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
@ -73,11 +73,11 @@ const clientConfig: ModuleOptions<'client_id'> = {
}; };
function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: string | undefined } { function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: string | undefined } {
const fragment = JSDOM.fragment(html); const doc = htmlParser.parse(`<div>${html}</div>`);
return { return {
transactionId: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]')?.content, transactionId: doc.querySelector('meta[name="misskey:oauth:transaction-id"]')?.attributes.content,
clientName: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content, clientName: doc.querySelector('meta[name="misskey:oauth:client-name"]')?.attributes.content,
clientLogo: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-logo"]')?.content, clientLogo: doc.querySelector('meta[name="misskey:oauth:client-logo"]')?.attributes.content,
}; };
} }
@ -148,7 +148,7 @@ function assertIndirectError(response: Response, error: string): void {
async function assertDirectError(response: Response, status: number, error: string): Promise<void> { async function assertDirectError(response: Response, status: number, error: string): Promise<void> {
assert.strictEqual(response.status, status); assert.strictEqual(response.status, status);
const data = await response.json(); const data = await response.json() as any;
assert.strictEqual(data.error, error); assert.strictEqual(data.error, error);
} }
@ -704,7 +704,7 @@ describe('OAuth', () => {
const response = await fetch(new URL('.well-known/oauth-authorization-server', host)); const response = await fetch(new URL('.well-known/oauth-authorization-server', host));
assert.strictEqual(response.status, 200); assert.strictEqual(response.status, 200);
const body = await response.json(); const body = await response.json() as any;
assert.strictEqual(body.issuer, 'http://misskey.local'); assert.strictEqual(body.issuer, 'http://misskey.local');
assert.ok(body.scopes_supported.includes('write:notes')); assert.ok(body.scopes_supported.includes('write:notes'));
}); });

View File

@ -9,7 +9,6 @@ import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { MiNote } from '@/models/Note.js';
describe('ApMfmService', () => { describe('ApMfmService', () => {
let apMfmService: ApMfmService; let apMfmService: ApMfmService;
@ -31,7 +30,7 @@ describe('ApMfmService', () => {
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, true, 'noMisskeyContent'); assert.equal(noMisskeyContent, true, 'noMisskeyContent');
assert.equal(content, '<p>テキスト <a href="http://misskey.local/tags/タグ" rel="tag">#タグ</a> <a href="http://misskey.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com">https://example.com</a></p>', 'content'); assert.equal(content, 'テキスト <a href="http://misskey.local/tags/%E3%82%BF%E3%82%B0" rel="tag">#タグ</a> <a href="http://misskey.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com/">https://example.com</a>', 'content');
}); });
test('Provide _misskey_content for MFM', () => { test('Provide _misskey_content for MFM', () => {
@ -43,7 +42,7 @@ describe('ApMfmService', () => {
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, false, 'noMisskeyContent'); assert.equal(noMisskeyContent, false, 'noMisskeyContent');
assert.equal(content, '<p><i>foo</i></p>', 'content'); assert.equal(content, '<i>foo</i>', 'content');
}); });
}); });
}); });

View File

@ -24,25 +24,25 @@ describe('MfmService', () => {
describe('toHtml', () => { describe('toHtml', () => {
test('br', () => { test('br', () => {
const input = 'foo\nbar\nbaz'; const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br />bar<br />baz</span></p>'; const output = 'foo<br />bar<br />baz';
assert.equal(mfmService.toHtml(mfm.parse(input)), output); assert.equal(mfmService.toHtml(mfm.parse(input)), output);
}); });
test('br alt', () => { test('br alt', () => {
const input = 'foo\r\nbar\rbaz'; const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br />bar<br />baz</span></p>'; const output = 'foo<br />bar<br />baz';
assert.equal(mfmService.toHtml(mfm.parse(input)), output); assert.equal(mfmService.toHtml(mfm.parse(input)), output);
}); });
test('Do not generate unnecessary span', () => { test('Do not generate unnecessary span', () => {
const input = 'foo $[tada bar]'; const input = 'foo $[tada bar]';
const output = '<p>foo <i>bar</i></p>'; const output = 'foo <i>bar</i>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output); assert.equal(mfmService.toHtml(mfm.parse(input)), output);
}); });
test('escape', () => { test('escape', () => {
const input = '```\n<p>Hello, world!</p>\n```'; const input = '```\n<p>Hello, world!</p>\n```';
const output = '<p><pre><code>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre></p>'; const output = '<pre><code>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output); assert.equal(mfmService.toHtml(mfm.parse(input)), output);
}); });
}); });
@ -118,7 +118,7 @@ describe('MfmService', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp> b</ruby> c</p>'), 'a Misskey(ミス キー) b c'); assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp> b</ruby> c</p>'), 'a Misskey(ミス キー) b c');
assert.deepStrictEqual( assert.deepStrictEqual(
mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'), mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'),
'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b' 'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b',
); );
}); });

View File

@ -10,8 +10,8 @@ import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util'; import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws'; import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit, type Headers } from 'node-fetch'; import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import * as htmlParser from 'node-html-parser';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom';
import { type Response } from 'node-fetch'; import { type Response } from 'node-fetch';
import Fastify from 'fastify'; import Fastify from 'fastify';
import { entities } from '../src/postgres.js'; import { entities } from '../src/postgres.js';
@ -468,7 +468,7 @@ export function makeStreamCatcher<T>(
export type SimpleGetResponse = { export type SimpleGetResponse = {
status: number, status: number,
body: any | JSDOM | null, body: any | null,
type: string | null, type: string | null,
location: string | null location: string | null
}; };
@ -499,7 +499,7 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
const body = const body =
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : htmlTypes.includes(res.headers.get('content-type') ?? '') ? htmlParser.parse(await res.text()) :
await bodyExtractor(res); await bodyExtractor(res);
return { return {

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2025.11.1", "version": "2025.11.2-alpha.0",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",

View File

@ -246,15 +246,9 @@ importers:
got: got:
specifier: 14.6.4 specifier: 14.6.4
version: 14.6.4 version: 14.6.4
happy-dom:
specifier: 20.0.10
version: 20.0.10
hpagent: hpagent:
specifier: 1.2.0 specifier: 1.2.0
version: 1.2.0 version: 1.2.0
htmlescape:
specifier: 1.1.1
version: 1.1.1
http-link-header: http-link-header:
specifier: 1.1.3 specifier: 1.1.3
version: 1.1.3 version: 1.1.3
@ -273,9 +267,6 @@ importers:
js-yaml: js-yaml:
specifier: 4.1.1 specifier: 4.1.1
version: 4.1.1 version: 4.1.1
jsdom:
specifier: 26.1.0
version: 26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
json5: json5:
specifier: 2.2.3 specifier: 2.2.3
version: 2.2.3 version: 2.2.3
@ -318,6 +309,9 @@ importers:
node-fetch: node-fetch:
specifier: 3.3.2 specifier: 3.3.2
version: 3.3.2 version: 3.3.2
node-html-parser:
specifier: 7.0.1
version: 7.0.1
nodemailer: nodemailer:
specifier: 7.0.10 specifier: 7.0.10
version: 7.0.10 version: 7.0.10
@ -339,9 +333,6 @@ importers:
otpauth: otpauth:
specifier: 9.4.1 specifier: 9.4.1
version: 9.4.1 version: 9.4.1
parse5:
specifier: 7.3.0
version: 7.3.0
pg: pg:
specifier: 8.16.3 specifier: 8.16.3
version: 8.16.3 version: 8.16.3
@ -475,9 +466,6 @@ importers:
'@types/fluent-ffmpeg': '@types/fluent-ffmpeg':
specifier: 2.1.28 specifier: 2.1.28
version: 2.1.28 version: 2.1.28
'@types/htmlescape':
specifier: 1.1.3
version: 1.1.3
'@types/http-link-header': '@types/http-link-header':
specifier: 1.0.7 specifier: 1.0.7
version: 1.0.7 version: 1.0.7
@ -487,9 +475,6 @@ importers:
'@types/js-yaml': '@types/js-yaml':
specifier: 4.0.9 specifier: 4.0.9
version: 4.0.9 version: 4.0.9
'@types/jsdom':
specifier: 21.1.7
version: 21.1.7
'@types/jsonld': '@types/jsonld':
specifier: 1.5.15 specifier: 1.5.15
version: 1.5.15 version: 1.5.15
@ -1605,9 +1590,6 @@ packages:
'@apm-js-collab/tracing-hooks@0.3.1': '@apm-js-collab/tracing-hooks@0.3.1':
resolution: {integrity: sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==} resolution: {integrity: sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==}
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
'@asamuzakjp/css-color@4.1.0': '@asamuzakjp/css-color@4.1.0':
resolution: {integrity: sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==} resolution: {integrity: sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==}
@ -4662,9 +4644,6 @@ packages:
'@types/hast@3.0.4': '@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/htmlescape@1.1.3':
resolution: {integrity: sha512-tuC81YJXGUe0q8WRtBNW+uyx79rkkzWK651ALIXXYq5/u/IxjX4iHneGF2uUqzsNp+F+9J2mFZOv9jiLTtIq0w==}
'@types/http-cache-semantics@4.0.4': '@types/http-cache-semantics@4.0.4':
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
@ -4689,9 +4668,6 @@ packages:
'@types/js-yaml@4.0.9': '@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
'@types/jsdom@21.1.7':
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -4878,9 +4854,6 @@ packages:
'@types/tmp@0.2.6': '@types/tmp@0.2.6':
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
'@types/unist@3.0.3': '@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@ -6150,10 +6123,6 @@ packages:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
cssstyle@4.6.0:
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
engines: {node: '>=18'}
cssstyle@5.3.3: cssstyle@5.3.3:
resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==} resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
engines: {node: '>=20'} engines: {node: '>=20'}
@ -6174,10 +6143,6 @@ packages:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
data-urls@6.0.0: data-urls@6.0.0:
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
engines: {node: '>=20'} engines: {node: '>=20'}
@ -7277,10 +7242,6 @@ packages:
html-void-elements@3.0.0: html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
htmlescape@1.1.1:
resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==}
engines: {node: '>=0.10'}
htmlparser2@10.0.0: htmlparser2@10.0.0:
resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
@ -7909,15 +7870,6 @@ packages:
resolution: {integrity: sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==} resolution: {integrity: sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==}
engines: {node: '>=0.1.90'} engines: {node: '>=0.1.90'}
jsdom@26.1.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
engines: {node: '>=18'}
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
jsdom@27.2.0: jsdom@27.2.0:
resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==} resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@ -8660,6 +8612,9 @@ packages:
engines: {node: ^18.17.0 || >=20.5.0} engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true hasBin: true
node-html-parser@7.0.1:
resolution: {integrity: sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==}
node-int64@0.4.0: node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
@ -8734,9 +8689,6 @@ packages:
nth-check@2.1.1: nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
nwsapi@2.2.22:
resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==}
oauth2orize-pkce@0.1.2: oauth2orize-pkce@0.1.2:
resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==} resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==}
@ -9734,9 +9686,6 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
rss-parser@3.13.0: rss-parser@3.13.0:
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
@ -10507,10 +10456,6 @@ packages:
tr46@0.0.3: tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
tr46@6.0.0: tr46@6.0.0:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'} engines: {node: '>=20'}
@ -11132,10 +11077,6 @@ packages:
webidl-conversions@3.0.1: webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
webidl-conversions@8.0.0: webidl-conversions@8.0.0:
resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==}
engines: {node: '>=20'} engines: {node: '>=20'}
@ -11155,10 +11096,6 @@ packages:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'} engines: {node: '>=18'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
whatwg-url@15.1.0: whatwg-url@15.1.0:
resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
engines: {node: '>=20'} engines: {node: '>=20'}
@ -11413,14 +11350,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@asamuzakjp/css-color@3.2.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
lru-cache: 10.4.3
'@asamuzakjp/css-color@4.1.0': '@asamuzakjp/css-color@4.1.0':
dependencies: dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
@ -12242,12 +12171,14 @@ snapshots:
'@cropper/utils@2.1.0': {} '@cropper/utils@2.1.0': {}
'@csstools/color-helpers@5.1.0': {} '@csstools/color-helpers@5.1.0':
optional: true
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies: dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4 '@csstools/css-tokenizer': 3.0.4
optional: true
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies: dependencies:
@ -12255,15 +12186,18 @@ snapshots:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4 '@csstools/css-tokenizer': 3.0.4
optional: true
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies: dependencies:
'@csstools/css-tokenizer': 3.0.4 '@csstools/css-tokenizer': 3.0.4
optional: true
'@csstools/css-syntax-patches-for-csstree@1.0.17': '@csstools/css-syntax-patches-for-csstree@1.0.17':
optional: true optional: true
'@csstools/css-tokenizer@3.0.4': {} '@csstools/css-tokenizer@3.0.4':
optional: true
'@cypress/request@3.0.9': '@cypress/request@3.0.9':
dependencies: dependencies:
@ -15180,8 +15114,6 @@ snapshots:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
'@types/htmlescape@1.1.3': {}
'@types/http-cache-semantics@4.0.4': {} '@types/http-cache-semantics@4.0.4': {}
'@types/http-errors@2.0.5': {} '@types/http-errors@2.0.5': {}
@ -15207,12 +15139,6 @@ snapshots:
'@types/js-yaml@4.0.9': {} '@types/js-yaml@4.0.9': {}
'@types/jsdom@21.1.7':
dependencies:
'@types/node': 24.10.1
'@types/tough-cookie': 4.0.5
parse5: 7.3.0
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {} '@types/json5@0.0.29': {}
@ -15401,8 +15327,6 @@ snapshots:
'@types/tmp@0.2.6': {} '@types/tmp@0.2.6': {}
'@types/tough-cookie@4.0.5': {}
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
'@types/uuid@9.0.8': {} '@types/uuid@9.0.8': {}
@ -16987,11 +16911,6 @@ snapshots:
dependencies: dependencies:
css-tree: 2.2.1 css-tree: 2.2.1
cssstyle@4.6.0:
dependencies:
'@asamuzakjp/css-color': 3.2.0
rrweb-cssom: 0.8.0
cssstyle@5.3.3: cssstyle@5.3.3:
dependencies: dependencies:
'@asamuzakjp/css-color': 4.1.0 '@asamuzakjp/css-color': 4.1.0
@ -17053,11 +16972,6 @@ snapshots:
data-uri-to-buffer@4.0.1: {} data-uri-to-buffer@4.0.1: {}
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
data-urls@6.0.0: data-urls@6.0.0:
dependencies: dependencies:
whatwg-mimetype: 4.0.0 whatwg-mimetype: 4.0.0
@ -17123,7 +17037,8 @@ snapshots:
decamelize@1.2.0: {} decamelize@1.2.0: {}
decimal.js@10.6.0: {} decimal.js@10.6.0:
optional: true
decode-bmp@0.2.1: decode-bmp@0.2.1:
dependencies: dependencies:
@ -18468,6 +18383,7 @@ snapshots:
html-encoding-sniffer@4.0.0: html-encoding-sniffer@4.0.0:
dependencies: dependencies:
whatwg-encoding: 3.1.1 whatwg-encoding: 3.1.1
optional: true
html-entities@2.6.0: {} html-entities@2.6.0: {}
@ -18475,8 +18391,6 @@ snapshots:
html-void-elements@3.0.0: {} html-void-elements@3.0.0: {}
htmlescape@1.1.1: {}
htmlparser2@10.0.0: htmlparser2@10.0.0:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
@ -18789,7 +18703,8 @@ snapshots:
is-plain-object@5.0.0: {} is-plain-object@5.0.0: {}
is-potential-custom-element-name@1.0.1: {} is-potential-custom-element-name@1.0.1:
optional: true
is-promise@2.2.2: {} is-promise@2.2.2: {}
@ -19288,33 +19203,6 @@ snapshots:
jschardet@3.1.4: {} jschardet@3.1.4: {}
jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5):
dependencies:
cssstyle: 4.6.0
data-urls: 5.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6(supports-color@10.2.2)
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.22
parse5: 7.3.0
rrweb-cssom: 0.8.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 5.1.2
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
jsdom@27.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5): jsdom@27.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5):
dependencies: dependencies:
'@acemir/cssom': 0.9.23 '@acemir/cssom': 0.9.23
@ -20251,6 +20139,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
node-html-parser@7.0.1:
dependencies:
css-select: 5.2.2
he: 1.2.0
node-int64@0.4.0: {} node-int64@0.4.0: {}
node-releases@2.0.27: {} node-releases@2.0.27: {}
@ -20333,8 +20226,6 @@ snapshots:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
nwsapi@2.2.22: {}
oauth2orize-pkce@0.1.2: {} oauth2orize-pkce@0.1.2: {}
oauth2orize@1.12.0: oauth2orize@1.12.0:
@ -21390,8 +21281,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
rrweb-cssom@0.8.0: {}
rss-parser@3.13.0: rss-parser@3.13.0:
dependencies: dependencies:
entities: 2.2.0 entities: 2.2.0
@ -21458,6 +21347,7 @@ snapshots:
saxes@6.0.0: saxes@6.0.0:
dependencies: dependencies:
xmlchars: 2.2.0 xmlchars: 2.2.0
optional: true
scheduler@0.27.0: {} scheduler@0.27.0: {}
@ -22099,7 +21989,8 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
sax: 1.4.3 sax: 1.4.3
symbol-tree@3.2.4: {} symbol-tree@3.2.4:
optional: true
systeminformation@5.27.11: {} systeminformation@5.27.11: {}
@ -22257,10 +22148,6 @@ snapshots:
tr46@0.0.3: {} tr46@0.0.3: {}
tr46@5.1.1:
dependencies:
punycode: 2.3.1
tr46@6.0.0: tr46@6.0.0:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
@ -22824,6 +22711,7 @@ snapshots:
w3c-xmlserializer@5.0.0: w3c-xmlserializer@5.0.0:
dependencies: dependencies:
xml-name-validator: 5.0.0 xml-name-validator: 5.0.0
optional: true
wait-on@8.0.5(debug@4.4.3): wait-on@8.0.5(debug@4.4.3):
dependencies: dependencies:
@ -22867,8 +22755,6 @@ snapshots:
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}
webidl-conversions@8.0.0: webidl-conversions@8.0.0:
optional: true optional: true
@ -22882,11 +22768,6 @@ snapshots:
whatwg-mimetype@4.0.0: {} whatwg-mimetype@4.0.0: {}
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
whatwg-url@15.1.0: whatwg-url@15.1.0:
dependencies: dependencies:
tr46: 6.0.0 tr46: 6.0.0
@ -23010,7 +22891,8 @@ snapshots:
xml-name-validator@4.0.0: {} xml-name-validator@4.0.0: {}
xml-name-validator@5.0.0: {} xml-name-validator@5.0.0:
optional: true
xml2js@0.5.0: xml2js@0.5.0:
dependencies: dependencies:
@ -23019,7 +22901,8 @@ snapshots:
xmlbuilder@11.0.1: {} xmlbuilder@11.0.1: {}
xmlchars@2.2.0: {} xmlchars@2.2.0:
optional: true
xtend@4.0.2: {} xtend@4.0.2: {}