diff --git a/src/index.ts b/src/index.ts
index 0d857e3aebf57795aff09dd77021e42ed2f00566..df0424e6ba68465509c4a30ae94c45d2eec107b4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,5 @@
 import peg from 'pegjs';
-import { MfmNode } from './mfm-node';
+import { MfmNode } from './util';
 const parser: peg.Parser = require('./parser');
 
 export function parse(input: string): MfmNode[] {
diff --git a/src/parser.pegjs b/src/parser.pegjs
index c9c8553c6882f9b25853cbfe05f5a3847eee4cab..9ae92fb35f1c8a4ef70b14ef60bccb18fa2a89f3 100644
--- a/src/parser.pegjs
+++ b/src/parser.pegjs
@@ -1,8 +1,10 @@
 {
 	const {
 		createNode,
-		mergeText
-	} = require('./mfm-node');
+		mergeText,
+		setConsumeCount,
+		consumeDynamically
+	} = require('./util');
 
 	function applyParser(input, startRule) {
 		let parseFunc = peg$parse;
@@ -12,8 +14,6 @@
 	// emoji
 
 	const emojiRegex = require('twemoji-parser/dist/lib/regex').default;
-
-	let emojiLoop = 0;
 	const anchoredEmojiRegex = RegExp(`^(?:${emojiRegex.source})`);
 
 	/**
@@ -26,23 +26,12 @@
 
 		const result = anchoredEmojiRegex.exec(src);
 		if (result != null) {
-			emojiLoop = result[0].length; // length(utf-16 byte length) of emoji sequence.
+			setConsumeCount(result[0].length); // length(utf-16 byte length) of emoji sequence.
 			return true;
 		}
 
 		return false;
 	}
-
-	/**
-	 * this is the process when the input is consumed as emojis.
-	*/
-	function forwardUnicodeEmoji() {
-		const forwarding = (emojiLoop > 0);
-		if (forwarding) {
-			emojiLoop--;
-		}
-		return forwarding;
-	}
 }
 
 //
@@ -167,6 +156,7 @@ inline
 	/ inlineCode
 	/ mathInline
 	/ hashtag
+	/ url
 	/ text
 
 // inline: emoji
@@ -185,7 +175,7 @@ emojiName
 
 // NOTE: if the text matches one of the emojis, it will count the length of the emoji sequence and consume it.
 unicodeEmoji
-	= &{ return matchUnicodeEmoji(); } (&{ return forwardUnicodeEmoji(); } .)+
+	= &{ return matchUnicodeEmoji(); } (&{ return consumeDynamically(); } .)+
 {
 	return createNode('emoji', { emoji: text() });
 }
@@ -222,14 +212,6 @@ small
 	return createNode('small', { }, mergeText(content));
 }
 
-// inline: strike
-
-strike
-	= "~~" content:(!("~" / LF) i:inline { return i; })+ "~~"
-{
-	return createNode('strike', { }, mergeText(content));
-}
-
 // inline: italic
 
 italic
@@ -248,6 +230,14 @@ italic
 	return createNode('italic', { }, parsedContent);
 }
 
+// inline: strike
+
+strike
+	= "~~" content:(!("~" / LF) i:inline { return i; })+ "~~"
+{
+	return createNode('strike', { }, mergeText(content));
+}
+
 // inline: inlineCode
 
 inlineCode
@@ -287,6 +277,36 @@ hashtagBracketPair
 hashtagChar
 	= ![  \t.,!?'"#:\/\[\]【】()「」] CHAR
 
+// inline: URL
+
+url
+	= "<" url:urlFormat ">"
+{
+	return createNode('url', { url: url });
+}
+	/ url:urlFormat
+{
+	return createNode('url', { url: url });
+}
+
+urlFormat
+	= "http" "s"? "://" urlContent
+{
+	return text();
+}
+
+urlContent
+	= urlContentPart+
+
+urlContentPart
+	= urlBracketPair
+	/ [.,] &urlContentPart // last char is neither "." nor ",".
+	/ [a-z0-9/:%#@$&?!~=+-]i
+
+urlBracketPair
+	= "(" urlContentPart* ")"
+	/ "[" urlContentPart* "]"
+
 // inline: text
 
 text
diff --git a/src/mfm-node.ts b/src/util.ts
similarity index 73%
rename from src/mfm-node.ts
rename to src/util.ts
index 2069a282f29b552c663f29e222e232a4d082ac2b..f61a6e496b668bd3ef6cae29db12b6968ee86517 100644
--- a/src/mfm-node.ts
+++ b/src/util.ts
@@ -61,3 +61,39 @@ export function mergeText(trees: MfmNode[], recursive?: boolean): MfmNode[] {
 		return createNode(tree.type, tree.props, recursive ? mergeText(tree.children) : tree.children);
 	});
 }
+
+//
+// dynamic consuming
+//
+
+/*
+	1. If you want to consume 3 chars, call the setConsumeCount.
+	```
+	setConsumeCount(3);
+	```
+
+	2. And the rule to consume the input is as below:
+	```
+	rule = (&{ return consumeDynamically(); } .)+
+	```
+*/
+
+let consumeCount = 0;
+
+/**
+ * set the length of dynamic consuming.
+*/
+export function setConsumeCount(count: number) {
+	consumeCount = count;
+}
+
+/**
+ * consume the input and returns matching result.
+*/
+export function consumeDynamically() {
+	const matched = (consumeCount > 0);
+	if (matched) {
+		consumeCount--;
+	}
+	return matched;
+}
diff --git a/test/inline.ts b/test/inline.ts
index 3351a4ab47e7aa8a23bc70f537fd7a4692273380..06b9acecd883c09412b1204b87e450f97140f25c 100644
--- a/test/inline.ts
+++ b/test/inline.ts
@@ -2,7 +2,7 @@ import assert from 'assert';
 import { parse, parsePlain } from '../built/index';
 import { createNode } from '../built/mfm-node';
 import {
-	TEXT, EMOJI, UNI_EMOJI, HASHTAG
+	TEXT, EMOJI, UNI_EMOJI, HASHTAG, N_URL
 } from './node';
 
 describe('text', () => {
@@ -35,3 +35,15 @@ describe('hashtag', () => {
 		assert.deepStrictEqual(parse(input), output);
 	});
 });
+
+describe('url', () => {
+	it('basic', () => {
+		const input = 'official instance: https://misskey.io/@ai.';
+		const output = [
+			TEXT('official instance: '),
+			N_URL('https://misskey.io/@ai'),
+			TEXT('.')
+		];
+		assert.deepStrictEqual(parse(input), output);
+	});
+});
diff --git a/test/node.ts b/test/node.ts
index 2b66e34143894efc762e81bfaeda3ae934d733c6..2e04d2a3bc382f6c861789811a094199fa278f78 100644
--- a/test/node.ts
+++ b/test/node.ts
@@ -4,3 +4,4 @@ export const TEXT = (value: string) => createNode('text', { text: value });
 export const EMOJI = (name: string) => createNode('emoji', { name: name });
 export const UNI_EMOJI = (value: string) => createNode('emoji', { emoji: value });
 export const HASHTAG = (value: string) => createNode('hashtag', { hashtag: value });
+export const N_URL = (value: string) => createNode('url', { url: value });