Repository: misskey-dev/mfm.js
Pull request #142 (2024-05-22 12:15:41)

fix lints and kill any
<!-- ℹ お読みください
PRありがとうございます！ PRを作成する前に、コントリビューションガイドをご確認ください:
https://github.com/misskey-dev/misskey.js/blob/develop/CONTRIBUTING.md
-->
<!-- ℹ README
Thank you for your PR! Before creating a PR, please check the contribution guide:
https://github.com/misskey-dev/misskey.js/blob/develop/docs/CONTRIBUTING.en.md
-->

# What
<!-- このPRで何をしたのか？ どう変わるのか？ -->
<!-- What did you do with this PR? How will it change things? -->

lintのエラーを消します

また、`[test](https://.)`でエラーになる問題を修正します。

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/misskey-dev/mfm.js/pull/142?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 97.82%. Comparing base [(`6aaf680`)](https://app.codecov.io/gh/misskey-dev/mfm.js/commit/6aaf68089023c6adebe44123eebbc4dcd75955e0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev) to head [(`befa1d4`)](https://app.codecov.io/gh/misskey-dev/mfm.js/pull/142?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev).


<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop     #142      +/-   ##
===========================================
+ Coverage    97.72%   97.82%   +0.09%     
===========================================
  Files            8        8              
  Lines         1541     1606      +65     
  Branches       233      326      +93     
===========================================
+ Hits          1506     1571      +65     
+ Misses          34       33       -1     
- Partials         1        2       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/misskey-dev/mfm.js/pull/142?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev).


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 92d542f..4f9858b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -43,14 +43,21 @@ module.exports = {
 		'prefer-arrow-callback': ['error'],
 		'no-throw-literal': ['error'],
 		'no-param-reassign': ['warn'],
-		'no-constant-condition': ['warn'],
+		'no-constant-condition': ['warn', {
+			checkLoops: false,
+		}],
 		'no-empty-pattern': ['warn'],
-		'@typescript-eslint/no-unnecessary-condition': ['warn'],
+		'@typescript-eslint/no-unnecessary-condition': ['warn', {
+			allowConstantLoopConditions: true,
+		}],
 		'@typescript-eslint/no-inferrable-types': ['warn'],
 		'@typescript-eslint/no-non-null-assertion': ['warn'],
 		'@typescript-eslint/explicit-function-return-type': ['warn'],
 		'@typescript-eslint/no-misused-promises': ['error', {
 			'checksVoidReturn': false,
 		}],
+		'@typescript-eslint/no-unused-vars': ['error', {
+			"argsIgnorePattern": "^_",
+		}]
 	},
 };
diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml
index dcbc7c0..7364074 100644
--- a/.github/workflows/api.yml
+++ b/.github/workflows/api.yml
@@ -14,7 +14,7 @@ jobs:
       - name: Setup Node.js
         uses: actions/setup-node@v1
         with:
-          node-version: 16.5.0
+          node-version: 16.10.0
 
       - name: Cache dependencies
         uses: actions/cache@v2
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index cdf6d20..dea9d5a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -14,7 +14,7 @@ jobs:
       - name: Setup Node.js
         uses: actions/setup-node@v1
         with:
-          node-version: 16.5.0
+          node-version: 16.10.0
 
       - name: Cache dependencies
         uses: actions/cache@v2
diff --git a/src/cli/parse.ts b/src/cli/parse.ts
index 120df08..b37e7ae 100644
--- a/src/cli/parse.ts
+++ b/src/cli/parse.ts
@@ -2,7 +2,7 @@ import { performance } from 'perf_hooks';
 import inputLine, { InputCanceledError } from './misc/inputLine';
 import { parse } from '..';
 
