From e7943f58c56cb92cdc593b0bc1a554f914e82bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:19:08 +0900 Subject: [PATCH 1/3] Merge commit from fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: avoid quadratic-time parsing of unclosed syntax * fix: avoid quadratic-time parsing of nested syntax and long tokens * chore: clean up comments * fix reviews * refactor: ルールパーサーにのみトレースを適用 * perf: 現在の文字から開始し得ない構文をスキップすることで解析を高速化 * perf: メモ化をやめ、入れ子の個数とは別に、入れ子を解釈しようとした回数でも打ち切るように --- src/internal/core/index.ts | 162 ++++++++++++++++++--- src/internal/index.ts | 1 + src/internal/parser.ts | 283 +++++++++++++++++-------------------- test/parser.ts | 122 ++++++++++++++++ 4 files changed, 391 insertions(+), 177 deletions(-) diff --git a/src/internal/core/index.ts b/src/internal/core/index.ts index 6856bbd..f6c1d53 100644 --- a/src/internal/core/index.ts +++ b/src/internal/core/index.ts @@ -12,11 +12,15 @@ export type Failure = { success: false }; export type Result = Success | Failure; +type StateKey = number; + interface State { trace?: boolean, linkLabel?: boolean, - linkLabelFailures?: Map>>, + untilSpans?: Map, Map>, + manyThenFailures?: Map, Map>>>, nestLimit: number, + nestBudget?: number, depth: number, } @@ -37,24 +41,11 @@ export function failure(): Failure { export class Parser { public name?: string; public handler: ParserHandler; + // the characters the parser can start with (see `first`) + public firstChars?: string; constructor(handler: ParserHandler, name?: string) { - this.handler = (input, index, state) : Failure | Success => { - if (state.trace && this.name != null) { - const pos = `${index}`; - console.log(`${pos.padEnd(6, ' ')}enter ${this.name}`); - const result = handler(input, index, state); - if (result.success) { - const pos = `${index}:${result.index}`; - console.log(`${pos.padEnd(6, ' ')}match ${this.name}`); - } else { - const pos = `${index}`; - console.log(`${pos.padEnd(6, ' ')}fail ${this.name}`); - } - return result; - } - return handler(input, index, state); - }; + this.handler = handler; this.name = name; } @@ -176,9 +167,22 @@ export function seq[]>(...parsers: Parsers): Par } export function alt[]>(parsers: Parsers): Parser> { + // parsers made by `first` are tried only at the characters they can start with + const candidatesByChar = parsers.some(parser => parser.firstChars != null) ? new Map() : null; return new Parser>((input, index, state): Result> => { - for (let i = 0; i < parsers.length; i++) { - const parser: Parsers[number] = parsers[i]; + let candidates: Parsers[number][] = parsers; + if (candidatesByChar != null) { + // non-ascii characters share an entry + const code = index < input.length ? Math.min(input.charCodeAt(index), 0x80) : -1; + let cached = candidatesByChar.get(code); + if (cached === undefined) { + cached = parsers.filter(parser => parser.firstChars == null || (code >= 0 && parser.firstChars.includes(String.fromCharCode(code)))); + candidatesByChar.set(code, cached); + } + candidates = cached; + } + for (let i = 0; i < candidates.length; i++) { + const parser: Parsers[number] = candidates[i]; const result = parser.handler(input, index, state); if (result.success) { return result as Result>; @@ -188,6 +192,21 @@ export function alt[]>(parsers: Parsers): Parser }); } +/** + * Declares that `parser` can only match at one of the ascii characters `chars`. + * `alt` uses this to skip the parser at other characters. +*/ +export function first(chars: string, parser: Parser): Parser { + const guarded = new Parser((input, index, state) => { + if (index >= input.length || !chars.includes(input.charAt(index))) { + return failure(); + } + return parser.handler(input, index, state); + }); + guarded.firstChars = chars; + return guarded; +} + function succeeded(value: T): Parser { return new Parser((_input, index, _state) => { return success(index, value); @@ -203,6 +222,88 @@ export function notMatch(parser: Parser): Parser { }); } +function getOrCreate(map: Map, key: K, create: () => V): V { + let value = map.get(key); + if (value === undefined) { + value = create(); + map.set(key, value); + } + return value; +} + +// must cover every part of the state that a parse result depends on +function stateKey(state: State): StateKey { + return state.depth * 2 + (state.linkLabel ? 1 : 0); +} + +/** + * Same as `seq(notMatch(terminator), char).select(1).many(min).text()`, caching the last scanned span per input. + * `terminator` must not depend on the state. +*/ +export function until(terminator: Parser, min: number): Parser { + const parser: Parser = new Parser((input, index, state) => { + state.untilSpans ??= new Map(); + const spans = getOrCreate(state.untilSpans, parser, () => new Map()); + const span = spans.get(input); + let end: number; + if (span != null && span.start <= index && index <= span.end) { + end = span.end; + } else { + end = index; + while (end < input.length && !terminator.handler(input, end, state).success) { + end++; + } + spans.set(input, { start: index, end }); + } + if (end - index < min) { + return failure(); + } + return success(end, input.slice(index, end)); + }); + return parser; +} + +/** + * Same as `seq(seq(notMatch(stop), item).select(1).many(min), after)`, caching the item boundaries that lead to a failure. + * `item`, `stop` and `after` must depend only on the input, the position and `stateKey`. +*/ +export function manyThen(item: Parser, stop: Parser, after: Parser, min: number): Parser<[T[], U]> { + const element = seq(notMatch(stop), item).select(1); + const parser: Parser<[T[], U]> = new Parser((input, index, state) => { + state.manyThenFailures ??= new Map(); + const byInput = getOrCreate(state.manyThenFailures, parser, () => new Map()); + const byState = getOrCreate(byInput, input, () => new Map()); + const failures = getOrCreate(byState, stateKey(state), () => new Set()); + const boundaries: number[] = []; + const items: T[] = []; + let latestIndex = index; + while (!failures.has(latestIndex)) { + boundaries.push(latestIndex); + if (latestIndex >= input.length) { + break; + } + const result = element.handler(input, latestIndex, state); + if (!result.success) { + break; + } + items.push(result.value); + latestIndex = result.index; + } + if (items.length < min) { + return failure(); + } + const afterResult = failures.has(latestIndex) ? failure() : after.handler(input, latestIndex, state); + if (!afterResult.success) { + for (const boundary of boundaries) { + failures.add(boundary); + } + return failure(); + } + return success(afterResult.index, [items, afterResult.value]); + }); + return parser; +} + export const cr = str('\r'); export const lf = str('\n'); export const crlf = str('\r\n'); @@ -256,6 +357,26 @@ export function lazy(fn: () => Parser): Parser { type ParserTable = { [K in keyof T]: Parser }; +// logs the calls of the parser when `state.trace` is set +function traced(parser: Parser, name: string): Parser { + return new Parser((input, index, state) => { + if (!state.trace) { + return parser.handler(input, index, state); + } + const pos = `${index}`; + console.log(`${pos.padEnd(6, ' ')}enter ${name}`); + const result = parser.handler(input, index, state); + if (result.success) { + const pos = `${index}:${result.index}`; + console.log(`${pos.padEnd(6, ' ')}match ${name}`); + } else { + const pos = `${index}`; + console.log(`${pos.padEnd(6, ' ')}fail ${name}`); + } + return result; + }, name); +} + // TODO: 関数の型宣言をいい感じにしたい export function createLanguage(syntaxes: { [K in keyof T]: (r: ParserTable) => Parser }): ParserTable { // @ts-expect-error initializing object so type error here @@ -266,8 +387,7 @@ export function createLanguage(syntaxes: { [K in keyof T]: (r: ParserTable if (parser == null) { throw new Error('syntax must return a parser.'); } - parser.name = key; - return parser; + return traced(parser, key); }); } return rules; diff --git a/src/internal/index.ts b/src/internal/index.ts index 562dc62..29d03a1 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -9,6 +9,7 @@ export type FullParserOpts = { export function fullParser(input: string, opts: FullParserOpts): M.MfmNode[] { const result = language.fullParser.handler(input, 0, { nestLimit: opts.nestLimit ?? 20, + nestBudget: input.length * 20 + 1000, // normal input uses at most about 4 per character depth: 0, linkLabel: false, trace: false, diff --git a/src/internal/parser.ts b/src/internal/parser.ts index 37104bf..521f0a5 100644 --- a/src/internal/parser.ts +++ b/src/internal/parser.ts @@ -39,7 +39,28 @@ const notLinkLabel = new P.Parser((_input, index, state) => { : P.failure(); }); +// whether the character before the position is alphanumeric +function alphaAndNumBefore(input: string, index: number): boolean { + return /[a-z0-9]/i.test(input.charAt(index - 1)); +} + +// instead of a regexp like /[.,]+$/, which takes quadratic time on a long run +function trailingLength(text: string, chars: string): number { + let length = 0; + while (length < text.length && chars.includes(text.charAt(text.length - 1 - length))) { + length++; + } + return length; +} + const nestable = new P.Parser((_input, index, state) => { + // every attempt uses the budget, and nesting stops once it runs out, which bounds the time on crafted input + if (state.nestBudget != null) { + if (state.nestBudget <= 0) { + return P.failure(); + } + state.nestBudget--; + } return (state.depth < state.nestLimit) ? P.success(index, null) : P.failure(); @@ -59,22 +80,6 @@ function nest(parser: P.Parser, fallback?: P.Parser): P.Parser>[2]): Set { - // boundaries are tracked per input (quote contents are parsed as a separate input) and per depth - state.linkLabelFailures ??= new Map(); - let byDepth = state.linkLabelFailures.get(input); - if (byDepth == null) { - byDepth = new Map(); - state.linkLabelFailures.set(input, byDepth); - } - let failures = byDepth.get(state.depth); - if (failures == null) { - failures = new Set(); - byDepth.set(state.depth, failures); - } - return failures; -} - interface TypeTable { fullParser: (M.MfmNode | string)[], simpleParser: (M.MfmSimpleNode | string)[], @@ -124,53 +129,53 @@ export const language = P.createLanguage({ // Regexp r.unicodeEmoji, // "
" block - r.centerTag, + P.first('\r\n<', r.centerTag), // "" - r.smallTag, + P.first('<', r.smallTag), // "" - r.plainTag, + P.first('<', r.plainTag), // "" - r.boldTag, + P.first('<', r.boldTag), // "" - r.italicTag, + P.first('<', r.italicTag), // "" - r.strikeTag, + P.first('<', r.strikeTag), // "" block - r.quote, + P.first('\r\n>', r.quote), // "\\[" block - r.mathBlock, + P.first('\r\n\\', r.mathBlock), // "\\(" - r.mathInline, + P.first('\\', r.mathInline), // "~~" - r.strikeWave, + P.first('~', r.strikeWave), // "$["" - r.fn, + P.first('$', r.fn), // "@" - r.mention, + P.first('@', r.mention), // "#" - r.hashtag, + P.first('#', r.hashtag), // ":" - r.emojiCode, + P.first(':', r.emojiCode), // "?[" or "[" - r.link, + P.first('?[', r.link), // http - r.url, + P.first('h', r.url), // block r.search, r.text, @@ -180,8 +185,8 @@ export const language = P.createLanguage({ simple: r => { return P.alt([ r.unicodeEmoji, // Regexp - r.emojiCode, // ":" - r.plainTag, // "" // to NOT parse emojiCode inside `` + P.first(':', r.emojiCode), // ":" + P.first('<', r.plainTag), // "" // to NOT parse emojiCode inside `` r.text, ]); }, @@ -191,45 +196,45 @@ export const language = P.createLanguage({ // Regexp r.unicodeEmoji, // "" - r.smallTag, + P.first('<', r.smallTag), // "" - r.plainTag, + P.first('<', r.plainTag), // "" - r.boldTag, + P.first('<', r.boldTag), // "" - r.italicTag, + P.first('<', r.italicTag), // "" - r.strikeTag, + P.first('<', r.strikeTag), // ({ mark, P.seq(P.notMatch(newLine), P.char).select(1).many(0), newLine, - P.seq(P.notMatch(P.seq(newLine, mark, P.lineEnd)), P.char).select(1).many(1), + P.until(P.seq(newLine, mark, P.lineEnd), 1), newLine, mark, P.lineEnd, newLine.option(), ).map(result => { const lang = result[3].join('').trim(); - const code = result[5].join(''); + const code = result[5]; return M.CODE_BLOCK(code, (lang.length > 0 ? lang : null)); }); }, @@ -299,13 +304,13 @@ export const language = P.createLanguage({ P.lineBegin, open, newLine.option(), - P.seq(P.notMatch(P.seq(newLine.option(), close)), P.char).select(1).many(1), + P.until(P.seq(newLine.option(), close), 1), newLine.option(), close, P.lineEnd, newLine.option(), ).map(result => { - const formula = result[4].join(''); + const formula = result[4]; return M.MATH_BLOCK(formula); }); }, @@ -318,13 +323,19 @@ export const language = P.createLanguage({ P.lineBegin, open, newLine.option(), - P.seq(P.notMatch(P.seq(newLine.option(), close)), nest(r.inline)).select(1).many(1), - newLine.option(), - close, - P.lineEnd, - newLine.option(), + P.manyThen( + nest(r.inline), + P.seq(newLine.option(), close), + P.seq( + newLine.option(), + close, + P.lineEnd, + newLine.option(), + ), + 1, + ), ).map(result => { - return M.CENTER(mergeText(result[4])); + return M.CENTER(mergeText(result[4][0])); }); }, @@ -413,8 +424,7 @@ export const language = P.createLanguage({ return P.failure(); } // check before - const beforeStr = input.slice(0, index); - if (/[a-z0-9]$/i.test(beforeStr)) { + if (alphaAndNumBefore(input, index)) { return P.failure(); } return P.success(result.index, M.ITALIC(mergeText(result.value[1]))); @@ -434,8 +444,7 @@ export const language = P.createLanguage({ return P.failure(); } // check before - const beforeStr = input.slice(0, index); - if (/[a-z0-9]$/i.test(beforeStr)) { + if (alphaAndNumBefore(input, index)) { return P.failure(); } return P.success(result.index, M.ITALIC(mergeText(result.value[1]))); @@ -481,10 +490,7 @@ export const language = P.createLanguage({ return P.seq( open, newLine.option(), - P.seq( - P.notMatch(P.seq(newLine.option(), close)), - P.char, - ).select(1).many(1).text(), + P.until(P.seq(newLine.option(), close), 1), newLine.option(), close, ).select(2).map(result => M.PLAIN(result)); @@ -541,12 +547,9 @@ export const language = P.createLanguage({ const mark = P.str('`'); return P.seq( mark, - P.seq( - P.notMatch(P.alt([mark, P.str('´'), newLine])), - P.char, - ).select(1).many(1), + P.until(P.alt([mark, P.str('´'), newLine]), 1), mark, - ).map(result => M.INLINE_CODE(result[1].join(''))); + ).map(result => M.INLINE_CODE(result[1])); }, mathInline: () => { @@ -554,12 +557,9 @@ export const language = P.createLanguage({ const close = P.str('\\)'); return P.seq( open, - P.seq( - P.notMatch(P.alt([close, newLine])), - P.char, - ).select(1).many(1), + P.until(P.alt([close, newLine]), 1), close, - ).map(result => M.MATH_INLINE(result[1].join(''))); + ).map(result => M.MATH_INLINE(result[1])); }, mention: () => { @@ -579,8 +579,7 @@ export const language = P.createLanguage({ return P.failure(); } // check before (not mention) - const beforeStr = input.slice(0, index); - if (/[a-z0-9]$/i.test(beforeStr)) { + if (alphaAndNumBefore(input, index)) { return P.failure(); } let invalidMention = false; @@ -590,9 +589,9 @@ export const language = P.createLanguage({ // remove [.-] of tail of hostname let modifiedHost = hostname; if (hostname != null) { - result = /[.-]+$/.exec(hostname); - if (result != null) { - modifiedHost = hostname.slice(0, (-1 * result[0].length)); + const trailing = trailingLength(hostname, '.-'); + if (trailing > 0) { + modifiedHost = hostname.slice(0, (-1 * trailing)); if (modifiedHost.length === 0) { // disallow invalid char only hostname invalidMention = true; @@ -602,10 +601,10 @@ export const language = P.createLanguage({ } // remove [.-] of tail of username let modifiedName = username; - result = /[.-]+$/.exec(username); - if (result != null) { + const trailing = trailingLength(username, '.-'); + if (trailing > 0) { if (modifiedHost == null) { - modifiedName = username.slice(0, (-1 * result[0].length)); + modifiedName = username.slice(0, (-1 * trailing)); } else { // cannnot to remove tail of username if exist hostname invalidMention = true; @@ -660,8 +659,7 @@ export const language = P.createLanguage({ return P.failure(); } // check before - const beforeStr = input.slice(0, index); - if (/[a-z0-9]$/i.test(beforeStr)) { + if (alphaAndNumBefore(input, index)) { return P.failure(); } const resultIndex = result.index; @@ -694,57 +692,33 @@ export const language = P.createLanguage({ return result; }); const closeLabel = P.str(']'); - const openLabel = P.seq( - notLinkLabel, - P.alt([P.str('?['), P.str('[')]), - ).select(1); - const labelItem = P.seq( - P.notMatch(P.alt([closeLabel, newLine])), - nest(labelInline), - ).select(1); const afterLabel = P.seq( closeLabel, P.str('('), P.alt([r.urlAlt, r.url]), P.str(')'), ).select(2); - return new P.Parser((input, index, state) => { - const openResult = openLabel.handler(input, index, state); - if (!openResult.success) { + // fail on an invalid url (a string) so that `manyThen` caches the failure + const afterLabelUrl = new P.Parser((input, index, state) => { + const result = afterLabel.handler(input, index, state); + if (!result.success || typeof result.value === 'string') { return P.failure(); } - - const failedBoundaries = getLinkLabelFailures(input, state); - const boundaries: number[] = []; - const label: (M.MfmInline | string)[] = []; - let latestIndex = openResult.index; - while (!failedBoundaries.has(latestIndex)) { - boundaries.push(latestIndex); - if (latestIndex >= input.length) { - break; - } - const itemResult = labelItem.handler(input, latestIndex, state); - if (!itemResult.success) { - break; - } - label.push(itemResult.value); - latestIndex = itemResult.index; - } - if (label.length === 0) { - return P.failure(); - } - - const knownFailure = failedBoundaries.has(latestIndex); - const afterResult = knownFailure ? P.failure() : afterLabel.handler(input, latestIndex, state); - if (!afterResult.success || typeof afterResult.value === 'string') { - for (const boundary of boundaries) { - failedBoundaries.add(boundary); - } - return P.failure(); - } - - const silent = (openResult.value === '?['); - return P.success(afterResult.index, M.LINK(silent, afterResult.value.props.url, mergeText(label))); + return P.success(result.index, result.value); + }); + return P.seq( + notLinkLabel, + P.alt([P.str('?['), P.str('[')]), + P.manyThen( + nest(labelInline), + P.alt([closeLabel, newLine]), + afterLabelUrl, + 1, + ), + ).map(result => { + const silent = (result[1] === '?['); + const [label, url] = result[2]; + return M.LINK(silent, url.props.url, mergeText(label)); }); }, @@ -775,10 +749,10 @@ export const language = P.createLanguage({ const schema: string = result.value[1]; let content: string = result.value[2]; // remove the ".," at the right end - result = /[.,]+$/.exec(content); - if (result != null) { - modifiedIndex -= result[0].length; - content = content.slice(0, (-1 * result[0].length)); + const trailing = trailingLength(content, '.,'); + if (trailing > 0) { + modifiedIndex -= trailing; + content = content.slice(0, (-1 * trailing)); if (content.length === 0) { return P.success(resultIndex, input.slice(index, resultIndex)); } @@ -794,7 +768,7 @@ export const language = P.createLanguage({ notLinkLabel, open, P.regexp(/https?:\/\//), - P.seq(P.notMatch(P.alt([close, space])), P.char).select(1).many(1), + P.until(P.alt([close, space]), 1), close, ).text(); return new P.Parser((input, index, state) => { @@ -815,19 +789,16 @@ export const language = P.createLanguage({ return P.seq( newLine.option(), P.lineBegin, - P.seq( - P.notMatch(P.alt([ - newLine, - P.seq(space, button, P.lineEnd), - ])), - P.char, - ).select(1).many(1), + P.until(P.alt([ + newLine, + P.seq(space, button, P.lineEnd), + ]), 1), space, button, P.lineEnd, newLine.option(), ).map(result => { - const query = result[2].join(''); + const query = result[2]; return M.SEARCH(query, `${query}${result[3]}${result[4]}`); }); }, diff --git a/test/parser.ts b/test/parser.ts index 8c6a4d0..e45627c 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -281,6 +281,12 @@ hoge`; ]; assert.deepStrictEqual(mfm.parse(input), output); }); + + test('ignore internal markers not at line end', () => { + const input = '```a\nb\n```c\n```d\ne\n```'; + const output = [CODE_BLOCK('b\n```c\n```d\ne', 'a')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); }); describe('mathBlock', () => { @@ -314,6 +320,14 @@ hoge`; ]; assert.deepStrictEqual(mfm.parse(input), output); }); + test('with unclosed tag before', () => { + const input = '\\[a\\]b\n\\[c\\]'; + const output = [ + TEXT('\\[a\\]b'), + MATH_BLOCK('c') + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); }); describe('center', () => { @@ -337,6 +351,25 @@ hoge`; ]; assert.deepStrictEqual(mfm.parse(input), output); }); + test('with unclosed tag before', () => { + const input = '
a
b\n
c
'; + const output = [ + TEXT('
a
b'), + CENTER([ + TEXT('c') + ]), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + test('with close tag in plain', () => { + const input = '
'; + const output = [ + CENTER([ + PLAIN('') + ]), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); }); describe('emoji code', () => { @@ -677,6 +710,12 @@ hoge`; const output = [MATH_INLINE('x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}')]; assert.deepStrictEqual(mfm.parse(input), output); }); + + test('with unclosed tag in previous line', () => { + const input = '\\(a\n\\(b\\)'; + const output = [TEXT('\\(a\n'), MATH_INLINE('b')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); }); describe('mention', () => { @@ -734,6 +773,18 @@ hoge`; assert.deepStrictEqual(mfm.parse(input), output); }); + test('remove trailing periods only', () => { + const input = '@abc..def..'; + const output = [MENTION('abc..def', null, '@abc..def'), TEXT('..')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + test('remove trailing periods of hostname only', () => { + const input = '@abc..def@misskey..io..'; + const output = [MENTION('abc..def', 'misskey..io', '@abc..def@misskey..io'), TEXT('..')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + test('allow "-" in username', () => { const input = '@abc-d'; const output = [MENTION('abc-d', null, '@abc-d')]; @@ -973,6 +1024,15 @@ hoge`; assert.deepStrictEqual(mfm.parse(input), output); }); + test('ignore trailing periods only', () => { + const input = 'https://misskey.io/@ai...foo...'; + const output = [ + N_URL('https://misskey.io/@ai...foo'), + TEXT('...') + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + test('with comma', () => { const input = 'https://example.com/foo?bar=a,b'; const output = [ @@ -1054,6 +1114,38 @@ hoge`; assert.deepStrictEqual(mfm.parse(input), output); }); + test('with unclosed angle brackets before', () => { + const input = ''; + const output = [ + TEXT('<'), + N_URL('https://a'), + TEXT(' '), + N_URL('https://b', true), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + test('with unclosed angle brackets in previous line', () => { + const input = ''; + const output = [ + N_URL('https://a\n { + const input = '****'; + const output = [ + BOLD([ + TEXT('<'), + N_URL('https://a'), + TEXT(' '), + N_URL('https://b', true), + ]), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + test('prevent xss', () => { const input = 'javascript:foo'; const output = [ @@ -1226,6 +1318,28 @@ hoge`; assert.deepStrictEqual(mfm.parse(input), output); }) + test('with unclosed angle brackets in label', () => { + const input = '[](https://c)'; + const output = [ + LINK(false, 'https://c', [ + TEXT('') + ]), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + test('with unclosed angle brackets in url part', () => { + const input = '[x]()'; + const output = [ + TEXT('[x](<'), + N_URL('https://a'), + TEXT(' '), + N_URL('https://b', true), + TEXT(')'), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + test('with unclosed brackets before', () => { const input = '[[[foo] [bar](https://example.com)'; const output = [ @@ -1350,6 +1464,14 @@ hoge`; ]; assert.deepStrictEqual(mfm.parse(input), output); }); + + test('with unclosed tag before', () => { + const input = 'a\nb'; + const output = [ + PLAIN('a\nb'), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); }); describe('nesting limit', () => { From cad786c72a04471d61a2b866d164908b86b67cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:31:42 +0900 Subject: [PATCH 2/3] deps: update dependencies (#196) --- package.json | 8 +- pnpm-lock.yaml | 356 ++++++++++++++++++++++++------------------------- 2 files changed, 179 insertions(+), 185 deletions(-) diff --git a/package.json b/package.json index 225bd0e..50febac 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.27.0", "description": "An MFM parser implementation with TypeScript", "type": "module", - "packageManager": "pnpm@12.11.2", + "packageManager": "pnpm@12.14.0", "main": "./built/index.mjs", "types": "./built/index.d.ts", "exports": { @@ -36,13 +36,13 @@ "@misskey-dev/oxlint-fmt-config": "0.1.1", "@stylistic/eslint-plugin": "5.10.0", "@types/node": "26.6.5", - "@vitest/coverage-v8": "5.0.3", - "oxlint": "1.87.0", + "@vitest/coverage-v8": "5.0.5", + "oxlint": "1.88.0", "oxlint-tsgolint": "7.0.2004", "tsd": "0.33.0", "tsdown": "0.23.1", "typescript": "7.0.2", - "vitest": "5.0.3" + "vitest": "5.0.5" }, "dependencies": { "@misskey-dev/emoji-data": "17.0.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14a220a..78e469e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.11.2 - version: 12.11.2 + specifier: 12.14.0 + version: 12.14.0 packages: - '@pnpm/exe.android-arm64@12.11.2': - resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==} + '@pnpm/exe.android-arm64@12.14.0': + resolution: {integrity: sha512-+iSh7x21C3A2hqc+Z0ZmbKTcn6YNlzrXM142PsnQH/dLj0DYxFzoBvc6XAnL/yfQ5nnH2ebBiRj/zVxr61fZbQ==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.11.2': - resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==} + '@pnpm/exe.android-x64@12.14.0': + resolution: {integrity: sha512-qT+0LqEr2wBVaxcG2T8cX/R8SSRz2Rvpp8nRR4fh8tnGUZRch87gFmsHUAoSQh7w6j1h0qTjrA384j4xi8frvw==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.11.2': - resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==} + '@pnpm/exe.darwin-arm64@12.14.0': + resolution: {integrity: sha512-tnrdUB8ecJoehuKKqi1YHCBVl61xtJ9XVGcx5EviC6fWO4DeGI8GtXrjvJxDBM3nuVjJeWistgsUp/N142x4sw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.11.2': - resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==} + '@pnpm/exe.darwin-x64@12.14.0': + resolution: {integrity: sha512-vlBfGkruqQRsEHTW43cPwpYd7QlbIJ//cvpAdb3bVXkI8SgTPoXhCnc05m+ZNiPrrEkBsX/t7KHw9aXpkzPm3Q==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.11.2': - resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==} + '@pnpm/exe.freebsd-x64@12.14.0': + resolution: {integrity: sha512-Qg5rxBrGYl6Tfso+EyygPf84fY1Fzy55E/0or+iuYKtnf0vc0MlHJfVIlT7pBukMRntQHRik7hUi0Z4s7AlJsw==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.11.2': - resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==} + '@pnpm/exe.linux-arm64-musl@12.14.0': + resolution: {integrity: sha512-DRIe4yQkXBGA/PhVJdzlbnllxBdAtnMMvTVLLcGpsK5FvJz9kxD2Hyxtn68ovmNkVX06f6p45cQwfm3wx2jn1A==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.11.2': - resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==} + '@pnpm/exe.linux-arm64@12.14.0': + resolution: {integrity: sha512-7joGShZ44Nk3WpNU+60JoOPjCXlgxhJAq11S2UiVtwCUDgTer1yTPPfxGW8pxaD6046actYLIYDbZNlubHWs0A==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.11.2': - resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==} + '@pnpm/exe.linux-ppc64@12.14.0': + resolution: {integrity: sha512-gSoryn7ySdXd//gjgf3MvvA8ZerJayaSbuMZMtAWq4vb2/wlVEgpl0Kvqt54y58TRj533Qjq2txKgnxd4JyoAQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.11.2': - resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==} + '@pnpm/exe.linux-riscv64@12.14.0': + resolution: {integrity: sha512-EMjew1H9WqQ7pL+tYqCPwiuB+0PeCiJRV7fyWeuFw0wwYUajFBjcaPO3IGAVu9sdmJDU+e+HYv/Zey6aKae0Vw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.11.2': - resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==} + '@pnpm/exe.linux-s390x@12.14.0': + resolution: {integrity: sha512-shYOj/SFoXD35fHe0pWr2JofwSXulC0BnEIXLj2QokDi0EF//AfKsui35yKIkTEl+s2VJybc3PO4iJNhKIj5Qw==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.11.2': - resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==} + '@pnpm/exe.linux-x64-musl@12.14.0': + resolution: {integrity: sha512-btZiZ2Ot0llKK1ndSfA5i3IRX7/S0VrmzseNH/BJ4c+Cady+S81H6vvU1zByggHMyqQhC8OgxO5PiIY7D9bH0Q==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.11.2': - resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==} + '@pnpm/exe.linux-x64@12.14.0': + resolution: {integrity: sha512-F6K8e/jWxiuEYJaQtRRUG9ynBSTEHa+t7Lf86MKuOO5UA7WVVMbi+oB8nY6iKW++ncWS2yG6T9faXZGxqNsfoQ==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.11.2': - resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==} + '@pnpm/exe.win32-arm64@12.14.0': + resolution: {integrity: sha512-DOjlkVB6TtTKeAAe0hNZXn5S7YqESmo1Z6SGbR4BZNQ4isRg31XVHjX2RFTbY8gnZq68IwzyLmPqlEjMmYxofg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.11.2': - resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==} + '@pnpm/exe.win32-x64@12.14.0': + resolution: {integrity: sha512-/LSX6mew/sLf08XqcjemuklNKf1K/Ftpgj4//hK/HKzI2dKbmxDB27a/2OaQ5a0O3/3OFzlssXDziZsi+yVBwQ==} cpu: [x64] os: [win32] - pnpm@12.11.2: - resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==} + pnpm@12.14.0: + resolution: {integrity: sha512-wYum2HmT09Fky16JSPMrLAbuhUSH4GzY2GeBNC+z5btGNDZwNHEehoArqpw3vqkGzhkprQjXCITJfYAd8TOJpg==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.11.2': + '@pnpm/exe.android-arm64@12.14.0': optional: true - '@pnpm/exe.android-x64@12.11.2': + '@pnpm/exe.android-x64@12.14.0': optional: true - '@pnpm/exe.darwin-arm64@12.11.2': + '@pnpm/exe.darwin-arm64@12.14.0': optional: true - '@pnpm/exe.darwin-x64@12.11.2': + '@pnpm/exe.darwin-x64@12.14.0': optional: true - '@pnpm/exe.freebsd-x64@12.11.2': + '@pnpm/exe.freebsd-x64@12.14.0': optional: true - '@pnpm/exe.linux-arm64-musl@12.11.2': + '@pnpm/exe.linux-arm64-musl@12.14.0': optional: true - '@pnpm/exe.linux-arm64@12.11.2': + '@pnpm/exe.linux-arm64@12.14.0': optional: true - '@pnpm/exe.linux-ppc64@12.11.2': + '@pnpm/exe.linux-ppc64@12.14.0': optional: true - '@pnpm/exe.linux-riscv64@12.11.2': + '@pnpm/exe.linux-riscv64@12.14.0': optional: true - '@pnpm/exe.linux-s390x@12.11.2': + '@pnpm/exe.linux-s390x@12.14.0': optional: true - '@pnpm/exe.linux-x64-musl@12.11.2': + '@pnpm/exe.linux-x64-musl@12.14.0': optional: true - '@pnpm/exe.linux-x64@12.11.2': + '@pnpm/exe.linux-x64@12.14.0': optional: true - '@pnpm/exe.win32-arm64@12.11.2': + '@pnpm/exe.win32-arm64@12.14.0': optional: true - '@pnpm/exe.win32-x64@12.11.2': + '@pnpm/exe.win32-x64@12.14.0': optional: true - pnpm@12.11.2: + pnpm@12.14.0: optionalDependencies: - '@pnpm/exe.android-arm64': 12.11.2 - '@pnpm/exe.android-x64': 12.11.2 - '@pnpm/exe.darwin-arm64': 12.11.2 - '@pnpm/exe.darwin-x64': 12.11.2 - '@pnpm/exe.freebsd-x64': 12.11.2 - '@pnpm/exe.linux-arm64': 12.11.2 - '@pnpm/exe.linux-arm64-musl': 12.11.2 - '@pnpm/exe.linux-ppc64': 12.11.2 - '@pnpm/exe.linux-riscv64': 12.11.2 - '@pnpm/exe.linux-s390x': 12.11.2 - '@pnpm/exe.linux-x64': 12.11.2 - '@pnpm/exe.linux-x64-musl': 12.11.2 - '@pnpm/exe.win32-arm64': 12.11.2 - '@pnpm/exe.win32-x64': 12.11.2 + '@pnpm/exe.android-arm64': 12.14.0 + '@pnpm/exe.android-x64': 12.14.0 + '@pnpm/exe.darwin-arm64': 12.14.0 + '@pnpm/exe.darwin-x64': 12.14.0 + '@pnpm/exe.freebsd-x64': 12.14.0 + '@pnpm/exe.linux-arm64': 12.14.0 + '@pnpm/exe.linux-arm64-musl': 12.14.0 + '@pnpm/exe.linux-ppc64': 12.14.0 + '@pnpm/exe.linux-riscv64': 12.14.0 + '@pnpm/exe.linux-s390x': 12.14.0 + '@pnpm/exe.linux-x64': 12.14.0 + '@pnpm/exe.linux-x64-musl': 12.14.0 + '@pnpm/exe.win32-arm64': 12.14.0 + '@pnpm/exe.win32-x64': 12.14.0 --- lockfileVersion: '9.0' @@ -175,7 +175,7 @@ importers: version: 7.59.4(@types/node@26.6.5) '@misskey-dev/oxlint-fmt-config': specifier: 0.1.1 - version: 0.1.1(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(supports-color@8.1.1)))(oxlint-tsgolint@7.0.2004)(oxlint@1.87.0(oxlint-tsgolint@7.0.2004)) + version: 0.1.1(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(supports-color@8.1.1)))(oxlint-tsgolint@7.0.2004)(oxlint@1.88.0(oxlint-tsgolint@7.0.2004)) '@stylistic/eslint-plugin': specifier: 5.10.0 version: 5.10.0(eslint@9.39.4(supports-color@8.1.1)) @@ -183,11 +183,11 @@ importers: specifier: 26.6.5 version: 26.6.5 '@vitest/coverage-v8': - specifier: 5.0.3 - version: 5.0.3(vitest@5.0.3) + specifier: 5.0.5 + version: 5.0.5(vitest@5.0.5) oxlint: - specifier: 1.87.0 - version: 1.87.0(oxlint-tsgolint@7.0.2004) + specifier: 1.88.0 + version: 1.88.0(oxlint-tsgolint@7.0.2004) oxlint-tsgolint: specifier: 7.0.2004 version: 7.0.2004 @@ -201,8 +201,8 @@ importers: specifier: 7.0.2 version: 7.0.2 vitest: - specifier: 5.0.3 - version: 5.0.3(@types/node@26.6.5)(@vitest/coverage-v8@5.0.3)(vite@8.0.10(@types/node@26.6.5)) + specifier: 5.0.5 + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(vite@8.0.10(@types/node@26.6.5)) packages: @@ -395,124 +395,124 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.87.0': - resolution: {integrity: sha512-22tVIHlB9898OYsBKmYB6He1MCSqdTmK1BVWuSNUTVrpS0KWNege+U9uEYqGJ+KatQag0r/oU44i3OOs46z7HQ==} + '@oxlint/binding-android-arm-eabi@1.88.0': + resolution: {integrity: sha512-k6Fgkf86ubfeH4GvqYsRolfuVAp0pfYVZvifSlXNHPdMaWUOfxUrmLEeDD7hF6U2oJuhsVxhSWq86/xCJ+9MmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.87.0': - resolution: {integrity: sha512-JfZQ3MWf74n8ov0DojZpkOduIubYJvZL5tbTMMhDutsal6u1xH1QuNpN+lK+EQwF8AeAfgwTR2fgh5U2br27XA==} + '@oxlint/binding-android-arm64@1.88.0': + resolution: {integrity: sha512-AsIyhl9o1ejwtac7NMbhKpMdSml6NWR0ljKjZERxC8OHYDAW8o/Om9n1QHVLi4VCEUo42Zte2/Op9kyoGpJZMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.87.0': - resolution: {integrity: sha512-rkx6Xvvw93z2GImVOyvbZwAZcoY0Z6MJW177XNsSB+i/CBsH7ANyelTp01VZyIkM3iSXtqKVb4+ptBgPHap5pQ==} + '@oxlint/binding-darwin-arm64@1.88.0': + resolution: {integrity: sha512-qgBjB7eEKcCB4RqjXGuoSZq74g0wD09fWFJ+HWcCJcKaga8TeUHtb+BY0SsxdleSLBgx4RZ7qmgo0EzqEq7JoQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.87.0': - resolution: {integrity: sha512-cw5nYw7L/pQ2SgGBgOcg/Qo89llQre3vIc/kuBLhSICzu1r5s8L76H3ug6AIFsD98WpROj8/z5JVzkf4daxHaQ==} + '@oxlint/binding-darwin-x64@1.88.0': + resolution: {integrity: sha512-sCz3X5Mw/xdzEzn4fsCt1Iq8NKJnGNfhR+b9B5k+4DysYAHsmIhYoocnBPiQ9kvY3+pUun2A0Z8Vtun/Nv6YoQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.87.0': - resolution: {integrity: sha512-x/SuznE6loRZK9gz9AS4ZmzNzSu+5KdeoCS+sVHhnrE8HM6AD8UyTjwldQTTqlXGvkwHASgB0NiTNOc3o3PdAg==} + '@oxlint/binding-freebsd-x64@1.88.0': + resolution: {integrity: sha512-xTOdYOIqCufgIj0knvxu86FBfovP6QBiD7ngNva01gb8xPQCD5gXkE5APGczH35zStim1O3zqvFbLQmY85xXRg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.87.0': - resolution: {integrity: sha512-+JEDmCAXiiFmKhAjZhgAOwaHWY6WENOQ5QA+lFuY59tgsUCDOTRi46B+lENK6CWvtxsXpwkZXNOgpjAQcpIpnA==} + '@oxlint/binding-linux-arm-gnueabihf@1.88.0': + resolution: {integrity: sha512-BLadtw88NdYJBrAtC1dYzrPfSP2d5zgx6iuZycdD0YmZBJ2Nqbit9Z4PSaIb6gxK25dweArGMPbOmq/0ugwUPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.87.0': - resolution: {integrity: sha512-/uC9XqXLAC9AHFLNycwmSQEvKqGrOCgU+uXALI7gNW0DG81TJ/3T2eHnKdRvcb5R1gFEfjxDwI4e4zIa29rICQ==} + '@oxlint/binding-linux-arm-musleabihf@1.88.0': + resolution: {integrity: sha512-wILSczl/DaZsCcf7j7tvNbssp8hxEHIJxrv4j7+JjI3TAL1U/JTWbeUtoJJC/vo8+2hkTmHLYoKJjmiNbijdXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.87.0': - resolution: {integrity: sha512-FTRYPsBy/M728vnUv7l+KAwSAa3Qi4BQbhVvy7NYuMTZ0sUa/OWsbMx79HVFw6u/5KWcaYayDsoOiv7xbevzDg==} + '@oxlint/binding-linux-arm64-gnu@1.88.0': + resolution: {integrity: sha512-jutT2Hy8JCuZuMwIFHo2HtyxfOfBJHyQxlBHBK1SL9W2f8n/WhjAP0FnDMz9d/mjhv9+QA0mx5AdQ9fDjsrFFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.87.0': - resolution: {integrity: sha512-6sC+N7IY0h1egd8SrYUGNN7CUyKJZ4bIZakKa7V7hfinf4jfi8Kq4+klJvFp5DqXcwMn44I5MeBwmJluO1odFg==} + '@oxlint/binding-linux-arm64-musl@1.88.0': + resolution: {integrity: sha512-8N8iGV9hh4Lkeuq7QsEkjoXNRRflqR1G2qyoGYXcA0k5qllrdqmfylKEXyHIVERxqNDyMO4StqDQ0uLxXcBQAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.87.0': - resolution: {integrity: sha512-d5Mnn+ICbhoJPi37Ccjy+86T5qfH2+mSTrTMBXNPfB+wqSWOayBBLLIrCXdUy1YssluCVdGkl+DO9w5r5ODmUA==} + '@oxlint/binding-linux-ppc64-gnu@1.88.0': + resolution: {integrity: sha512-5ydcQt3znNQ/rGEWYRvpS2cK9+T18P2VYgnyJ+eloEAwi9YUmsXy5VGrRqNWIK6qzWwHGnEE3ojVqqQNo+f0hA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.87.0': - resolution: {integrity: sha512-sf5YWuUo1Dx9loqWCgF8P278MycfcD4JHjDC/LV5j11L3Lqa4PFiwCjD6B8kP4AjsgZZkLn3xbpJEtZBaccs4Q==} + '@oxlint/binding-linux-riscv64-gnu@1.88.0': + resolution: {integrity: sha512-iGx0uvFT54b/5DPJdEBCxYIrVB3v0uTy+JhRh3CyEp8/22BXDkG8rffkFwREdLsdj9mOrPbJYVOZw8ipcBuzjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.87.0': - resolution: {integrity: sha512-+aCraYbmK3uU3P0rFojjeaK9pGY6JvBDXh/0CM+gPyWKmNmXmnFe0qj4qj02U4ibFpbd9Pa9CmDvIdjJUd29rA==} + '@oxlint/binding-linux-riscv64-musl@1.88.0': + resolution: {integrity: sha512-Wt47mKMWelVjRjuR7BCFxXkB0m79K1sTEbqMHRWciKUgo5iv7sWsF4RXSOyFYHD9zerNrkRZblnN273rhwOA1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.87.0': - resolution: {integrity: sha512-7rzbZQIljo6VLWVPqUyLa2P/WV3VFsJB4M52R4NCUJn1A50yjtUsHxiMHFL9opEKGFIiTbZ3VQzmtcJOPUq4NQ==} + '@oxlint/binding-linux-s390x-gnu@1.88.0': + resolution: {integrity: sha512-g3L12KCS4+1gtUt3RbJ4F+Yxx4LNmmZLEPfhd8rAHEJjJk2lAgNfnjGoP/phJyhG5WS6VbNYbJtadJelNfvTMA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.87.0': - resolution: {integrity: sha512-Dry46Fa+AXE/xt9J/YriujU1pU9mzfNWHGQwtOYibAFH8HLrKWCi93AhIGnuUMiwn2JbuBkRyz51f50Ly4r4ow==} + '@oxlint/binding-linux-x64-gnu@1.88.0': + resolution: {integrity: sha512-og/XHVDOD9ZJgRiZkT9DgzQd4sr+JZW7e4l17Q/jVMZiIbeEd4uIu9iAoUbTF1gIvVZoRc4QDOxyVUvQGp4WHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.87.0': - resolution: {integrity: sha512-f/HMZez74gJvnRpGz+b5sdGyWrEgRLmFTXg9sNoqjPWogGxYHUhXKTKyMiYsu9rUGR2ciYqL3fk+Ua4JhMJbRA==} + '@oxlint/binding-linux-x64-musl@1.88.0': + resolution: {integrity: sha512-EmhBsp+xuKb6YbZNEMLBo80Xw71AZOIm18do8ni/GLX/Lw1XzNhVYpbxO1KasYtoAvrGtCOdCPULRlIE0bPbTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.87.0': - resolution: {integrity: sha512-lEC6ZfPZm/07JlB7FsOWX9o1RiHlXiBW594LSUGuXmekC8LZmvUPP9XzE2WZcNxJi5cHA/Fw56o02c3JxgMA5g==} + '@oxlint/binding-openharmony-arm64@1.88.0': + resolution: {integrity: sha512-DBUcx/KwYdg1DAz+G01P/GFv512J04EsZadbHptx2FTpWT/MWRg21ZToNHt9pnsfQRBQ3et/g+vqzbvrdAVbuA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.87.0': - resolution: {integrity: sha512-afBWKOD36dUiA9AenaiWpCg8xfyUVPYznyAAVXu413brK4RA5UI/Xoc4yfeGIT8G1a44MV+JR7hioryerBDzGQ==} + '@oxlint/binding-win32-arm64-msvc@1.88.0': + resolution: {integrity: sha512-xeVd0n4XG5HvynxfYvXi6uB+JvZbVZataowPS8cYpMbYg9mwE4sSyElFQd4LVJFifq8bRLDSR18XgEildmcXAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.87.0': - resolution: {integrity: sha512-daptNsEOuru4/nJTxV4DBHNJ5GepzmaBVQf+jjgKQGi9NRlZKik+RFq4hUL7ewsF1pwjKM+SkYaMbF82UwFGnw==} + '@oxlint/binding-win32-ia32-msvc@1.88.0': + resolution: {integrity: sha512-MA6iOw//amleaz2mVMsZrA7NHlEexjTrhIxSMFCXQi6IUuQO+3Od00GZhr2iCYw6RFzMW36jkKK/WHBXMsoQAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.87.0': - resolution: {integrity: sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw==} + '@oxlint/binding-win32-x64-msvc@1.88.0': + resolution: {integrity: sha512-3Sz6rVWe8Dy+ufsXurFN1GX2dX43F/lQMSLelK9gFjiba7nHvLlqlhR5cHlWVnCpSqUY1hN9pyCX9XToe6ve6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -918,11 +918,11 @@ packages: cpu: [x64] os: [win32] - '@vitest/coverage-v8@5.0.3': - resolution: {integrity: sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==} + '@vitest/coverage-v8@5.0.5': + resolution: {integrity: sha512-4KpkGdIrSqBihx/JyP/e+S+IKB98etg1XDQiLAxksLfcShnwi1VReGeOT9NI43hLHbknosKp/1CCLn9Zgcs8mg==} peerDependencies: - '@vitest/browser': 5.0.3 - vitest: 5.0.3 + '@vitest/browser': 5.0.5 + vitest: 5.0.5 peerDependenciesMeta: '@vitest/browser': optional: true @@ -935,10 +935,10 @@ packages: resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==} engines: {node: '>=22'} - '@vitest/mocker@5.0.3': - resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} + '@vitest/mocker@5.0.5': + resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} peerDependencies: - msw: ^2.4.9 + msw: ^2.4.9 || ^3.0.0 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: @@ -946,8 +946,8 @@ packages: vite: optional: true - '@vitest/spy@5.0.3': - resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} + '@vitest/spy@5.0.5': + resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} '@yuku-core/binding-android-arm64@0.19.0': resolution: {integrity: sha512-wdL2XmTiZixAOaHk9Z0JJZ8u9Qm8d333Zjpc6gaxXFX+UR5BlsfxZ7a2+2Rm71pamppsPYGUGFq2oVLbh6DISw==} @@ -1642,10 +1642,6 @@ packages: resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} engines: {node: '>=10'} - obug@2.2.1: - resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} - engines: {node: '>=12.20.0'} - obug@3.0.0: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} @@ -1658,8 +1654,8 @@ packages: resolution: {integrity: sha512-vQ9WDF3fiZJvRdSDosNOflerOCOpwol6vWzVDxW76RcU19cjTh7engvkHLvmv+tzedR8fAiJUVUGr6IVZdkzXQ==} hasBin: true - oxlint@1.87.0: - resolution: {integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg==} + oxlint@1.88.0: + resolution: {integrity: sha512-uIcFgw8BmqvFRGzH5+r7d2zJMEexwNSrEmM6WQNnGh3VkO0XYTEFoyxyNE6tjKMUib9Cue4XJA2DoWruO5D6aw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2086,20 +2082,20 @@ packages: yaml: optional: true - vitest@5.0.3: - resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + vitest@5.0.5: + resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 5.0.3 - '@vitest/browser-preview': 5.0.3 + '@vitest/browser-playwright': 5.0.5 + '@vitest/browser-preview': 5.0.5 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 - '@vitest/coverage-istanbul': 5.0.3 - '@vitest/coverage-v8': 5.0.3 - '@vitest/ui': 5.0.3 + '@vitest/coverage-istanbul': 5.0.5 + '@vitest/coverage-v8': 5.0.5 + '@vitest/ui': 5.0.5 happy-dom: '*' jsdom: '*' vite: ^6.4.0 || ^7.0.0 || ^8.0.0 @@ -2316,9 +2312,9 @@ snapshots: '@misskey-dev/emoji-data@17.0.3': {} - '@misskey-dev/oxlint-fmt-config@0.1.1(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(supports-color@8.1.1)))(oxlint-tsgolint@7.0.2004)(oxlint@1.87.0(oxlint-tsgolint@7.0.2004))': + '@misskey-dev/oxlint-fmt-config@0.1.1(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(supports-color@8.1.1)))(oxlint-tsgolint@7.0.2004)(oxlint@1.88.0(oxlint-tsgolint@7.0.2004))': dependencies: - oxlint: 1.87.0(oxlint-tsgolint@7.0.2004) + oxlint: 1.88.0(oxlint-tsgolint@7.0.2004) oxlint-tsgolint: 7.0.2004 optionalDependencies: '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(supports-color@8.1.1)) @@ -2364,61 +2360,61 @@ snapshots: '@oxlint-tsgolint/win32-x64@7.0.2004': optional: true - '@oxlint/binding-android-arm-eabi@1.87.0': + '@oxlint/binding-android-arm-eabi@1.88.0': optional: true - '@oxlint/binding-android-arm64@1.87.0': + '@oxlint/binding-android-arm64@1.88.0': optional: true - '@oxlint/binding-darwin-arm64@1.87.0': + '@oxlint/binding-darwin-arm64@1.88.0': optional: true - '@oxlint/binding-darwin-x64@1.87.0': + '@oxlint/binding-darwin-x64@1.88.0': optional: true - '@oxlint/binding-freebsd-x64@1.87.0': + '@oxlint/binding-freebsd-x64@1.88.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.87.0': + '@oxlint/binding-linux-arm-gnueabihf@1.88.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.87.0': + '@oxlint/binding-linux-arm-musleabihf@1.88.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.87.0': + '@oxlint/binding-linux-arm64-gnu@1.88.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.87.0': + '@oxlint/binding-linux-arm64-musl@1.88.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.87.0': + '@oxlint/binding-linux-ppc64-gnu@1.88.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.87.0': + '@oxlint/binding-linux-riscv64-gnu@1.88.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.87.0': + '@oxlint/binding-linux-riscv64-musl@1.88.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.87.0': + '@oxlint/binding-linux-s390x-gnu@1.88.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.87.0': + '@oxlint/binding-linux-x64-gnu@1.88.0': optional: true - '@oxlint/binding-linux-x64-musl@1.87.0': + '@oxlint/binding-linux-x64-musl@1.88.0': optional: true - '@oxlint/binding-openharmony-arm64@1.87.0': + '@oxlint/binding-openharmony-arm64@1.88.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.87.0': + '@oxlint/binding-win32-arm64-msvc@1.88.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.87.0': + '@oxlint/binding-win32-ia32-msvc@1.88.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.87.0': + '@oxlint/binding-win32-x64-msvc@1.88.0': optional: true '@quansync/fs@1.1.0': @@ -2669,17 +2665,17 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vitest/coverage-v8@5.0.3(vitest@5.0.3)': + '@vitest/coverage-v8@5.0.5(vitest@5.0.5)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/istanbul-lib-coverage': 1.0.2 '@vitest/istanbul-lib-report': 1.0.2 ast-v8-to-istanbul: 1.0.7 magicast: 0.5.5 - obug: 2.2.1 + obug: 3.0.0 std-env: 4.3.0 tinyrainbow: 3.2.0 - vitest: 5.0.3(@types/node@26.6.5)(@vitest/coverage-v8@5.0.3)(vite@8.0.10(@types/node@26.6.5)) + vitest: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(vite@8.0.10(@types/node@26.6.5)) '@vitest/istanbul-lib-coverage@1.0.2': {} @@ -2687,16 +2683,16 @@ snapshots: dependencies: '@vitest/istanbul-lib-coverage': 1.0.2 - '@vitest/mocker@5.0.3(vite@8.0.10(@types/node@26.6.5))': + '@vitest/mocker@5.0.5(vite@8.0.10(@types/node@26.6.5))': dependencies: '@jridgewell/trace-mapping': 0.3.31 - '@vitest/spy': 5.0.3 + '@vitest/spy': 5.0.5 estree-walker: 3.0.3 magic-string: 1.4.3 optionalDependencies: vite: 8.0.10(@types/node@26.6.5) - '@vitest/spy@5.0.3': {} + '@vitest/spy@5.0.5': {} '@yuku-core/binding-android-arm64@0.19.0': optional: true @@ -3307,8 +3303,6 @@ snapshots: semver: 7.8.5 validate-npm-package-license: 3.0.4 - obug@2.2.1: {} - obug@3.0.0: {} optionator@0.9.4: @@ -3329,27 +3323,27 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2004 '@oxlint-tsgolint/win32-x64': 7.0.2004 - oxlint@1.87.0(oxlint-tsgolint@7.0.2004): + oxlint@1.88.0(oxlint-tsgolint@7.0.2004): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.87.0 - '@oxlint/binding-android-arm64': 1.87.0 - '@oxlint/binding-darwin-arm64': 1.87.0 - '@oxlint/binding-darwin-x64': 1.87.0 - '@oxlint/binding-freebsd-x64': 1.87.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.87.0 - '@oxlint/binding-linux-arm-musleabihf': 1.87.0 - '@oxlint/binding-linux-arm64-gnu': 1.87.0 - '@oxlint/binding-linux-arm64-musl': 1.87.0 - '@oxlint/binding-linux-ppc64-gnu': 1.87.0 - '@oxlint/binding-linux-riscv64-gnu': 1.87.0 - '@oxlint/binding-linux-riscv64-musl': 1.87.0 - '@oxlint/binding-linux-s390x-gnu': 1.87.0 - '@oxlint/binding-linux-x64-gnu': 1.87.0 - '@oxlint/binding-linux-x64-musl': 1.87.0 - '@oxlint/binding-openharmony-arm64': 1.87.0 - '@oxlint/binding-win32-arm64-msvc': 1.87.0 - '@oxlint/binding-win32-ia32-msvc': 1.87.0 - '@oxlint/binding-win32-x64-msvc': 1.87.0 + '@oxlint/binding-android-arm-eabi': 1.88.0 + '@oxlint/binding-android-arm64': 1.88.0 + '@oxlint/binding-darwin-arm64': 1.88.0 + '@oxlint/binding-darwin-x64': 1.88.0 + '@oxlint/binding-freebsd-x64': 1.88.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.88.0 + '@oxlint/binding-linux-arm-musleabihf': 1.88.0 + '@oxlint/binding-linux-arm64-gnu': 1.88.0 + '@oxlint/binding-linux-arm64-musl': 1.88.0 + '@oxlint/binding-linux-ppc64-gnu': 1.88.0 + '@oxlint/binding-linux-riscv64-gnu': 1.88.0 + '@oxlint/binding-linux-riscv64-musl': 1.88.0 + '@oxlint/binding-linux-s390x-gnu': 1.88.0 + '@oxlint/binding-linux-x64-gnu': 1.88.0 + '@oxlint/binding-linux-x64-musl': 1.88.0 + '@oxlint/binding-openharmony-arm64': 1.88.0 + '@oxlint/binding-win32-arm64-msvc': 1.88.0 + '@oxlint/binding-win32-ia32-msvc': 1.88.0 + '@oxlint/binding-win32-x64-msvc': 1.88.0 oxlint-tsgolint: 7.0.2004 p-limit@2.3.0: @@ -3716,15 +3710,15 @@ snapshots: '@types/node': 26.6.5 fsevents: 2.3.3 - vitest@5.0.3(@types/node@26.6.5)(@vitest/coverage-v8@5.0.3)(vite@8.0.10(@types/node@26.6.5)): + vitest@5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(vite@8.0.10(@types/node@26.6.5)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.3(vite@8.0.10(@types/node@26.6.5)) + '@vitest/mocker': 5.0.5(vite@8.0.10(@types/node@26.6.5)) chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 magic-string: 1.4.3 - obug: 2.2.1 + obug: 3.0.0 picomatch: 4.0.7 std-env: 4.3.0 tinybench: 6.2.2 @@ -3734,7 +3728,7 @@ snapshots: why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 26.6.5 - '@vitest/coverage-v8': 5.0.3(vitest@5.0.3) + '@vitest/coverage-v8': 5.0.5(vitest@5.0.5) transitivePeerDependencies: - msw From e1d12a5f1d2bdc7887677c74f203d12a0d35fa13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:59:30 +0900 Subject: [PATCH 3/3] Release 0.28.0 (#197) * add tests * Release 0.28.0 --- CHANGELOG.md | 9 +++ package.json | 2 +- test/core.ts | 157 +++++++++++++++++++++++++++++++++++++++++++++++++ test/parser.ts | 10 ++++ 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 test/core.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ea2ef..beb0066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ ### Bugfixes --> +## 0.28.0 +### Changes +- We have added new restrictions during the parsing process to enhance security. While it is extremely rare, this may result in some highly complex MFM patterns failing to render.\ + We have tested several common examples of "MFM art" under these new constraints and confirmed that they can still be parsed successfully. + +### Bugfixes +- Update dependencies +- Security fixes + ## 0.27.0 ### Bugfixes - Restore CLI commands that are accidentally removed in 0.26.0 diff --git a/package.json b/package.json index 50febac..379f2bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mfm-js", - "version": "0.27.0", + "version": "0.28.0", "description": "An MFM parser implementation with TypeScript", "type": "module", "packageManager": "pnpm@12.14.0", diff --git a/test/core.ts b/test/core.ts new file mode 100644 index 0000000..b22876f --- /dev/null +++ b/test/core.ts @@ -0,0 +1,157 @@ +import { describe, test, vi } from 'vitest'; +import assert from 'assert'; +import * as P from '../src/internal/core'; +import { language } from '../src/internal/parser'; +import { mergeText } from '../src/internal/util'; +import { TEXT, BOLD, ITALIC } from '../src/index'; + +function createState() { + return { nestLimit: 20, depth: 0 }; +} + +describe('core', () => { + describe('first', () => { + test('match at the declared chars', () => { + const parser = P.first('a', P.str('ab')); + const result = parser.handler('ab', 0, createState()); + assert.deepStrictEqual(result, P.success(2, 'ab')); + }); + + test('fail at other chars', () => { + const parser = P.first('a', P.str('b')); + assert.deepStrictEqual(parser.handler('b', 0, createState()), P.failure()); + }); + + test('fail at the end of input', () => { + const parser = P.first('a', P.lineEnd); + assert.deepStrictEqual(parser.handler('a', 1, createState()), P.failure()); + }); + }); + + describe('alt', () => { + test('skip parsers that cannot start with the current char', () => { + const handler = vi.fn(() => P.success(1, 'b')); + const parser = P.alt([ + P.first('a', new P.Parser(handler)), + P.str('b'), + ]); + assert.deepStrictEqual(parser.handler('b', 0, createState()), P.success(1, 'b')); + assert.strictEqual(handler.mock.calls.length, 0); + }); + + test('skip parsers made by first at the end of input', () => { + const parser = P.alt([ + P.first('a', P.str('a')), + P.lineEnd, + ]); + assert.deepStrictEqual(parser.handler('a', 1, createState()), P.success(1, null)); + }); + }); + + describe('until', () => { + test('basic', () => { + const parser = P.until(P.str(']'), 1); + assert.deepStrictEqual(parser.handler('ab]', 0, createState()), P.success(2, 'ab')); + }); + + test('reuse the scanned span', () => { + const terminator = vi.fn(P.str(']').handler); + const parser = P.until(new P.Parser(terminator), 1); + const state = createState(); + assert.deepStrictEqual(parser.handler('ab]', 0, state), P.success(2, 'ab')); + const calls = terminator.mock.calls.length; + assert.deepStrictEqual(parser.handler('ab]', 1, state), P.success(2, 'b')); + assert.strictEqual(terminator.mock.calls.length, calls); + }); + + test('fail if shorter than min', () => { + const parser = P.until(P.str(']'), 1); + assert.deepStrictEqual(parser.handler(']', 0, createState()), P.failure()); + }); + }); + + describe('manyThen', () => { + test('basic', () => { + const parser = P.manyThen(P.char, P.str(']'), P.str(']'), 1); + assert.deepStrictEqual(parser.handler('ab]', 0, createState()), P.success(3, [['a', 'b'], ']'])); + }); + + test('fail if fewer items than min', () => { + const parser = P.manyThen(P.char, P.str(']'), P.str(']'), 1); + assert.deepStrictEqual(parser.handler(']', 0, createState()), P.failure()); + }); + + test('skip the after parser when reaching a known failure', () => { + const after = vi.fn(P.str(']').handler); + const parser = P.manyThen(P.char, P.str(']'), new P.Parser(after), 1); + const state = createState(); + assert.deepStrictEqual(parser.handler('xab', 1, state), P.failure()); + assert.strictEqual(after.mock.calls.length, 1); + assert.deepStrictEqual(parser.handler('xab', 0, state), P.failure()); + assert.strictEqual(after.mock.calls.length, 1); + }); + + test('keep failures separately for each state', () => { + // "after" succeeds only in link labels + const after = new P.Parser((_input, index, state) => state.linkLabel ? P.success(index, null) : P.failure()); + const parser = P.manyThen(P.char, P.str(']'), after, 1); + const state = { ...createState(), linkLabel: false }; + assert.deepStrictEqual(parser.handler('ab', 0, state), P.failure()); + state.linkLabel = true; + assert.deepStrictEqual(parser.handler('ab', 0, state), P.success(2, [['a', 'b'], null])); + }); + }); + + describe('createLanguage', () => { + test('log the calls of the rules when trace is enabled', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + const lang = P.createLanguage<{ a: string }>({ + a: () => P.str('a'), + }); + const state = { ...createState(), trace: true }; + lang.a.handler('a', 0, state); + lang.a.handler('b', 0, state); + assert.deepStrictEqual(log.mock.calls, [ + ['0 enter a'], + ['0:1 match a'], + ['0 enter a'], + ['0 fail a'], + ]); + } finally { + log.mockRestore(); + } + }); + + test('throw if the syntax does not return a parser', () => { + const lang = P.createLanguage<{ a: string }>({ + // @ts-expect-error returning an invalid value + a: () => null, + }); + assert.throws(() => lang.a.handler('a', 0, createState()), /syntax must return a parser\./); + }); + }); +}); + +describe('nest budget', () => { + test('nest while the budget remains', () => { + const input = '**a**'; + const result = language.fullParser.handler(input, 0, { ...createState(), nestBudget: 2 }); + assert.ok(result.success); + assert.deepStrictEqual(mergeText(result.value), [BOLD([ITALIC([TEXT('a')])])]); + }); + + test('stop nesting once the budget runs out', () => { + const input = '**a**'; + const result = language.fullParser.handler(input, 0, { ...createState(), nestBudget: 0 }); + assert.ok(result.success); + assert.deepStrictEqual(mergeText(result.value), [BOLD([TEXT('a')])]); + }); + + test('no limit without the budget', () => { + const input = '**a**'; + const result = language.fullParser.handler(input, 0, createState()); + assert.ok(result.success); + assert.deepStrictEqual(mergeText(result.value), [BOLD([ITALIC([TEXT('a')])])]); + }); +}); diff --git a/test/parser.ts b/test/parser.ts index e45627c..dffdafb 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -1351,6 +1351,16 @@ hoge`; assert.deepStrictEqual(mfm.parse(input), output); }); + test('with unclosed brackets after hashtag', () => { + const input = '[#a$[x'; + const output = [ + TEXT('['), + HASHTAG('a$'), + TEXT('[x'), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + test('with unclosed brackets in label', () => { const input = '[[[foo](https://example.com)'; const output = [