diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2d1a0..a645233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,20 @@ --> +## 0.23.0 (unreleased) + +### Features +- Add Plain syntax (#101) + +### Improvements +- The parser is now implemented in TypeScript! 🎉 (#92) +- Disable all syntax when nesting limited (#90) + +### Changes +- Rename existing plain series (#113): + - parsePlain -> parseSimple + - MfmPlainNode -> MfmSimpleNode + ## 0.22.1 npm: https://www.npmjs.com/package/mfm-js/v/0.22.1 diff --git a/README.md b/README.md index 0c50462..afdccf7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # mfm.js -An MFM parser implementation with PEG.js. +An MFM parser implementation with TypeScript. [Try it out!](https://runkit.com/npm/mfm-js) [![Test](https://github.com/misskey-dev/mfm.js/actions/workflows/test.yml/badge.svg)](https://github.com/misskey-dev/mfm.js/actions/workflows/test.yml) diff --git a/docs/syntax.md b/docs/syntax.md index 11b4856..529bfe2 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -295,7 +295,7 @@ _italic_ 構文2,3のみ: ※1つ目の`*`と`_`を開始記号と呼ぶ。 - 内容には`[a-z0-9 \t]i`にマッチする文字が使用できる。 -- 開始記号の前の文字が(無い、改行、半角スペース、[a-zA-Z0-9]に一致しない)のいずれかの時にイタリック文字として判定される。 +- 開始記号の前の文字が`[a-z0-9]i`に一致しない時にイタリック文字として判定される。 ## ノード ```js @@ -402,7 +402,7 @@ _italic_ ``` ## 詳細 -- 最初の`@`の前の文字が(改行、スペース、無し、[a-zA-Z0-9]に一致しない)のいずれかの場合にメンションとして認識する。 +- 最初の`@`の前の文字が`[a-z0-9]i`に一致しない場合にメンションとして認識する。 ### ユーザ名 - 1文字以上。 @@ -451,7 +451,7 @@ _italic_ - 内容には半角スペース、全角スペース、改行、タブ文字を含めることができない。 - 内容には`.` `,` `!` `?` `'` `"` `#` `:` `/` `【` `】` `<` `>` `【` `】` `(` `)` `「` `」` `(` `)` を含めることができない。 - 括弧は対になっている時のみ内容に含めることができる。対象: `()` `[]` `「」` `()` -- `#`の前の文字が(改行、スペース、無し、[a-zA-Z0-9]に一致しない)のいずれかの場合にハッシュタグとして認識する。 +- `#`の前の文字が`[a-z0-9]i`に一致しない場合にハッシュタグとして認識する。 - 内容が数字のみの場合はハッシュタグとして認識しない。 ## ノード diff --git a/package-lock.json b/package-lock.json index b3580d3..d678f67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,8 @@ "@types/node": "18.0.3", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", - "copyfiles": "^2.4.1", "eslint": "^8.19.0", "jest": "^28.1.2", - "peggy": "1.2.0", "ts-jest": "^28.0.5", "ts-node": "10.8.2", "tsd": "^0.22.0", @@ -2116,31 +2114,6 @@ "safe-buffer": "~5.1.1" } }, - "node_modules/copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, - "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3151,12 +3124,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4178,18 +4145,6 @@ "node": ">= 6" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4214,16 +4169,6 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, - "node_modules/noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha1-2o69nzr51nYJGbJ9nNyAkqczKFk=", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - } - }, "node_modules/normalize-package-data": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz", @@ -4409,18 +4354,6 @@ "node": ">=8" } }, - "node_modules/peggy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/peggy/-/peggy-1.2.0.tgz", - "integrity": "sha512-PQ+NKpAobImfMprYQtc4Egmyi29bidRGEX0kKjCU5uuW09s0Cthwqhfy7mLkwcB4VcgacE5L/ZjruD/kOPCUUw==", - "dev": true, - "bin": { - "peggy": "bin/peggy" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4511,12 +4444,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -4651,18 +4578,6 @@ "node": ">=8" } }, - "node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4938,12 +4853,6 @@ "node": ">=8" } }, - "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, "node_modules/string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -5101,46 +5010,6 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", @@ -5375,15 +5244,6 @@ "node": ">= 4.0.0" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz", @@ -5419,12 +5279,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -5539,15 +5393,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", @@ -5563,24 +5408,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yargs-parser": { "version": "20.2.4", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", @@ -7245,27 +7072,6 @@ "safe-buffer": "~5.1.1" } }, - "copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, - "requires": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -8014,12 +7820,6 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8814,12 +8614,6 @@ "kind-of": "^6.0.3" } }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8844,16 +8638,6 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, - "noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha1-2o69nzr51nYJGbJ9nNyAkqczKFk=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - } - }, "normalize-package-data": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz", @@ -8988,12 +8772,6 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "peggy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/peggy/-/peggy-1.2.0.tgz", - "integrity": "sha512-PQ+NKpAobImfMprYQtc4Egmyi29bidRGEX0kKjCU5uuW09s0Cthwqhfy7mLkwcB4VcgacE5L/ZjruD/kOPCUUw==", - "dev": true - }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -9056,12 +8834,6 @@ } } }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9159,18 +8931,6 @@ } } }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -9375,12 +9135,6 @@ } } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -9496,48 +9250,6 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", @@ -9683,12 +9395,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, - "untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true - }, "update-browserslist-db": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz", @@ -9708,12 +9414,6 @@ "punycode": "^2.1.0" } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -9804,12 +9504,6 @@ "signal-exit": "^3.0.7" } }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, "y18n": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", @@ -9822,21 +9516,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, "yargs-parser": { "version": "20.2.4", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", diff --git a/package.json b/package.json index 2955806..f856d6e 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,11 @@ { "name": "mfm-js", - "version": "0.22.1", - "description": "An MFM parser implementation with PEG.js", + "version": "0.23.0-canary.1", + "description": "An MFM parser implementation with TypeScript", "main": "./built/index.js", "types": "./built/index.d.ts", "scripts": { - "build": "npm run tsc && npm run peg", - "build-debug": "npm run tsc && npm run peg-debug", - "peg": "peggy --cache -o src/internal/parser.js --allowed-start-rules fullParser,simpleParser src/internal/parser.pegjs && npm run peg-copy", - "peg-debug": "peggy --cache -o src/internal/parser.js --allowed-start-rules fullParser,inlineParser,simpleParser --trace src/internal/parser.pegjs && npm run peg-copy", - "peg-copy": "copyfiles -f src/internal/parser.js built/internal/", + "build": "npm run tsc", "tsc": "tsc", "tsd": "tsd", "parse": "node ./built/cli/parse", @@ -32,10 +28,8 @@ "@types/node": "18.0.3", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", - "copyfiles": "^2.4.1", "eslint": "^8.19.0", "jest": "^28.1.2", - "peggy": "1.2.0", "ts-jest": "^28.0.5", "ts-node": "10.8.2", "tsd": "^0.22.0", diff --git a/src/@types/twemoji.d.ts b/src/@types/twemoji.d.ts new file mode 100644 index 0000000..4c2c311 --- /dev/null +++ b/src/@types/twemoji.d.ts @@ -0,0 +1,4 @@ +declare module 'twemoji-parser/dist/lib/regex' { + const regex: RegExp; + export default regex; +} diff --git a/src/api.ts b/src/api.ts index 96e0302..f3194a8 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,16 +1,12 @@ -import peg from 'peggy'; +import { fullParser, simpleParser } from './internal'; +import { inspectOne, stringifyNode, stringifyTree } from './internal/util'; import { MfmNode, MfmSimpleNode } from './node'; -import { stringifyNode, stringifyTree, inspectOne } from './internal/util'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const parser: peg.Parser = require('./internal/parser'); /** * Generates a MfmNode tree from the MFM string. */ export function parse(input: string, opts: Partial<{ fnNameList: string[]; nestLimit: number; }> = {}): MfmNode[] { - const nodes = parser.parse(input, { - startRule: 'fullParser', + const nodes = fullParser(input, { fnNameList: opts.fnNameList, nestLimit: opts.nestLimit, }); @@ -21,7 +17,7 @@ export function parse(input: string, opts: Partial<{ fnNameList: string[]; nestL * Generates a MfmSimpleNode tree from the MFM string. */ export function parseSimple(input: string): MfmSimpleNode[] { - const nodes = parser.parse(input, { startRule: 'simpleParser' }); + const nodes = simpleParser(input); return nodes; } diff --git a/src/internal/core/index.ts b/src/internal/core/index.ts new file mode 100644 index 0000000..2241d50 --- /dev/null +++ b/src/internal/core/index.ts @@ -0,0 +1,249 @@ +// +// Parsimmon-like stateful parser combinators +// + +export type Success = { + success: true; + value: T; + index: number; +}; + +export type Failure = { success: false }; + +export type Result = Success | Failure; + +export type ParserHandler = (input: string, index: number, state: any) => Result + +export function success(index: number, value: T): Success { + return { + success: true, + value: value, + index: index, + }; +} + +export function failure(): Failure { + return { success: false }; +} + +export class Parser { + public name?: string; + public handler: ParserHandler; + + constructor(handler: ParserHandler, name?: string) { + this.handler = (input, index, state) => { + 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.name = name; + } + + map(fn: (value: T) => U): Parser { + return new Parser((input, index, state) => { + const result = this.handler(input, index, state); + if (!result.success) { + return result; + } + return success(result.index, fn(result.value)); + }); + } + + text(): Parser { + return new Parser((input, index, state) => { + const result = this.handler(input, index, state); + if (!result.success) { + return result; + } + const text = input.slice(index, result.index); + return success(result.index, text); + }); + } + + many(min: number): Parser { + return new Parser((input, index, state) => { + let result; + let latestIndex = index; + const accum: T[] = []; + while (latestIndex < input.length) { + result = this.handler(input, latestIndex, state); + if (!result.success) { + break; + } + latestIndex = result.index; + accum.push(result.value); + } + if (accum.length < min) { + return failure(); + } + return success(latestIndex, accum); + }); + } + + sep(separator: Parser, min: number): Parser { + if (min < 1) { + throw new Error('"min" must be a value greater than or equal to 1.'); + } + return seq([ + this, + seq([ + separator, + this, + ], 1).many(min - 1), + ]).map(result => [result[0], ...result[1]]); + } + + option(): Parser { + return alt([ + this, + succeeded(null), + ]); + } +} + +export function str(value: T): Parser { + return new Parser((input, index, _state) => { + if ((input.length - index) < value.length) { + return failure(); + } + if (input.substr(index, value.length) !== value) { + return failure(); + } + return success(index + value.length, value); + }); +} + +export function regexp(pattern: T): Parser { + const re = RegExp(`^(?:${pattern.source})`, pattern.flags); + return new Parser((input, index, _state) => { + const text = input.slice(index); + const result = re.exec(text); + if (result == null) { + return failure(); + } + return success(index + result[0].length, result[0]); + }); +} + +export function seq(parsers: Parser[], select?: number): Parser { + return new Parser((input, index, state) => { + let result; + let latestIndex = index; + const accum = []; + for (let i = 0; i < parsers.length; i++) { + result = parsers[i].handler(input, latestIndex, state); + if (!result.success) { + return result; + } + latestIndex = result.index; + accum.push(result.value); + } + return success(latestIndex, (select != null ? accum[select] : accum)); + }); +} + +export function alt(parsers: Parser[]): Parser { + return new Parser((input, index, state) => { + let result; + for (let i = 0; i < parsers.length; i++) { + result = parsers[i].handler(input, index, state); + if (result.success) { + return result; + } + } + return failure(); + }); +} + +function succeeded(value: T): Parser { + return new Parser((_input, index, _state) => { + return success(index, value); + }); +} + +export function notMatch(parser: Parser): Parser { + return new Parser((input, index, state) => { + const result = parser.handler(input, index, state); + return !result.success + ? success(index, null) + : failure(); + }); +} + +export const cr = str('\r'); +export const lf = str('\n'); +export const crlf = str('\r\n'); +export const newline = alt([crlf, cr, lf]); + +export const char = new Parser((input, index, _state) => { + if ((input.length - index) < 1) { + return failure(); + } + const value = input.charAt(index); + return success(index + 1, value); +}); + +export const lineBegin = new Parser((input, index, state) => { + if (index === 0) { + return success(index, null); + } + if (cr.handler(input, index - 1, state).success) { + return success(index, null); + } + if (lf.handler(input, index - 1, state).success) { + return success(index, null); + } + return failure(); +}); + +export const lineEnd = new Parser((input, index, state) => { + if (index === input.length) { + return success(index, null); + } + if (cr.handler(input, index, state).success) { + return success(index, null); + } + if (lf.handler(input, index, state).success) { + return success(index, null); + } + return failure(); +}); + +export function lazy(fn: () => Parser): Parser { + const parser: Parser = new Parser((input, index, state) => { + parser.handler = fn().handler; + return parser.handler(input, index, state); + }); + return parser; +} + +//type Syntax = (rules: Record>) => Parser; +//type SyntaxReturn = T extends (rules: Record>) => infer R ? R : never; +//export function createLanguage2>>(syntaxes: T): { [K in keyof T]: SyntaxReturn } { + +// TODO: 関数の型宣言をいい感じにしたい +export function createLanguage(syntaxes: { [K in keyof T]: (r: Record>) => T[K] }): T { + const rules: Record> = {}; + for (const key of Object.keys(syntaxes)) { + rules[key] = lazy(() => { + const parser = (syntaxes as any)[key](rules); + if (parser == null) { + throw new Error('syntax must return a parser.'); + } + parser.name = key; + return parser; + }); + } + return rules as any; +} diff --git a/src/internal/index.ts b/src/internal/index.ts new file mode 100644 index 0000000..86eb7d8 --- /dev/null +++ b/src/internal/index.ts @@ -0,0 +1,25 @@ +import * as M from '..'; +import { language } from './parser'; +import { mergeText } from './util'; +import * as P from './core'; + +export type FullParserOpts = { + fnNameList?: string[]; + nestLimit?: number; +}; + +export function fullParser(input: string, opts: FullParserOpts): M.MfmNode[] { + const result = language.fullParser.handler(input, 0, { + nestLimit: (opts.nestLimit != null) ? opts.nestLimit : 20, + fnNameList: opts.fnNameList, + depth: 0, + linkLabel: false, + trace: false, + }) as P.Success; + return mergeText(result.value); +} + +export function simpleParser(input: string): M.MfmSimpleNode[] { + const result = language.simpleParser.handler(input, 0, { }) as P.Success; + return mergeText(result.value); +} diff --git a/src/internal/parser.pegjs b/src/internal/parser.pegjs deleted file mode 100644 index 6a955f5..0000000 --- a/src/internal/parser.pegjs +++ /dev/null @@ -1,609 +0,0 @@ -{ - const { - // block - QUOTE, - SEARCH, - CODE_BLOCK, - MATH_BLOCK, - CENTER, - - // inline - UNI_EMOJI, - EMOJI_CODE, - BOLD, - SMALL, - ITALIC, - STRIKE, - INLINE_CODE, - MATH_INLINE, - MENTION, - HASHTAG, - N_URL, - LINK, - FN, - PLAIN, - TEXT - } = require('../node'); - - const { - mergeText, - setConsumeCount, - consumeDynamically - } = require('./util'); - - function applyParser(input, startRule, opts) { - const parseFunc = peg$parse; - const parseOpts = { - fnNameList: options.fnNameList, - nestLimit: (nestLimit - depth), - ...(opts || {}), - }; - if (startRule) parseOpts.startRule = startRule; - return parseFunc(input, parseOpts); - } - - // emoji - - const emojiRegex = require('twemoji-parser/dist/lib/regex').default; - const anchoredEmojiRegex = RegExp(`^(?:${emojiRegex.source})`); - - /** - * check if the input matches the emoji regexp. - * if they match, set the byte length of the emoji. - */ - function matchUnicodeEmoji() { - const offset = location().start.offset; - const src = input.substr(offset); - - const result = anchoredEmojiRegex.exec(src); - if (result != null) { - setConsumeCount(result[0].length); // length(utf-16 byte length) of emoji sequence. - return true; - } - - return false; - } - - function ensureFnName(name) { - if (!options.fnNameList) return true; - if (!Array.isArray(options.fnNameList)) { - error("options.fnNameList must be an array."); - } - return options.fnNameList.includes(name); - } - - // nesting control - - const nestLimit = (options.nestLimit != null ? options.nestLimit : 20); - let depth = 0; - function enterNest() { - if (depth + 1 > nestLimit) { - return false; - } - depth++; - return true; - } - - function leaveNest() { - depth--; - return true; - } - - function fallbackNest() { - depth--; - return false; - } -} - -// -// parsers -// - -fullParser - = nodes:(&. @full)* { return mergeText(nodes); } - -simpleParser - = nodes:(&. @simple)* { return mergeText(nodes); } - -// -// syntax list -// - -full - = quote // block - / codeBlock // block - / mathBlock // block - / center // block - / emojiCode - / unicodeEmoji - / big - / bold - / small - / italic - / strike - / inlineCode - / mathInline - / mention - / hashtag - / url - / fn - / plain - / link - / search // block - / inlineText - -inline - = emojiCode - / unicodeEmoji - / big - / bold - / small - / italic - / strike - / inlineCode - / mathInline - / mention - / hashtag - / url - / fn - / plain - / link - / inlineText - -L_inline - = emojiCode - / unicodeEmoji - / L_big - / L_bold - / L_small - / L_italic - / L_strike - / inlineCode - / mathInline - / L_fn - / plain - / L_inlineText - -simple - = emojiCode - / unicodeEmoji - / simpleText - -// -// block rules -// - -// block: quote - -quote - = &(BEGIN ">") &{ return (depth + 1 <= nestLimit); } @quoteInner LF? - -quoteInner - = head:(quoteLine / quoteEmptyLine) tails:(quoteLine / quoteEmptyLine)+ -{ - depth++; - const children = applyParser([head, ...tails].join('\n'), 'fullParser'); - depth--; - return QUOTE(children); -} - / line:quoteLine -{ - depth++; - const children = applyParser(line, 'fullParser'); - depth--; - return QUOTE(children); -} - -quoteLine - = BEGIN ">" _? @$CHAR+ END - -quoteEmptyLine - = BEGIN ">" _? END -{ - return ''; -} - -// block: search - -search - = BEGIN q:searchQuery sp:_ key:searchKey END -{ - return SEARCH(q, `${ q }${ sp }${ key }`); -} - -searchQuery - = (!(_ searchKey END) CHAR)+ { return text(); } - -searchKey - = "[" ("検索" / "Search"i) "]" { return text(); } - / "検索" - / "Search"i - -// block: codeBlock - -codeBlock - = BEGIN "```" lang:$(CHAR*) LF code:codeBlockContent LF "```" END -{ - lang = lang.trim(); - return CODE_BLOCK(code, lang.length > 0 ? lang : null); -} - -codeBlockContent - = (!(LF "```" END) .)+ -{ return text(); } - -// block: mathBlock - -mathBlock - = BEGIN "\\[" LF? formula:mathBlockLines LF? "\\]" END -{ - return MATH_BLOCK(formula.trim()); -} - -mathBlockLines - = mathBlockLine (LF mathBlockLine)* -{ return text(); } - -mathBlockLine - = (!"\\]" CHAR)+ - -// block: center - -center - = BEGIN "
" LF? content:(!(LF? "
" END) @inline)* LF? "" END -{ - return CENTER(mergeText(content)); -} - -// -// inline rules -// - -// inline: emoji code - -emojiCode - = ":" name:$[a-z0-9_+-]i+ ":" -{ - return EMOJI_CODE(name); -} - -// inline: unicode emoji - -// 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 consumeDynamically(); } .)+ -{ - return UNI_EMOJI(text()); -} - -// inline: big - -big - = "***" content:bigContent "***" -{ - return FN('tada', { }, mergeText(content)); -} - -bigContent - = &{ return enterNest(); } @(@(!"***" @inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -L_big - = "***" content:L_bigContent "***" -{ - return FN('tada', { }, mergeText(content)); -} - -L_bigContent - = &{ return enterNest(); } @(@(!"***" @L_inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -// inline: bold - -bold - = "**" content:boldContent "**" -{ - return BOLD(mergeText(content)); -} - / "" content:boldTagContent "" -{ - return BOLD(mergeText(content)); -} - / "__" content:$(!"__" @([a-z0-9]i / _))+ "__" -{ - return BOLD([TEXT(content)]); -} - -boldContent - = &{ return enterNest(); } @(@(!"**" @inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -boldTagContent - = &{ return enterNest(); } @(@(!"" @inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -L_bold - = "**" content:L_boldContent "**" -{ - return BOLD(mergeText(content)); -} - / "" content:L_boldTagContent "" -{ - return BOLD(mergeText(content)); -} - / "__" content:$(!"__" @([a-z0-9]i / _))+ "__" -{ - return BOLD([TEXT(content)]); -} - -L_boldContent - = &{ return enterNest(); } @(@(!"**" @L_inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -L_boldTagContent - = &{ return enterNest(); } @(@(!"" @L_inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -// inline: small - -small - = "" content:smallContent "" -{ - return SMALL(mergeText(content)); -} - -smallContent - = &{ return enterNest(); } @(@(!"" @inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -L_small - = "" content:L_smallContent "" -{ - return SMALL(mergeText(content)); -} - -L_smallContent - = &{ return enterNest(); } @(@(!"" @L_inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -// inline: italic - -italic - = "" content:italicContent "" -{ - return ITALIC(mergeText(content)); -} - / italicAlt - -L_italic - = "" content:L_italicContent "" -{ - return ITALIC(mergeText(content)); -} - / italicAlt - -italicAlt - = "*" content:$([a-z0-9]i / _)+ "*" &(EOF / LF / _ / ![a-z0-9]i) -{ - return ITALIC([TEXT(content)]); -} - / "_" content:$([a-z0-9]i / _)+ "_" &(EOF / LF / _ / ![a-z0-9]i) -{ - return ITALIC([TEXT(content)]); -} - -italicContent - = &{ return enterNest(); } @(@(!"" @inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -L_italicContent - = &{ return enterNest(); } @(@(!"" @L_inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -// inline: strike - -strike - = "~~" content:strikeContent "~~" -{ - return STRIKE(mergeText(content)); -} - / "" content:strikeTagContent "" -{ - return STRIKE(mergeText(content)); -} - -strikeContent - = &{ return enterNest(); } @(@(!("~" / LF) @inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -strikeTagContent - = &{ return enterNest(); } @(@(!"" @inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -L_strike - = "~~" content:L_strikeContent "~~" -{ - return STRIKE(mergeText(content)); -} - / "" content:L_strikeTagContent "" -{ - return STRIKE(mergeText(content)); -} - -L_strikeContent - = &{ return enterNest(); } @(@(!("~" / LF) @L_inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -L_strikeTagContent - = &{ return enterNest(); } @(@(!"" @L_inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -// inline: inlineCode - -inlineCode - = "`" content:$(![`´] CHAR)+ "`" -{ - return INLINE_CODE(content); -} - -// inline: mathInline - -mathInline - = "\\(" content:$(!"\\)" CHAR)+ "\\)" -{ - return MATH_INLINE(content); -} - -// inline: mention - -mention - = "@" name:mentionName host:("@" @mentionHost)? -{ - return MENTION(name, host, text()); -} - -mentionName - = [a-z0-9_]i (&("-"+ [a-z0-9_]i) . / [a-z0-9_]i)* -{ - // NOTE: first char and last char are not "-". - return text(); -} - -mentionHost - = [a-z0-9_]i (&([.-]i+ [a-z0-9_]i) . / [a-z0-9_]i)* -{ - // NOTE: first char and last char are neither "." nor "-". - return text(); -} - -// inline: hashtag - -hashtag - = "#" !("\uFE0F"? "\u20E3") !(invalidHashtagContent !hashtagContentPart) content:$hashtagContentPart+ -{ - return HASHTAG(content); -} - -invalidHashtagContent - = [0-9]+ - -hashtagContentPart - = "(" hashPairInner ")" - / "[" hashPairInner "]" - / "「" hashPairInner "」" - / "(" hashPairInner ")" - / ![  \t.,!?'"#:\/\[\]【】()「」()<>] CHAR - -hashPairInner - = &{ return enterNest(); } @(@hashtagContentPart* &{ return leaveNest(); } / &{ return fallbackNest(); }) - -// inline: URL - -url - = "<" url:$("http" "s"? "://" (!(">" / _) CHAR)+) ">" -{ - return N_URL(url, true); -} - / "http" "s"? "://" (&([.,]+ urlContentPart) . / urlContentPart)+ -{ - // NOTE: last char is neither "." nor ",". - return N_URL(text()); -} - -urlContentPart - = "(" urlPairInner ")" - / "[" urlPairInner "]" - / [a-z0-9_/:%#@$&?!~=+-]i - -urlPairInner - = &{ return enterNest(); } @(@(urlContentPart / [.,])* &{ return leaveNest(); } / &{ return fallbackNest(); }) - -// inline: link - -link - = silent:"?"? "[" label:linkLabel "](" url:url ")" -{ - return LINK((silent != null), url.props.url, mergeText(label)); -} - -linkLabel - = (!"]" @L_inline)+ - -// inline: fn - -fn - = "$[" name:$([a-z0-9_]i)+ &{ return ensureFnName(name); } args:fnArgs? _ content:fnContent "]" -{ - args = args || {}; - return FN(name, args, mergeText(content)); -} - -L_fn - = "$[" name:$([a-z0-9_]i)+ &{ return ensureFnName(name); } args:fnArgs? _ content:L_fnContent "]" -{ - args = args || {}; - return FN(name, args, mergeText(content)); -} - -fnContent - = &{ return enterNest(); } @(@(!"]" @inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -L_fnContent - = &{ return enterNest(); } @(@(!"]" @L_inline)+ &{ return leaveNest(); } / &{ return fallbackNest(); }) - -fnArgs - = "." head:fnArg tails:("," @fnArg)* -{ - const args = { }; - for (const pair of [head, ...tails]) { - args[pair.k] = pair.v; - } - return args; -} - -fnArg - = k:$([a-z0-9_]i)+ "=" v:$([a-z0-9_.]i)+ -{ - return { k, v }; -} - / k:$([a-z0-9_]i)+ -{ - return { k: k, v: true }; -} - -// inline: plain - -plain - = "" LF? content:plainContent LF? "" -{ - return PLAIN(content); -} - -plainContent - = (!(LF? "") .)+ -{ - return text(); -} - -// inline: text - -inlineText - = !(LF / _) [a-z0-9]i &(hashtag / mention / italicAlt) . { return text(); } // hashtag, mention, italic ignore - / . /* text node */ - -L_inlineText - = !(LF / _) [a-z0-9]i &italicAlt . { return text(); } // italic ignore - / . /* text node */ - -// inline: text (for simpleParser) - -simpleText - = . /* text node */ - -// -// General -// - -BEGIN "beginning of line" - = LF / &{ return location().start.column == 1; } - -END "end of line" - = LF / EOF - -EOF - = !. - -CHAR - = !LF . { return text(); } - -LF - = "\r\n" / [\r\n] - -_ "whitespace" - = [  \t\u00a0] diff --git a/src/internal/parser.ts b/src/internal/parser.ts new file mode 100644 index 0000000..3cca5ea --- /dev/null +++ b/src/internal/parser.ts @@ -0,0 +1,749 @@ +import * as M from '..'; +import * as P from './core'; +import { mergeText } from './util'; + +// NOTE: +// tsdのテストでファイルを追加しているにも関わらず「twemoji-parser/dist/lib/regex」の型定義ファイルがないとエラーが出るため、 +// このエラーを無視する。 +/* eslint @typescript-eslint/ban-ts-comment: 1 */ +// @ts-ignore +import twemojiRegex from 'twemoji-parser/dist/lib/regex'; + +type ArgPair = { k: string, v: string | true }; +type Args = Record; + +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[]): P.Parser { + return new P.Parser((input, index, state) => { + const accum: any[] = []; + let latestIndex = index; + for (let i = 0 ; i < parsers.length; i++) { + const result = parsers[i].handler(input, latestIndex, state); + if (!result.success) { + if (latestIndex === index) { + return P.failure(); + } else { + return P.success(latestIndex, input.slice(index, latestIndex)); + } + } + accum.push(result.value); + latestIndex = result.index; + } + return P.success(latestIndex, accum); + }); +} + +const notLinkLabel = new P.Parser((_input, index, state) => { + return (!state.linkLabel) + ? P.success(index, null) + : P.failure(); +}); + +const nestable = new P.Parser((_input, index, state) => { + return (state.depth < state.nestLimit) + ? P.success(index, null) + : P.failure(); +}); + +function nest(parser: P.Parser, fallback?: P.Parser): P.Parser { + // nesting limited? -> No: specified parser, Yes: fallback parser (default = P.char) + const inner = P.alt([ + P.seq([nestable, parser], 1), + (fallback != null) ? fallback : P.char, + ]); + return new P.Parser((input, index, state) => { + state.depth++; + const result = inner.handler(input, index, state); + state.depth--; + return result; + }); +} + +export const language = P.createLanguage({ + fullParser: r => { + return r.full.many(0); + }, + + simpleParser: r => { + return r.simple.many(0); + }, + + full: r => { + return P.alt([ + // Regexp + r.unicodeEmoji, + // "
" block + r.centerTag, + // "" + r.smallTag, + // "" + r.plainTag, + // "" + r.boldTag, + // "" + r.italicTag, + // "" + r.strikeTag, + // "" block + r.quote, + // "\\[" block + r.mathBlock, + // "\\(" + r.mathInline, + // "~~" + r.strikeWave, + // "$["" + r.fn, + // "@" + r.mention, + // "#" + r.hashtag, + // ":" + r.emojiCode, + // "?[" or "[" + r.link, + // http + r.url, + // block + r.search, + r.text, + ]); + }, + + simple: r => { + return P.alt([ + r.unicodeEmoji, // Regexp + r.emojiCode, // ":" + r.text, + ]); + }, + + inline: r => { + return P.alt([ + // Regexp + r.unicodeEmoji, + // "" + r.smallTag, + // "" + r.plainTag, + // "" + r.boldTag, + // "" + r.italicTag, + // "" + r.strikeTag, + // { + const lines: P.Parser = 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([ + newLine.option(), + newLine.option(), + P.lineBegin, + lines, + newLine.option(), + newLine.option(), + ], 3); + return new P.Parser((input, index, state) => { + let result; + // parse quote + result = parser.handler(input, index, state); + if (!result.success) { + return result; + } + const contents = result.value; + const quoteIndex = result.index; + // disallow empty content if single line + if (contents.length === 1 && contents[0].length === 0) { + return P.failure(); + } + // parse inner content + const contentParser = nest(r.fullParser).many(0); + result = contentParser.handler(contents.join('\n'), 0, state); + if (!result.success) { + return result; + } + return P.success(quoteIndex, M.QUOTE(mergeText(result.value))); + }); + }, + + codeBlock: r => { + const mark = P.str('```'); + return P.seq([ + newLine.option(), + P.lineBegin, + mark, + P.seq([P.notMatch(newLine), P.char], 1).many(0), + newLine, + P.seq([P.notMatch(P.seq([newLine, mark, P.lineEnd])), P.char], 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(''); + return M.CODE_BLOCK(code, (lang.length > 0 ? lang : null)); + }); + }, + + mathBlock: r => { + const open = P.str('\\['); + const close = P.str('\\]'); + return P.seq([ + newLine.option(), + P.lineBegin, + open, + newLine.option(), + P.seq([P.notMatch(P.seq([newLine.option(), close])), P.char], 1).many(1), + newLine.option(), + close, + P.lineEnd, + newLine.option(), + ]).map(result => { + const formula = (result[4] as string[]).join(''); + return M.MATH_BLOCK(formula); + }); + }, + + centerTag: r => { + const open = P.str('
'); + const close = P.str('
'); + 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), + newLine.option(), + close, + P.lineEnd, + newLine.option(), + ]).map(result => { + return M.CENTER(mergeText(result[4])); + }); + }, + + big: r => { + const mark = P.str('***'); + return seqOrText([ + mark, + P.seq([P.notMatch(mark), nest(r.inline)], 1).many(1), + mark, + ]).map(result => { + if (typeof result === 'string') return result; + return M.FN('tada', {}, mergeText(result[1])); + }); + }, + + boldAsta: r => { + const mark = P.str('**'); + return seqOrText([ + mark, + P.seq([P.notMatch(mark), nest(r.inline)], 1).many(1), + mark, + ]).map(result => { + if (typeof result === 'string') return result; + return M.BOLD(mergeText(result[1] as (M.MfmInline | string)[])); + }); + }, + + boldTag: r => { + const open = P.str(''); + const close = P.str(''); + return seqOrText([ + open, + P.seq([P.notMatch(close), nest(r.inline)], 1).many(1), + close, + ]).map(result => { + if (typeof result === 'string') return result; + return M.BOLD(mergeText(result[1] as (M.MfmInline | string)[])); + }); + }, + + boldUnder: r => { + const mark = P.str('__'); + return P.seq([ + mark, + P.alt([alphaAndNum, space]).many(1), + mark, + ]).map(result => M.BOLD(mergeText(result[1] as string[]))); + }, + + smallTag: r => { + const open = P.str(''); + const close = P.str(''); + return seqOrText([ + open, + P.seq([P.notMatch(close), nest(r.inline)], 1).many(1), + close, + ]).map(result => { + if (typeof result === 'string') return result; + return M.SMALL(mergeText(result[1] as (M.MfmInline | string)[])); + }); + }, + + italicTag: r => { + const open = P.str(''); + const close = P.str(''); + return seqOrText([ + open, + P.seq([P.notMatch(close), nest(r.inline)], 1).many(1), + close, + ]).map(result => { + if (typeof result === 'string') return result; + return M.ITALIC(mergeText(result[1] as (M.MfmInline | string)[])); + }); + }, + + italicAsta: r => { + const mark = P.str('*'); + 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) { + return P.failure(); + } + // check before + const beforeStr = input.slice(0, index); + if (/[a-z0-9]$/i.test(beforeStr)) { + return P.failure(); + } + return P.success(result.index, M.ITALIC(mergeText(result.value[1] as string[]))); + }); + }, + + italicUnder: r => { + const mark = P.str('_'); + 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) { + return P.failure(); + } + // check before + const beforeStr = input.slice(0, index); + if (/[a-z0-9]$/i.test(beforeStr)) { + return P.failure(); + } + return P.success(result.index, M.ITALIC(mergeText(result.value[1] as string[]))); + }); + }, + + strikeTag: r => { + const open = P.str(''); + const close = P.str(''); + return seqOrText([ + open, + P.seq([P.notMatch(close), nest(r.inline)], 1).many(1), + close, + ]).map(result => { + if (typeof result === 'string') return result; + return M.STRIKE(mergeText(result[1] as (M.MfmInline | string)[])); + }); + }, + + strikeWave: r => { + const mark = P.str('~~'); + return seqOrText([ + mark, + P.seq([P.notMatch(P.alt([mark, newLine])), nest(r.inline)], 1).many(1), + mark, + ]).map(result => { + if (typeof result === 'string') return result; + return M.STRIKE(mergeText(result[1] as (M.MfmInline | string)[])); + }); + }, + + unicodeEmoji: r => { + const emoji = RegExp(twemojiRegex.source); + return P.regexp(emoji).map(content => M.UNI_EMOJI(content)); + }, + + plainTag: r => { + const open = P.str(''); + const close = P.str(''); + return P.seq([ + open, + newLine.option(), + P.seq([ + P.notMatch(P.seq([newLine.option(), close])), + P.char, + ], 1).many(1).text(), + newLine.option(), + close, + ], 2).map(result => M.PLAIN(result)); + }, + + fn: r => { + const fnName = new P.Parser((input, index, state) => { + const result = P.regexp(/[a-z0-9_]+/i).handler(input, index, state); + if (!result.success) { + return result; + } + if (state.fnNameList != null && !state.fnNameList.includes(result.value)) { + return P.failure(); + } + return P.success(result.index, result.value); + }); + const arg: P.Parser = P.seq([ + P.regexp(/[a-z0-9_]+/i), + P.seq([ + P.str('='), + P.regexp(/[a-z0-9_.]+/i), + ], 1).option(), + ]).map(result => { + return { + k: result[0], + v: (result[1] != null) ? result[1] : true, + }; + }); + const args = P.seq([ + P.str('.'), + arg.sep(P.str(','), 1), + ], 1).map(pairs => { + const result: Args = { }; + for (const pair of pairs) { + result[pair.k] = pair.v; + } + return result; + }); + const fnClose = P.str(']'); + return seqOrText([ + P.str('$['), + fnName, + args.option(), + P.str(' '), + P.seq([P.notMatch(fnClose), nest(r.inline)], 1).many(1), + fnClose, + ]).map(result => { + if (typeof result === 'string') return result; + const name = result[1]; + const args = result[2] || {}; + const content = result[4]; + return M.FN(name, args, mergeText(content)); + }); + }, + + inlineCode: r => { + const mark = P.str('`'); + return P.seq([ + mark, + P.seq([ + P.notMatch(P.alt([mark, P.str('´'), newLine])), + P.char, + ], 1).many(1), + mark, + ]).map(result => M.INLINE_CODE(result[1].join(''))); + }, + + mathInline: r => { + const open = P.str('\\('); + const close = P.str('\\)'); + return P.seq([ + open, + P.seq([ + P.notMatch(P.alt([close, newLine])), + P.char, + ], 1).many(1), + close, + ]).map(result => M.MATH_INLINE(result[1].join(''))); + }, + + mention: r => { + const parser = P.seq([ + notLinkLabel, + P.str('@'), + P.regexp(/[a-z0-9_-]+/i), + P.seq([ + P.str('@'), + P.regexp(/[a-z0-9_.-]+/i), + ], 1).option(), + ]); + return new P.Parser((input, index, state) => { + let result; + result = parser.handler(input, index, state); + if (!result.success) { + return P.failure(); + } + // check before (not mention) + const beforeStr = input.slice(0, index); + if (/[a-z0-9]$/i.test(beforeStr)) { + return P.failure(); + } + let invalidMention = false; + const resultIndex = result.index; + const username: string = result.value[2]; + const hostname: string | null = result.value[3]; + // 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)); + if (modifiedHost.length === 0) { + // disallow invalid char only hostname + invalidMention = true; + modifiedHost = null; + } + } + } + // remove "-" of tail of username + let modifiedName = username; + result = /-+$/.exec(username); + if (result != null) { + if (modifiedHost == null) { + modifiedName = username.slice(0, (-1 * result[0].length)); + } else { + // cannnot to remove tail of username if exist hostname + invalidMention = true; + } + } + // disallow "-" of head of username + if (modifiedName.length === 0 || modifiedName[0] === '-') { + invalidMention = true; + } + // disallow [.-] of head of hostname + if (modifiedHost != null && /^[.-]/.test(modifiedHost)) { + invalidMention = true; + } + // generate a text if mention is invalid + if (invalidMention) { + return P.success(resultIndex, input.slice(index, resultIndex)); + } + const acct = modifiedHost != null ? `@${modifiedName}@${modifiedHost}` : `@${modifiedName}`; + return P.success(index + acct.length, M.MENTION(modifiedName, modifiedHost, acct)); + }); + }, + + hashtag: r => { + const mark = P.str('#'); + const hashTagChar = P.seq([ + P.notMatch(P.alt([P.regexp(/[ \u3000\t.,!?'"#:/[\]【】()「」()<>]/), space, newLine])), + P.char, + ], 1); + const innerItem: P.Parser = P.lazy(() => P.alt([ + P.seq([ + P.str('('), nest(innerItem, hashTagChar).many(0), P.str(')'), + ]), + P.seq([ + P.str('['), nest(innerItem, hashTagChar).many(0), P.str(']'), + ]), + P.seq([ + P.str('「'), nest(innerItem, hashTagChar).many(0), P.str('」'), + ]), + P.seq([ + P.str('('), nest(innerItem, hashTagChar).many(0), P.str(')'), + ]), + hashTagChar, + ])); + const parser = P.seq([ + notLinkLabel, + mark, + innerItem.many(1).text(), + ], 2); + return new P.Parser((input, index, state) => { + const result = parser.handler(input, index, state); + if (!result.success) { + return P.failure(); + } + // check before + const beforeStr = input.slice(0, index); + if (/[a-z0-9]$/i.test(beforeStr)) { + return P.failure(); + } + const resultIndex = result.index; + const resultValue = result.value; + // disallow number only + if (/^[0-9]+$/.test(resultValue)) { + return P.failure(); + } + return P.success(resultIndex, M.HASHTAG(resultValue)); + }); + }, + + emojiCode: r => { + const mark = P.str(':'); + return P.seq([ + mark, + P.regexp(/[a-z0-9_+-]+/i), + mark, + ], 1).map(name => M.EMOJI_CODE(name as string)); + }, + + link: r => { + const labelInline = new P.Parser((input, index, state) => { + state.linkLabel = true; + const result = r.inline.handler(input, index, state); + state.linkLabel = false; + return result; + }); + const closeLabel = P.str(']'); + return P.seq([ + notLinkLabel, + P.alt([P.str('?['), P.str('[')]), + P.seq([ + P.notMatch(P.alt([closeLabel, newLine])), + nest(labelInline), + ], 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)); + }); + }, + + url: r => { + const urlChar = P.regexp(/[.,a-z0-9_/:%#@$&?!~=+-]/i); + const innerItem: P.Parser = P.lazy(() => P.alt([ + P.seq([ + P.str('('), nest(innerItem, urlChar).many(0), P.str(')'), + ]), + P.seq([ + P.str('['), nest(innerItem, urlChar).many(0), P.str(']'), + ]), + urlChar, + ])); + const parser = P.seq([ + notLinkLabel, + P.regexp(/https?:\/\//), + innerItem.many(1).text(), + ]); + return new P.Parser((input, index, state) => { + let result; + result = parser.handler(input, index, state); + if (!result.success) { + return P.failure(); + } + const resultIndex = result.index; + let modifiedIndex = resultIndex; + 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)); + if (content.length === 0) { + return P.success(resultIndex, input.slice(index, resultIndex)); + } + } + return P.success(modifiedIndex, M.N_URL(schema + content, false)); + }); + }, + + urlAlt: r => { + const open = P.str('<'); + const close = P.str('>'); + const parser = P.seq([ + notLinkLabel, + open, + P.regexp(/https?:\/\//), + P.seq([P.notMatch(P.alt([close, space])), P.char], 1).many(1), + close, + ]).text(); + return new P.Parser((input, index, state) => { + const result = parser.handler(input, index, state); + if (!result.success) { + return P.failure(); + } + const text = result.value.slice(1, (result.value.length - 1)); + return P.success(result.index, M.N_URL(text, true)); + }); + }, + + search: r => { + const button = P.alt([ + P.regexp(/\[(検索|search)\]/i), + P.regexp(/(検索|search)/i), + ]); + return P.seq([ + newLine.option(), + P.lineBegin, + P.seq([ + P.notMatch(P.alt([ + newLine, + P.seq([space, button, P.lineEnd]), + ])), + P.char, + ], 1).many(1), + space, + button, + P.lineEnd, + newLine.option(), + ]).map(result => { + const query = result[2].join(''); + return M.SEARCH(query, `${query}${result[3]}${result[4]}`); + }); + }, + + text: r => P.char, +}); diff --git a/src/internal/util.ts b/src/internal/util.ts index 04df9e2..3f0cffc 100644 --- a/src/internal/util.ts +++ b/src/internal/util.ts @@ -1,7 +1,7 @@ -import { isMfmBlock, MfmNode, TEXT } from '../node'; +import { isMfmBlock, MfmInline, MfmNode, MfmText, TEXT } from '../node'; -export function mergeText(nodes: (MfmNode | string)[]): MfmNode[] { - const dest: MfmNode[] = []; +export function mergeText(nodes: ((T extends MfmInline ? MfmInline : MfmNode) | string)[]): (T | MfmText)[] { + const dest: (T | MfmText)[] = []; const storedChars: string[] = []; /** @@ -14,11 +14,15 @@ export function mergeText(nodes: (MfmNode | string)[]): MfmNode[] { } } - for (const node of nodes) { + const flatten = nodes.flat(1) as (string | T)[]; + for (const node of flatten) { if (typeof node === 'string') { // Store the char. storedChars.push(node); } + else if (!Array.isArray(node) && node.type === 'text') { + storedChars.push((node as MfmText).props.text); + } else { generateText(); dest.push(node); @@ -163,39 +167,3 @@ export function inspectOne(node: MfmNode, action: (node: MfmNode) => void) { } } } - -// -// 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/parser.ts b/test/parser.ts index 93a9b7c..6a2393f 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -143,6 +143,24 @@ hoge`; ]; assert.deepStrictEqual(mfm.parse(input), output); }); + it('2つの引用行の間に空行がある場合は2つの引用ブロックが生成される', () => { + const input = ` +> foo + +> bar + +hoge`; + const output = [ + QUOTE([ + TEXT('foo') + ]), + QUOTE([ + TEXT('bar') + ]), + TEXT('hoge'), + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); }); describe('search', () => { @@ -684,6 +702,60 @@ hoge`; const output = [TEXT('あいう'), MENTION('abc', null, '@abc')]; assert.deepStrictEqual(mfm.parse(input), output); }); + + it('invalid char only username', () => { + const input = '@-'; + const output = [TEXT('@-')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('invalid char only hostname', () => { + const input = '@abc@.'; + const output = [TEXT('@abc@.')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('allow "-" in username', () => { + const input = '@abc-d'; + const output = [MENTION('abc-d', null, '@abc-d')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('disallow "-" in head of username', () => { + const input = '@-abc'; + const output = [TEXT('@-abc')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('disallow "-" in tail of username', () => { + const input = '@abc-'; + const output = [MENTION('abc', null, '@abc'), TEXT('-')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('disallow "." in head of hostname', () => { + const input = '@abc@.aaa'; + const output = [TEXT('@abc@.aaa')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('disallow "." in tail of hostname', () => { + const input = '@abc@aaa.'; + const output = [MENTION('abc', 'aaa', '@abc@aaa'), TEXT('.')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('disallow "-" in head of hostname', () => { + const input = '@abc@-aaa'; + const output = [TEXT('@abc@-aaa')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('disallow "-" in tail of hostname', () => { + const input = '@abc@aaa-'; + const output = [MENTION('abc', 'aaa', '@abc@aaa'), TEXT('-')]; + assert.deepStrictEqual(mfm.parse(input), output); + }); }); describe('hashtag', () => { @@ -847,6 +919,14 @@ hoge`; assert.deepStrictEqual(mfm.parse(input), output); }); + it('disallow period only', () => { + const input = 'https://.'; + const output = [ + TEXT('https://.') + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + it('ignore trailing periods', () => { const input = 'https://misskey.io/@ai...'; const output = [ @@ -1107,6 +1187,14 @@ hoge`; assert.deepStrictEqual(mfm.parse(input), output); }); + it('invalid fn name', () => { + const input = '$[関数 text]'; + const output = [ + TEXT('$[関数 text]') + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + it('nest', () => { const input = '$[spin.speed=1.1s $[shake a]]'; const output = [ @@ -1179,11 +1267,7 @@ hoge`; const output = [ QUOTE([ QUOTE([ - TEXT('*'), - ITALIC([ - TEXT('abc'), - ]), - TEXT('*'), + TEXT('**abc**'), ]), ]), ]; @@ -1196,11 +1280,7 @@ hoge`; const output = [ BOLD([ BOLD([ - TEXT('**'), - ITALIC([ - TEXT('abc'), - ]), - TEXT('**'), + TEXT('***abc***'), ]), ]), ]; @@ -1213,11 +1293,7 @@ hoge`; const output = [ ITALIC([ ITALIC([ - TEXT('*'), - ITALIC([ - TEXT('abc'), - ]), - TEXT('*'), + TEXT('**abc**'), ]), ]), ]; @@ -1289,13 +1365,19 @@ hoge`; describe('hashtag', () => { it('basic', () => { - const input = '#abc(xyz)'; - const output = [ + let input, output; + input = '#abc(xyz)'; + output = [ BOLD([ - BOLD([ - HASHTAG('abc'), - TEXT('(xyz)'), - ]), + HASHTAG('abc(xyz)'), + ]), + ]; + assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output); + input = '#abc(x(y)z)'; + output = [ + BOLD([ + HASHTAG('abc'), + TEXT('(x(y)z)'), ]), ]; assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output); @@ -1343,13 +1425,19 @@ hoge`; }); it('url', () => { - const input = 'https://example.com/abc(xyz)'; - const output = [ + let input, output; + input = 'https://example.com/abc(xyz)'; + output = [ BOLD([ - BOLD([ - N_URL('https://example.com/abc'), - TEXT('(xyz)'), - ]), + N_URL('https://example.com/abc(xyz)'), + ]), + ]; + assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output); + input = 'https://example.com/abc(x(y)z)'; + output = [ + BOLD([ + N_URL('https://example.com/abc'), + TEXT('(x(y)z)'), ]), ]; assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output); diff --git a/tsconfig.json b/tsconfig.json index 6e4f3a8..e78c11f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,10 @@ "noImplicitReturns": true, "esModuleInterop": true, }, + "typeRoots": [ + "node_modules/@types", + "src/@types", + ], "include": [ "src/**/*", ],