-async function entryPoint() {
+async function entryPoint(): Promise<void> {
 	console.log('intaractive parser');
 
 	while (true) {
diff --git a/src/cli/parseSimple.ts b/src/cli/parseSimple.ts
index 41674b4..e01b299 100644
--- a/src/cli/parseSimple.ts
+++ b/src/cli/parseSimple.ts
@@ -2,7 +2,7 @@ import { performance } from 'perf_hooks';
 import inputLine, { InputCanceledError } from './misc/inputLine';
 import { parseSimple } from '..';
 
-async function entryPoint() {
+async function entryPoint(): Promise<void> {
 	console.log('intaractive simple parser');
 
 	while (true) {
diff --git a/src/internal/core/index.ts b/src/internal/core/index.ts
index 2241d50..b9c21a1 100644
--- a/src/internal/core/index.ts
+++ b/src/internal/core/index.ts
@@ -12,7 +12,14 @@ export type Failure = { success: false };
 
 export type Result<T> = Success<T> | Failure;
 
-export type ParserHandler<T> = (input: string, index: number, state: any) => Result<T>
+interface State {
+	trace?: boolean,
+	linkLabel?: boolean,
+	nestLimit: number,
+	depth: number,
+}
+
+export type ParserHandler<T> = (input: string, index: number, state: State) => Result<T>
 
 export function success<T>(index: number, value: T): Success<T> {
 	return {
@@ -31,7 +38,7 @@ export class Parser<T> {
 	public handler: ParserHandler<T>;
 
 	constructor(handler: ParserHandler<T>, name?: string) {
-		this.handler = (input, index, state) => {
+		this.handler = (input, index, state) : Failure | Success<T> => {
 			if (state.trace && this.name != null) {
 				const pos = `${index}`;
 				console.log(`${pos.padEnd(6, ' ')}enter ${this.name}`);
@@ -91,20 +98,24 @@ export class Parser<T> {
 		});
 	}
 
-	sep(separator: Parser<any>, min: number): Parser<T[]> {
+	sep(separator: Parser<unknown>, min: number): Parser<T[]> {
 		if (min < 1) {
 			throw new Error('"min" must be a value greater than or equal to 1.');
 		}
-		return seq([
+		return seq(
 			this,
-			seq([
+			seq(
 				separator,
 				this,
-			], 1).many(min - 1),
-		]).map(result => [result[0], ...result[1]]);
+			).select(1).many(min - 1),
+		).map(result => [result[0], ...result[1]]);
 	}
 
-	option<T>(): Parser<T | null> {
+	select<K extends keyof T>(key: K): Parser<T[K]> {
+		return this.map(v => v[key]);
+	}
+
+	option(): Parser<T | null> {
 		return alt([
 			this,
 			succeeded(null),
@@ -136,7 +147,17 @@ export function regexp<T extends RegExp>(pattern: T): Parser<string> {
 	});
 }
 
-export function seq(parsers: Parser<any>[], select?: number): Parser<any> {
+type ParsedType<T extends Parser<unknown>> = T extends Parser<infer U> ? U : never;
+
+export type SeqParseResult<T extends unknown[]> =
+	T extends [] ? [] 
+		: T extends [infer F, ...infer R]
+		? (
+			F extends Parser<unknown> ? [ParsedType<F>, ...SeqParseResult<R>] : [unknown, ...SeqParseResult<R>]
+			)
+		: unknown[];
+
+export function seq<Parsers extends Parser<unknown>[]>(...parsers: Parsers): Parser<SeqParseResult<Parsers>> {
 	return new Parser((input, index, state) => {
 		let result;
 		let latestIndex = index;
@@ -149,17 +170,17 @@ export function seq(parsers: Parser<any>[], select?: number): Parser<any> {
 			latestIndex = result.index;
 			accum.push(result.value);
 		}
-		return success(latestIndex, (select != null ? accum[select] : accum));
+		return success(latestIndex, accum as SeqParseResult<Parsers>);
 	});
 }
 
-export function alt(parsers: Parser<any>[]): Parser<any> {
-	return new Parser((input, index, state) => {
-		let result;
+export function alt<Parsers extends Parser<unknown>[]>(parsers: Parsers): Parser<ParsedType<Parsers[number]>> {
+	return new Parser<ParsedType<Parsers[number]>>((input, index, state): Result<ParsedType<Parsers[number]>> => {
 		for (let i = 0; i < parsers.length; i++) {
-			result = parsers[i].handler(input, index, state);
+			const parser: Parsers[number] = parsers[i];
+			const result = parser.handler(input, index, state);
 			if (result.success) {
-				return result;
+				return result as Result<ParsedType<Parsers[number]>>;
 			}
 		}
 		return failure();
@@ -172,7 +193,7 @@ function succeeded<T>(value: T): Parser<T> {
 	});
 }
 
-export function notMatch(parser: Parser<any>): Parser<null> {
+export function notMatch(parser: Parser<unknown>): Parser<null> {
 	return new Parser((input, index, state) => {
 		const result = parser.handler(input, index, state);
 		return !result.success
@@ -232,12 +253,15 @@ export function lazy<T>(fn: () => Parser<T>): Parser<T> {
 //type SyntaxReturn<T> = T extends (rules: Record<string, Parser<any>>) => infer R ? R : never;
 //export function createLanguage2<T extends Record<string, Syntax<any>>>(syntaxes: T): { [K in keyof T]: SyntaxReturn<T[K]> } {
 
+type ParserTable<T> = { [K in keyof T]: Parser<T[K]> };
+
 // TODO: 関数の型宣言をいい感じにしたい
-export function createLanguage<T>(syntaxes: { [K in keyof T]: (r: Record<string, Parser<any>>) => T[K] }): T {
-	const rules: Record<string, Parser<any>> = {};
-	for (const key of Object.keys(syntaxes)) {
+export function createLanguage<T>(syntaxes: { [K in keyof T]: (r: ParserTable<T>) => Parser<T[K]> }): ParserTable<T> {
+	// @ts-expect-error initializing object so type error here
+	const rules: ParserTable<T> = {};
+	for (const key of Object.keys(syntaxes) as (keyof T & string)[]) {
 		rules[key] = lazy(() => {
-			const parser = (syntaxes as any)[key](rules);
+			const parser = syntaxes[key](rules);
 			if (parser == null) {
 				throw new Error('syntax must return a parser.');
 			}
@@ -245,5 +269,5 @@ export function createLanguage<T>(syntaxes: { [K in keyof T]: (r: Record<string,
 			return parser;
 		});
 	}
-	return rules as any;
+	return rules;
 }
diff --git a/src/internal/index.ts b/src/internal/index.ts
index 45138c3..a62fa13 100644
--- a/src/internal/index.ts
+++ b/src/internal/index.ts
@@ -1,7 +1,6 @@
 import * as M from '..';
 import { language } from './parser';
 import { mergeText } from './util';
-import * as P from './core';
 
 export type FullParserOpts = {
 	nestLimit?: number;
@@ -13,11 +12,16 @@ export function fullParser(input: string, opts: FullParserOpts): M.MfmNode[] {
 		depth: 0,
 		linkLabel: false,
 		trace: false,
-	}) as P.Success<any>;
+	});
+	if (!result.success) throw new Error('Unexpected parse error');
 	return mergeText(result.value);
 }
 
 export function simpleParser(input: string): M.MfmSimpleNode[] {
-	const result = language.simpleParser.handler(input, 0, { }) as P.Success<any>;
+	const result = language.simpleParser.handler(input, 0, {
+		depth: 0,
+		nestLimit: 1 / 0, // reliable infinite
+	});
+	if (!result.success) throw new Error('Unexpected parse error');
 	return mergeText(result.value);
 }
diff --git a/src/internal/parser.ts b/src/internal/parser.ts
index 20e0072..d64b122 100644
--- a/src/internal/parser.ts
+++ b/src/internal/parser.ts
@@ -1,6 +1,7 @@
 import * as M from '..';
 import * as P from './core';
 import { mergeText } from './util';
+import { SeqParseResult } from './core';
 
 // NOTE:
 // tsdのテストでファイルを追加しているにも関わらず「@twemoji/parser/dist/lib/regex」の型定義ファイルがないとエラーが出るため、
@@ -16,9 +17,10 @@ const space = P.regexp(/[\u0020\u3000\t]/);
 const alphaAndNum = P.regexp(/[a-z0-9]/i);
 const newLine = P.alt([P.crlf, P.cr, P.lf]);
 
-function seqOrText(parsers: P.Parser<any>[]): P.Parser<any[] | string> {
-	return new P.Parser<any[] | string>((input, index, state) => {
-		const accum: any[] = [];
+function seqOrText<Parsers extends P.Parser<unknown>[]>(...parsers: Parsers): P.Parser<SeqParseResult<Parsers> | string> {
+	return new P.Parser<SeqParseResult<Parsers> | string>((input, index, state) => {
+		// TODO: typesafe implementation
+		const accum: unknown[] = [];
 		let latestIndex = index;
 		for (let i = 0 ; i < parsers.length; i++) {
 			const result = parsers[i].handler(input, latestIndex, state);
@@ -32,7 +34,7 @@ function seqOrText(parsers: P.Parser<any>[]): P.Parser<any[] | string> {
 			accum.push(result.value);
 			latestIndex = result.index;
 		}
-		return P.success(latestIndex, accum);
+		return P.success(latestIndex, accum as SeqParseResult<Parsers>);
 	});
 }
 
@@ -51,7 +53,7 @@ const nestable = new P.Parser((_input, index, state) => {
 function nest<T>(parser: P.Parser<T>, fallback?: P.Parser<string>): P.Parser<T | string> {
 	// nesting limited? -> No: specified parser, Yes: fallback parser (default = P.char)
 	const inner = P.alt([
-		P.seq([nestable, parser], 1),
+		P.seq(nestable, parser).select(1),
 		(fallback != null) ? fallback : P.char,
 	]);
 	return new P.Parser<T | string>((input, index, state) => {
@@ -62,7 +64,42 @@ function nest<T>(parser: P.Parser<T>, fallback?: P.Parser<string>): P.Parser<T |
 	});
 }
 
-export const language = P.createLanguage({
+interface TypeTable {
+	fullParser: (M.MfmNode | string)[],
+	simpleParser: (M.MfmSimpleNode | string)[],
+	full: M.MfmNode | string,
+	simple: M.MfmSimpleNode | string,
+	inline: M.MfmInline | string,
+	quote: M.NodeType<'quote'>,
+	codeBlock: M.NodeType<'blockCode'>,
+	mathBlock: M.NodeType<'mathBlock'>,
+	centerTag: M.NodeType<'center'>,
+	big: M.NodeType<'fn'> | string,
+	boldAsta: M.NodeType<'bold'> | string,
+	boldTag: M.NodeType<'bold'> | string,
+	boldUnder: M.NodeType<'bold'>,
+	smallTag: M.NodeType<'small'> | string,
+	italicTag: M.NodeType<'italic'> | string,
+	italicAsta: M.NodeType<'italic'>,
+	italicUnder: M.NodeType<'italic'>,
+	strikeTag: M.NodeType<'strike'> | string,
+	strikeWave: M.NodeType<'strike'> | string,
+	unicodeEmoji: M.NodeType<'unicodeEmoji'>,
+	plainTag: M.NodeType<'plain'>,
+	fn: M.NodeType<'fn'> | string,
+	inlineCode: M.NodeType<'inlineCode'>,
+	mathInline: M.NodeType<'mathInline'>,
+	mention: M.NodeType<'mention'> | string,
+	hashtag: M.NodeType<'hashtag'>,
+	emojiCode: M.NodeType<'emojiCode'>,
+	link: M.NodeType<'link'>,
+	url: M.NodeType<'url'> | string,
+	urlAlt: M.NodeType<'url'>,
+	search: M.NodeType<'search'>,
+	text: string,
+}
+
+export const language = P.createLanguage<TypeTable>({
 	fullParser: r => {
 		return r.full.many(0);
 	},
@@ -186,19 +223,19 @@ export const language = P.createLanguage({
 	},
 
 	quote: r => {
-		const lines: P.Parser<string[]> = P.seq([
+		const lines: P.Parser<string[]> = P.seq( 
 			P.str('>'),
 			space.option(),
-			P.seq([P.notMatch(newLine), P.char], 1).many(0).text(),
-		], 2).sep(newLine, 1);
-		const parser = P.seq([
+			P.seq(P.notMatch(newLine), P.char).select(1).many(0).text(),
+		).select(2).sep(newLine, 1);
+		const parser = P.seq( 
 			newLine.option(),
 			newLine.option(),
 			P.lineBegin,
 			lines,
 			newLine.option(),
 			newLine.option(),
-		], 3);
+		).select(3);
 		return new P.Parser((input, index, state) => {
 			let result;
 			// parse quote
@@ -222,41 +259,41 @@ export const language = P.createLanguage({
 		});
 	},
 
-	codeBlock: r => {
+	codeBlock: () => {
 		const mark = P.str('```');
-		return P.seq([
+		return P.seq(
 			newLine.option(),
 			P.lineBegin,
 			mark,
-			P.seq([P.notMatch(newLine), P.char], 1).many(0),
+			P.seq(P.notMatch(newLine), P.char).select(1).many(0),
 			newLine,
-			P.seq([P.notMatch(P.seq([newLine, mark, P.lineEnd])), P.char], 1).many(1),
+			P.seq(P.notMatch(P.seq(newLine, mark, P.lineEnd)), P.char).select(1).many(1),
 			newLine,
 			mark,
 			P.lineEnd,
 			newLine.option(),
-		]).map(result => {
-			const lang = (result[3] as string[]).join('').trim();
-			const code = (result[5] as string[]).join('');
+		).map(result => {
+			const lang = result[3].join('').trim();
+			const code = result[5].join('');
 			return M.CODE_BLOCK(code, (lang.length > 0 ? lang : null));
 		});
 	},
 
-	mathBlock: r => {
+	mathBlock: () => {
 		const open = P.str('\\[');
 		const close = P.str('\\]');
-		return P.seq([
+		return P.seq(
 			newLine.option(),
 			P.lineBegin,
 			open,
 			newLine.option(),
-			P.seq([P.notMatch(P.seq([newLine.option(), close])), P.char], 1).many(1),
+			P.seq(P.notMatch(P.seq(newLine.option(), close)), P.char).select(1).many(1),
 			newLine.option(),
 			close,
 			P.lineEnd,
 			newLine.option(),
-		]).map(result => {
-			const formula = (result[4] as string[]).join('');
+		).map(result => {
+			const formula = result[4].join('');
 			return M.MATH_BLOCK(formula);
 		});
 	},
@@ -264,28 +301,28 @@ export const language = P.createLanguage({
 	centerTag: r => {
 		const open = P.str('<center>');
 		const close = P.str('</center>');
-		return P.seq([
+		return P.seq(
 			newLine.option(),
 			P.lineBegin,
 			open,
 			newLine.option(),
-			P.seq([P.notMatch(P.seq([newLine.option(), close])), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(P.seq(newLine.option(), close)), nest(r.inline)).select(1).many(1),
 			newLine.option(),
 			close,
 			P.lineEnd,
 			newLine.option(),
-		]).map(result => {
+		).map(result => {
 			return M.CENTER(mergeText(result[4]));
 		});
 	},
 
 	big: r => {
 		const mark = P.str('***');
-		return seqOrText([
+		return seqOrText(
 			mark,
-			P.seq([P.notMatch(mark), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(mark), nest(r.inline)).select(1).many(1),
 			mark,
-		]).map(result => {
+		).map(result => {
 			if (typeof result === 'string') return result;
 			return M.FN('tada', {}, mergeText(result[1]));
 		});
@@ -293,71 +330,71 @@ export const language = P.createLanguage({
 
 	boldAsta: r => {
 		const mark = P.str('**');
-		return seqOrText([
+		return seqOrText(
 			mark,
-			P.seq([P.notMatch(mark), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(mark), nest(r.inline)).select(1).many(1),
 			mark,
-		]).map(result => {
+		).map(result => {
 			if (typeof result === 'string') return result;
-			return M.BOLD(mergeText(result[1] as (M.MfmInline | string)[]));
+			return M.BOLD(mergeText(result[1]));
 		});
 	},
 
 	boldTag: r => {
 		const open = P.str('<b>');
 		const close = P.str('</b>');
-		return seqOrText([
+		return seqOrText(
 			open,
-			P.seq([P.notMatch(close), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(close), nest(r.inline)).select(1).many(1),
 			close,
-		]).map(result => {
+		).map(result => {
 			if (typeof result === 'string') return result;
-			return M.BOLD(mergeText(result[1] as (M.MfmInline | string)[]));
+			return M.BOLD(mergeText(result[1]));
 		});
 	},
 
-	boldUnder: r => {
+	boldUnder: () => {
 		const mark = P.str('__');
-		return P.seq([
+		return P.seq(
 			mark,
 			P.alt([alphaAndNum, space]).many(1),
 			mark,
-		]).map(result => M.BOLD(mergeText(result[1] as string[])));
+		).map(result => M.BOLD(mergeText(result[1])));
 	},
 
 	smallTag: r => {
 		const open = P.str('<small>');
 		const close = P.str('</small>');
-		return seqOrText([
+		return seqOrText(
 			open,
-			P.seq([P.notMatch(close), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(close), nest(r.inline)).select(1).many(1),
 			close,
-		]).map(result => {
+		).map(result => {
 			if (typeof result === 'string') return result;
-			return M.SMALL(mergeText(result[1] as (M.MfmInline | string)[]));
+			return M.SMALL(mergeText(result[1]));
 		});
 	},
 
 	italicTag: r => {
 		const open = P.str('<i>');
 		const close = P.str('</i>');
-		return seqOrText([
+		return seqOrText(
 			open,
-			P.seq([P.notMatch(close), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(close), nest(r.inline)).select(1).many(1),
 			close,
-		]).map(result => {
+		).map(result => {
 			if (typeof result === 'string') return result;
-			return M.ITALIC(mergeText(result[1] as (M.MfmInline | string)[]));
+			return M.ITALIC(mergeText(result[1]));
 		});
 	},
 
-	italicAsta: r => {
+	italicAsta: () => {
 		const mark = P.str('*');
-		const parser = P.seq([
+		const parser = P.seq(
 			mark,
 			P.alt([alphaAndNum, space]).many(1),
 			mark,
-		]);
+		);
 		return new P.Parser((input, index, state) => {
 			const result = parser.handler(input, index, state);
 			if (!result.success) {
@@ -368,17 +405,17 @@ export const language = P.createLanguage({
 			if (/[a-z0-9]$/i.test(beforeStr)) {
 				return P.failure();
 			}
-			return P.success(result.index, M.ITALIC(mergeText(result.value[1] as string[])));
+			return P.success(result.index, M.ITALIC(mergeText(result.value[1])));
 		});
 	},
 
-	italicUnder: r => {
+	italicUnder: () => {
 		const mark = P.str('_');
-		const parser = P.seq([
+		const parser = P.seq(
 			mark,
 			P.alt([alphaAndNum, space]).many(1),
 			mark,
-		]);
+		);
 		return new P.Parser((input, index, state) => {
 			const result = parser.handler(input, index, state);
 			if (!result.success) {
@@ -389,53 +426,53 @@ export const language = P.createLanguage({
 			if (/[a-z0-9]$/i.test(beforeStr)) {
 				return P.failure();
 			}
-			return P.success(result.index, M.ITALIC(mergeText(result.value[1] as string[])));
+			return P.success(result.index, M.ITALIC(mergeText(result.value[1])));
 		});
 	},
 
 	strikeTag: r => {
 		const open = P.str('
');
 		const close = P.str('
');
-		return seqOrText([
+		return seqOrText(
 			open,
-			P.seq([P.notMatch(close), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(close), nest(r.inline)).select(1).many(1),
 			close,
-		]).map(result => {
+		).map(result => {
 			if (typeof result === 'string') return result;
-			return M.STRIKE(mergeText(result[1] as (M.MfmInline | string)[]));
+			return M.STRIKE(mergeText(result[1]));
 		});
 	},
 
 	strikeWave: r => {
 		const mark = P.str('~~');
-		return seqOrText([
+		return seqOrText(
 			mark,
-			P.seq([P.notMatch(P.alt([mark, newLine])), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(P.alt([mark, newLine])), nest(r.inline)).select(1).many(1),
 			mark,
-		]).map(result => {
+		).map(result => {
 			if (typeof result === 'string') return result;
-			return M.STRIKE(mergeText(result[1] as (M.MfmInline | string)[]));
+			return M.STRIKE(mergeText(result[1]));
 		});
 	},
 
-	unicodeEmoji: r => {
+	unicodeEmoji: () => {
 		const emoji = RegExp(twemojiRegex.source);
 		return P.regexp(emoji).map(content => M.UNI_EMOJI(content));
 	},
 
-	plainTag: r => {
+	plainTag: () => {
 		const open = P.str('<plain>');
 		const close = P.str('</plain>');
-		return P.seq([
+		return P.seq( 
 			open,
 			newLine.option(),
-			P.seq([
-				P.notMatch(P.seq([newLine.option(), close])),
+			P.seq(
+				P.notMatch(P.seq(newLine.option(), close)),
 				P.char,
-			], 1).many(1).text(),
+			).select(1).many(1).text(),
 			newLine.option(),
 			close,
-		], 2).map(result => M.PLAIN(result));
+		).select(2).map(result => M.PLAIN(result));
 	},
 
 	fn: r => {
@@ -446,22 +483,22 @@ export const language = P.createLanguage({
 			}
 			return P.success(result.index, result.value);
 		});
-		const arg: P.Parser<ArgPair> = P.seq([
+		const arg: P.Parser<ArgPair> = P.seq(
 			P.regexp(/[a-z0-9_]+/i),
-			P.seq([
+			P.seq(
 				P.str('='),
 				P.regexp(/[a-z0-9_.-]+/i),
-			], 1).option(),
-		]).map(result => {
+			).select(1).option(),
+		).map(result => {
 			return {
 				k: result[0],
 				v: (result[1] != null) ? result[1] : true,
 			};
 		});
-		const args = P.seq([
+		const args = P.seq(
 			P.str('.'),
 			arg.sep(P.str(','), 1),
-		], 1).map(pairs => {
+		).select(1).map(pairs => {
 			const result: Args = { };
 			for (const pair of pairs) {
 				result[pair.k] = pair.v;
@@ -469,57 +506,57 @@ export const language = P.createLanguage({
 			return result;
 		});
 		const fnClose = P.str(']');
-		return seqOrText([
+		return seqOrText(
 			P.str('$['),
 			fnName,
 			args.option(),
 			P.str(' '),
-			P.seq([P.notMatch(fnClose), nest(r.inline)], 1).many(1),
+			P.seq(P.notMatch(fnClose), nest(r.inline)).select(1).many(1),
 			fnClose,
-		]).map(result => {
+		).map(result => {
 			if (typeof result === 'string') return result;
 			const name = result[1];
-			const args = result[2] || {};
+			const args: Args = result[2] || {};
 			const content = result[4];
 			return M.FN(name, args, mergeText(content));
 		});
 	},
 
-	inlineCode: r => {
+	inlineCode: () => {
 		const mark = P.str('`');
-		return P.seq([
+		return P.seq(
 			mark,
-			P.seq([
+			P.seq(
 				P.notMatch(P.alt([mark, P.str('´'), newLine])),
 				P.char,
-			], 1).many(1),
+			).select(1).many(1),
 			mark,
-		]).map(result => M.INLINE_CODE(result[1].join('')));
+		).map(result => M.INLINE_CODE(result[1].join('')));
 	},
 
-	mathInline: r => {
+	mathInline: () => {
 		const open = P.str('\\(');
 		const close = P.str('\\)');
-		return P.seq([
+		return P.seq(
 			open,
-			P.seq([
+			P.seq(
 				P.notMatch(P.alt([close, newLine])),
 				P.char,
-			], 1).many(1),
+			).select(1).many(1),
 			close,
-		]).map(result => M.MATH_INLINE(result[1].join('')));
+		).map(result => M.MATH_INLINE(result[1].join('')));
 	},
 
-	mention: r => {
-		const parser = P.seq([
+	mention: () => {
+		const parser = P.seq(
 			notLinkLabel,
 			P.str('@'),
 			P.regexp(/[a-z0-9_-]+/i),
-			P.seq([
+			P.seq(
 				P.str('@'),
 				P.regexp(/[a-z0-9_.-]+/i),
-			], 1).option(),
-		]);
+			).select(1).option(),
+		);
 		return new P.Parser<M.MfmMention | string>((input, index, state) => {
 			let result;
 			result = parser.handler(input, index, state);
@@ -576,32 +613,32 @@ export const language = P.createLanguage({
 		});
 	},
 
-	hashtag: r => {
+	hashtag: () => {
 		const mark = P.str('#');
-		const hashTagChar = P.seq([
+		const hashTagChar = P.seq(
 			P.notMatch(P.alt([P.regexp(/[ \u3000\t.,!?'"#:/[\]【】()「」（）<>]/), space, newLine])),
 			P.char,
-		], 1);
-		const innerItem: P.Parser<any> = P.lazy(() => P.alt([
-			P.seq([
+		).select(1);
+		const innerItem: P.Parser<unknown> = P.lazy(() => P.alt([
+			P.seq(
 				P.str('('), nest(innerItem, hashTagChar).many(0), P.str(')'),
-			]),
-			P.seq([
+			),
+			P.seq(
 				P.str('['), nest(innerItem, hashTagChar).many(0), P.str(']'),
-			]),
-			P.seq([
+			),
+			P.seq(
 				P.str('「'), nest(innerItem, hashTagChar).many(0), P.str('」'),
-			]),
-			P.seq([
+			),
+			P.seq(
 				P.str('（'), nest(innerItem, hashTagChar).many(0), P.str('）'),
-			]),
+			),
 			hashTagChar,
 		]));
-		const parser = P.seq([
+		const parser = P.seq( 
 			notLinkLabel,
 			mark,
 			innerItem.many(1).text(),
-		], 2);
+		).select(2);
 		return new P.Parser((input, index, state) => {
 			const result = parser.handler(input, index, state);
 			if (!result.success) {
@@ -622,16 +659,16 @@ export const language = P.createLanguage({
 		});
 	},
 
-	emojiCode: r => {
+	emojiCode: () => {
 		const side = P.notMatch(P.regexp(/[a-z0-9]/i));
 		const mark = P.str(':');
-		return P.seq([
+		return P.seq( 
 			P.alt([P.lineBegin, side]),
 			mark,
 			P.regexp(/[a-z0-9_+-]+/i),
 			mark,
 			P.alt([P.lineEnd, side]),
-		], 2).map(name => M.EMOJI_CODE(name as string));
+		).select(2).map(name => M.EMOJI_CODE(name));
 	},
 
 	link: r => {
@@ -642,41 +679,49 @@ export const language = P.createLanguage({
 			return result;
 		});
 		const closeLabel = P.str(']');
-		return P.seq([
+		const parser = P.seq(
 			notLinkLabel,
 			P.alt([P.str('?['), P.str('[')]),
-			P.seq([
+			P.seq(
 				P.notMatch(P.alt([closeLabel, newLine])),
 				nest(labelInline),
-			], 1).many(1),
+			).select(1).many(1),
 			closeLabel,
 			P.str('('),
 			P.alt([r.urlAlt, r.url]),
 			P.str(')'),
-		]).map(result => {
-			const silent = (result[1] === '?[');
-			const label = result[2];
-			const url: M.MfmUrl = result[5];
-			return M.LINK(silent, url.props.url, mergeText(label));
+		);
+		return new P.Parser<M.MfmLink>((input, index, state) => {
+			const result = parser.handler(input, index, state);
+			if (!result.success) {
+				return P.failure();
+			}
+
+			const [, prefix, label,,, url] = result.value;
+
+			const silent = (prefix === '?[');
+			if (typeof url === 'string') return P.failure();
+			
+			return P.success(result.index, M.LINK(silent, url.props.url, mergeText(label)));
 		});
 	},
 
-	url: r => {
+	url: () => {
 		const urlChar = P.regexp(/[.,a-z0-9_/:%#@$&?!~=+-]/i);
-		const innerItem: P.Parser<any> = P.lazy(() => P.alt([
-			P.seq([
+		const innerItem: P.Parser<unknown> = P.lazy(() => P.alt([
+			P.seq(
 				P.str('('), nest(innerItem, urlChar).many(0), P.str(')'),
-			]),
-			P.seq([
+			),
+			P.seq(
 				P.str('['), nest(innerItem, urlChar).many(0), P.str(']'),
-			]),
+			),
 			urlChar,
 		]));
-		const parser = P.seq([
+		const parser = P.seq(
 			notLinkLabel,
 			P.regexp(/https?:\/\//),
 			innerItem.many(1).text(),
-		]);
+		);
 		return new P.Parser<M.MfmUrl | string>((input, index, state) => {
 			let result;
 			result = parser.handler(input, index, state);
@@ -700,16 +745,16 @@ export const language = P.createLanguage({
 		});
 	},
 
-	urlAlt: r => {
+	urlAlt: () => {
 		const open = P.str('<');
 		const close = P.str('>');
-		const parser = P.seq([
+		const parser = P.seq(
 			notLinkLabel,
 			open,
 			P.regexp(/https?:\/\//),
-			P.seq([P.notMatch(P.alt([close, space])), P.char], 1).many(1),
+			P.seq(P.notMatch(P.alt([close, space])), P.char).select(1).many(1),
 			close,
-		]).text();
+		).text();
 		return new P.Parser((input, index, state) => {
 			const result = parser.handler(input, index, state);
 			if (!result.success) {
@@ -720,30 +765,30 @@ export const language = P.createLanguage({
 		});
 	},
 
-	search: r => {
+	search: () => {
 		const button = P.alt([
 			P.regexp(/\[(検索|search)\]/i),
 			P.regexp(/(検索|search)/i),
 		]);
-		return P.seq([
+		return P.seq(
 			newLine.option(),
 			P.lineBegin,
-			P.seq([
+			P.seq(
 				P.notMatch(P.alt([
 					newLine,
-					P.seq([space, button, P.lineEnd]),
+					P.seq(space, button, P.lineEnd),
 				])),
 				P.char,
-			], 1).many(1),
+			).select(1).many(1),
 			space,
 			button,
 			P.lineEnd,
 			newLine.option(),
-		]).map(result => {
+		).map(result => {
 			const query = result[2].join('');
 			return M.SEARCH(query, `${query}${result[3]}${result[4]}`);
 		});
 	},
 
-	text: r => P.char,
+	text: () => P.char,
 });
diff --git a/src/internal/util.ts b/src/internal/util.ts
index 3f0cffc..4c3935b 100644
--- a/src/internal/util.ts
+++ b/src/internal/util.ts
@@ -1,13 +1,15 @@
 import { isMfmBlock, MfmInline, MfmNode, MfmText, TEXT } from '../node';
 
-export function mergeText<T extends MfmNode>(nodes: ((T extends MfmInline ? MfmInline : MfmNode) | string)[]): (T | MfmText)[] {
+type ArrayRecursive<T> = T | Array<ArrayRecursive<T>>;
+
+export function mergeText<T extends MfmNode>(nodes: ArrayRecursive<((T extends MfmInline ? MfmInline : MfmNode) | string)>[]): (T | MfmText)[] {
 	const dest: (T | MfmText)[] = [];
 	const storedChars: string[] = [];
 
 	/**
 	 * Generate a text node from the stored chars, And push it.
 	*/
-	function generateText() {
+	function generateText(): void {
 		if (storedChars.length > 0) {
 			dest.push(TEXT(storedChars.join('')));
 			storedChars.length = 0;
@@ -136,7 +138,7 @@ export function stringifyTree(nodes: MfmNode[]): string {
 		// block -> inline  : Yes
 		// block -> block   : Yes
 
-		let pushLf: boolean = true;
+		let pushLf = true;
 		if (isMfmBlock(node)) {
 			if (state === stringifyState.none) {
 				pushLf = false;
@@ -159,7 +161,7 @@ export function stringifyTree(nodes: MfmNode[]): string {
 	return dest.map(n => stringifyNode(n)).join('');
 }
 
-export function inspectOne(node: MfmNode, action: (node: MfmNode) => void) {
+export function inspectOne(node: MfmNode, action: (node: MfmNode) => void): void {
 	action(node);
 	if (node.children != null) {
 		for (const child of node.children) {


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e072e19..598042b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,7 +12,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [16.5.0]
+        node-version: [16.10.0]
 
     steps:
       - name: Checkout
diff --git a/test-d/index.ts b/test-d/index.ts
index 388aca3..423b5a3 100644
--- a/test-d/index.ts
+++ b/test-d/index.ts
@@ -5,6 +5,7 @@
 
 import { expectType } from 'tsd';
 import { NodeType, MfmUrl } from '../src';
+import * as P from '../src/internal/core';
 
 describe('#NodeType', () => {
 	test('returns node that has sprcified type', () => {
@@ -12,3 +13,18 @@ describe('#NodeType', () => {
 		expectType<MfmUrl>(x);
 	});
 });
+
+describe('parser internals', () => {
+	test('seq', () => {
+		const first = null as unknown as P.Parser<'first'>;
+		const second = null as unknown as P.Parser<'second'>;
+		const third = null as unknown as P.Parser<'third' | 'third-second'>;
+		expectType<P.Parser<['first', 'second', 'third' | 'third-second']>>(P.seq(first, second, third));
+	});
+	test('alt', () => {
+		const first = null as unknown as P.Parser<'first'>;
+		const second = null as unknown as P.Parser<'second'>;
+		const third = null as unknown as P.Parser<'third' | 'third-second'>;
+		expectType<P.Parser<'first' | 'second' | 'third' | 'third-second'>>(P.alt([first, second, third]));
+	});
+});
diff --git a/test/parser.ts b/test/parser.ts
index 50ae700..852699c 100644
--- a/test/parser.ts
+++ b/test/parser.ts
@@ -1192,6 +1192,14 @@ hoge`;
 			];
 			assert.deepStrictEqual(mfm.parse(input), output);
 		});
+
+		test('bad url in url part', () => {
+			const input = "[test](http://..)";
+			const output = [
+				TEXT("[test](http://..)")
+			];
+			assert.deepStrictEqual(mfm.parse(input), output);
+		})
 	});
 
 	describe('fn', () => {