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 });