snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
This commit is contained in:
2
packages/message-parser/.gitignore
vendored
Normal file
2
packages/message-parser/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/dist
|
||||
271
packages/message-parser/CHANGELOG.md
Normal file
271
packages/message-parser/CHANGELOG.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# Change Log
|
||||
|
||||
## 0.32.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ([#41113](https://github.com/RocketChat/Rocket.Chat/pull/41113)) Adds support for horizontal rules (thematic breaks) in the message parser. A line of 3 or more contiguous dashes (`---`, with nothing else on the line) is parsed into a new `HORIZONTAL_RULE` block node and rendered with Fuselage's `Divider`. The node carries an optional `fallback` — a `[start, end]` offset span into the original source — so renderers without horizontal-rule support can slice the source to show the raw markup instead of dropping it, without duplicating the text into the AST. Only `-` is accepted: CommonMark also allows `*` and `_`, but those collide with emphasis and with censored words (bad-words masks a term as a run of `*`), so a bare `***` / `_______` line stays text/emphasis instead of becoming a divider.
|
||||
|
||||
- ([#41109](https://github.com/RocketChat/Rocket.Chat/pull/41109)) Adds GFM-style table support to the message parser and renders it in gazzodown.
|
||||
|
||||
Parser: tables require a leading and trailing pipe on every row, support column alignment via the delimiter row (`:---`, `:--:`, `---:`), and allow inline markup inside cells (a literal pipe must be escaped as `\|`). New `TABLE`, `TABLE_ROW`, and `TABLE_CELL` AST nodes are emitted. The `TABLE` node also carries an optional `fallback` — a `[start, end]` offset span into the original source — so renderers without table support can slice the source to show the raw markup instead of dropping it, without duplicating the text into the AST.
|
||||
|
||||
Rendering: gazzodown renders these tables using Fuselage's `Table` components with per-column alignment, and shows a compact single-row preview of the table header in message previews.
|
||||
|
||||
- ([#41110](https://github.com/RocketChat/Rocket.Chat/pull/41110)) Normalizes the `Timestamp` node's `fallback` to the same `[start, end]` source-offset span used by other blocks, instead of a reconstructed plain-text node. The type still allows the previous `Plain` form so already-persisted data keeps type-checking and is safely ignored at render time.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#41312](https://github.com/RocketChat/Rocket.Chat/pull/41312)) Fixes code fences failing to render when a line inside them ends with an inline-code backtick (e.g. `` - **Node**: `22.22.3` ``). A trailing backtick immediately before a line break could not be consumed as content, causing the whole ` ``` ` block to fall back to markdown parsing and split apart. Trailing 1-2 backticks before a line end (or EOF) are now treated as code content.
|
||||
|
||||
- ([#41441](https://github.com/RocketChat/Rocket.Chat/pull/41441)) Fixes an issue in which some combined emojis like 😶🌫️, 😮💨 and 😵💫 were being displayed as two separate emojis, and the flags of some countries like England 🏴, Scotland 🏴 and Wales 🏴 were being displayed as a plain black flag
|
||||
|
||||
## 0.32.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ([#41113](https://github.com/RocketChat/Rocket.Chat/pull/41113)) Adds support for horizontal rules (thematic breaks) in the message parser. A line of 3 or more contiguous dashes (`---`, with nothing else on the line) is parsed into a new `HORIZONTAL_RULE` block node and rendered with Fuselage's `Divider`. The node carries an optional `fallback` — a `[start, end]` offset span into the original source — so renderers without horizontal-rule support can slice the source to show the raw markup instead of dropping it, without duplicating the text into the AST. Only `-` is accepted: CommonMark also allows `*` and `_`, but those collide with emphasis and with censored words (bad-words masks a term as a run of `*`), so a bare `***` / `_______` line stays text/emphasis instead of becoming a divider.
|
||||
|
||||
- ([#41109](https://github.com/RocketChat/Rocket.Chat/pull/41109)) Adds GFM-style table support to the message parser and renders it in gazzodown.
|
||||
|
||||
Parser: tables require a leading and trailing pipe on every row, support column alignment via the delimiter row (`:---`, `:--:`, `---:`), and allow inline markup inside cells (a literal pipe must be escaped as `\|`). New `TABLE`, `TABLE_ROW`, and `TABLE_CELL` AST nodes are emitted. The `TABLE` node also carries an optional `fallback` — a `[start, end]` offset span into the original source — so renderers without table support can slice the source to show the raw markup instead of dropping it, without duplicating the text into the AST.
|
||||
|
||||
Rendering: gazzodown renders these tables using Fuselage's `Table` components with per-column alignment, and shows a compact single-row preview of the table header in message previews.
|
||||
|
||||
- ([#41110](https://github.com/RocketChat/Rocket.Chat/pull/41110)) Normalizes the `Timestamp` node's `fallback` to the same `[start, end]` source-offset span used by other blocks, instead of a reconstructed plain-text node. The type still allows the previous `Plain` form so already-persisted data keeps type-checking and is safely ignored at render time.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#41312](https://github.com/RocketChat/Rocket.Chat/pull/41312)) Fixes code fences failing to render when a line inside them ends with an inline-code backtick (e.g. `` - **Node**: `22.22.3` ``). A trailing backtick immediately before a line break could not be consumed as content, causing the whole ` ``` ` block to fall back to markdown parsing and split apart. Trailing 1-2 backticks before a line end (or EOF) are now treated as code content.
|
||||
|
||||
- ([#41441](https://github.com/RocketChat/Rocket.Chat/pull/41441)) Fixes an issue in which some combined emojis like 😶🌫️, 😮💨 and 😵💫 were being displayed as two separate emojis, and the flags of some countries like England 🏴, Scotland 🏴 and Wales 🏴 were being displayed as a plain black flag
|
||||
|
||||
## 0.31.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#39853](https://github.com/RocketChat/Rocket.Chat/pull/39853)) Add property-based fuzz testing using `fast-check` to continuously evaluate memory limits, structural boundaries, and backtracking behavior against heavily randomized markdown arrays.
|
||||
|
||||
- ([#39853](https://github.com/RocketChat/Rocket.Chat/pull/39853)) feat(message-parser): implement BlockSplitter PoC (Layer 1)
|
||||
|
||||
- ([#39853](https://github.com/RocketChat/Rocket.Chat/pull/39853)) Add test coverage for isNodeOfType guard bringing branch coverage to 100%
|
||||
|
||||
- ([#39853](https://github.com/RocketChat/Rocket.Chat/pull/39853)) Add test coverage for joinEmoji behavior through reducePlainTexts
|
||||
|
||||
## 0.31.36-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#39853](https://github.com/RocketChat/Rocket.Chat/pull/39853)) Add property-based fuzz testing using `fast-check` to continuously evaluate memory limits, structural boundaries, and backtracking behavior against heavily randomized markdown arrays.
|
||||
|
||||
- ([#39853](https://github.com/RocketChat/Rocket.Chat/pull/39853)) feat(message-parser): implement BlockSplitter PoC (Layer 1)
|
||||
|
||||
- ([#39853](https://github.com/RocketChat/Rocket.Chat/pull/39853)) Add test coverage for isNodeOfType guard bringing branch coverage to 100%
|
||||
|
||||
- ([#39853](https://github.com/RocketChat/Rocket.Chat/pull/39853)) Add test coverage for joinEmoji behavior through reducePlainTexts
|
||||
|
||||
## 0.31.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#39062](https://github.com/RocketChat/Rocket.Chat/pull/39062) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixed blockquotes with empty lines between paragraphs not rendering as a single blockquote. Lines like `> ` or `>` (empty quote lines) are now treated as part of the surrounding blockquote rather than breaking it into separate quotes.
|
||||
|
||||
- ([#39046](https://github.com/RocketChat/Rocket.Chat/pull/39046) by [@smirk-dev](https://github.com/smirk-dev)) Replaces wasteful `filter().shift()` with `find(Boolean)` in `extractFirstResult` to avoid allocating an intermediate filtered array just to get the first truthy element.
|
||||
|
||||
- ([#39069](https://github.com/RocketChat/Rocket.Chat/pull/39069) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixes trailing punctuation (e.g. periods, exclamation marks) being incorrectly included in parsed URLs when they appear at the end of a message. For example, `go to https://www.google.com.` now correctly parses the URL as `https://www.google.com` without the trailing period.
|
||||
|
||||
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
|
||||
|
||||
- ([#39052](https://github.com/RocketChat/Rocket.Chat/pull/39052) by [@Shreyas2004wagh](https://github.com/Shreyas2004wagh)) Fixes ordered list AST generation to preserve `number: 0` for list items that start at index `0`.
|
||||
|
||||
## 0.31.35-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#39062](https://github.com/RocketChat/Rocket.Chat/pull/39062) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixed blockquotes with empty lines between paragraphs not rendering as a single blockquote. Lines like `> ` or `>` (empty quote lines) are now treated as part of the surrounding blockquote rather than breaking it into separate quotes.
|
||||
|
||||
- ([#39046](https://github.com/RocketChat/Rocket.Chat/pull/39046) by [@smirk-dev](https://github.com/smirk-dev)) Replaces wasteful `filter().shift()` with `find(Boolean)` in `extractFirstResult` to avoid allocating an intermediate filtered array just to get the first truthy element.
|
||||
|
||||
- ([#39069](https://github.com/RocketChat/Rocket.Chat/pull/39069) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixes trailing punctuation (e.g. periods, exclamation marks) being incorrectly included in parsed URLs when they appear at the end of a message. For example, `go to https://www.google.com.` now correctly parses the URL as `https://www.google.com` without the trailing period.
|
||||
|
||||
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
|
||||
|
||||
- ([#39052](https://github.com/RocketChat/Rocket.Chat/pull/39052) by [@Shreyas2004wagh](https://github.com/Shreyas2004wagh)) Fixes ordered list AST generation to preserve `number: 0` for list items that start at index `0`.
|
||||
|
||||
## 0.31.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38629](https://github.com/RocketChat/Rocket.Chat/pull/38629) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixes markdown breaking when text in square brackets appears between hyperlinks. This resolves issues #31418 and #31766 where typing `[text]` between links would incorrectly parse the markdown structure.
|
||||
|
||||
- ([#38779](https://github.com/RocketChat/Rocket.Chat/pull/38779) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) fixes an issues where markdown link parser to was not handling parentheses in URLs
|
||||
|
||||
## 0.31.34-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38629](https://github.com/RocketChat/Rocket.Chat/pull/38629) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) Fixes markdown breaking when text in square brackets appears between hyperlinks. This resolves issues #31418 and #31766 where typing `[text]` between links would incorrectly parse the markdown structure.
|
||||
|
||||
- ([#38779](https://github.com/RocketChat/Rocket.Chat/pull/38779) by [@copilot-swe-agent](https://github.com/copilot-swe-agent)) fixes an issues where markdown link parser to was not handling parentheses in URLs
|
||||
|
||||
## 0.31.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38509](https://github.com/RocketChat/Rocket.Chat/pull/38509)) Added support for parsing ISO-format timestamps in timestamp markdown
|
||||
|
||||
## 0.31.33-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38509](https://github.com/RocketChat/Rocket.Chat/pull/38509)) Added support for parsing ISO-format timestamps in timestamp markdown
|
||||
|
||||
## 0.31.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#35172](https://github.com/RocketChat/Rocket.Chat/pull/35172)) fixes an issue where the message parser was not properly parsing bold and italic when the symbols would also match an emoticon
|
||||
|
||||
## 0.31.32-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#35172](https://github.com/RocketChat/Rocket.Chat/pull/35172)) fixes an issue where the message parser was not properly parsing bold and italic when the symbols would also match an emoticon
|
||||
|
||||
## 0.31.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#33227](https://github.com/RocketChat/Rocket.Chat/pull/33227)) Improved the performance of the message parser
|
||||
|
||||
## 0.31.31-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#33227](https://github.com/RocketChat/Rocket.Chat/pull/33227)) Improved the performance of the message parser
|
||||
|
||||
## 0.31.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#33254](https://github.com/RocketChat/Rocket.Chat/pull/33254) by [@dionisio-bot](https://github.com/dionisio-bot)) Improved the performance of the message parser
|
||||
|
||||
## 0.31.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#31810](https://github.com/RocketChat/Rocket.Chat/pull/31810)) feat(message-parser): add timestamps pattern
|
||||
|
||||
### Usage
|
||||
|
||||
Pattern: <t:{timestamp}:?{format}>
|
||||
|
||||
- {timestamp} is a Unix timestamp
|
||||
- {format} is an optional parameter that can be used to customize the date and time format.
|
||||
|
||||
#### Formats
|
||||
|
||||
| Format | Description | Example |
|
||||
| ------ | ------------------------- | --------------------------------------- |
|
||||
| `t` | Short time | 12:00 AM |
|
||||
| `T` | Long time | 12:00:00 AM |
|
||||
| `d` | Short date | 12/31/2020 |
|
||||
| `D` | Long date | Thursday, December 31, 2020 |
|
||||
| `f` | Full date and time | Thursday, December 31, 2020 12:00 AM |
|
||||
| `F` | Full date and time (long) | Thursday, December 31, 2020 12:00:00 AM |
|
||||
| `R` | Relative time | 1 year ago |
|
||||
|
||||
## 0.31.29-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#31810](https://github.com/RocketChat/Rocket.Chat/pull/31810)) feat(message-parser): add timestamps pattern
|
||||
|
||||
### Usage
|
||||
|
||||
Pattern: <t:{timestamp}:?{format}>
|
||||
|
||||
- {timestamp} is a Unix timestamp
|
||||
- {format} is an optional parameter that can be used to customize the date and time format.
|
||||
|
||||
#### Formats
|
||||
|
||||
| Format | Description | Example |
|
||||
| ------ | ------------------------- | --------------------------------------- |
|
||||
| `t` | Short time | 12:00 AM |
|
||||
| `T` | Long time | 12:00:00 AM |
|
||||
| `d` | Short date | 12/31/2020 |
|
||||
| `D` | Long date | Thursday, December 31, 2020 |
|
||||
| `f` | Full date and time | Thursday, December 31, 2020 12:00 AM |
|
||||
| `F` | Full date and time (long) | Thursday, December 31, 2020 12:00:00 AM |
|
||||
| `R` | Relative time | 1 year ago |
|
||||
|
||||
## 0.31.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [`7fdfdb1b7`](https://github.com/RocketChat/fuselage/commit/7fdfdb1b7737808585b95cc62c4f9af2bc152b41) Thanks [@dougfabris](https://github.com/dougfabris)! - fix(message-parser): Made changes in grammar.pegjs for the strikedown approach
|
||||
|
||||
## 0.31.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [`a029dce78`](https://github.com/RocketChat/fuselage/commit/a029dce78935d8bba5cb5b09e251483fe8eabcb3) Thanks [@yash-rajpal](https://github.com/yash-rajpal)! - Stop accepting `[` in link titles
|
||||
|
||||
## 0.31.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1215](https://github.com/RocketChat/fuselage/pull/1215) [`684b73ca3`](https://github.com/RocketChat/fuselage/commit/684b73ca3b1e7c72f21f6dff23bfe46981ba472a) Thanks [@brf153](https://github.com/brf153)! - Added ChannelMention in the markup inside message-parser
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.31.0](https://github.com/RocketChat/fuselage/compare/v0.30.1...v0.31.0) (2021-12-28)
|
||||
|
||||
### Features
|
||||
|
||||
- New hooks for element size tracking ([#413](https://github.com/RocketChat/fuselage/issues/413)) ([8ca682c](https://github.com/RocketChat/fuselage/commit/8ca682c636d2e4813f7d346cb881513382be63cf))
|
||||
|
||||
# [0.30.0](https://github.com/RocketChat/fuselage/compare/v0.29.0...v0.30.0) (2021-10-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **jest:** Adjust jest and ts-jest dependencies ([#547](https://github.com/RocketChat/fuselage/issues/547)) ([91a4fa1](https://github.com/RocketChat/fuselage/commit/91a4fa1365394001afe1bd46480bda3bafed5505))
|
||||
- **message-parser:** <https://domain.com|Test> ([#546](https://github.com/RocketChat/fuselage/issues/546)) ([faca16f](https://github.com/RocketChat/fuselage/commit/faca16febe517e411dd377cae294f888f1199d40))
|
||||
- **message-parser:** Fix Url and Escaped Markdown ([#537](https://github.com/RocketChat/fuselage/issues/537)) ([bc0cbce](https://github.com/RocketChat/fuselage/commit/bc0cbce69589b9a056d797a03b78d7cd06423aaa))
|
||||
|
||||
# [0.29.0](https://github.com/RocketChat/fuselage/compare/v0.28.0...v0.29.0) (2021-08-31)
|
||||
|
||||
**Note:** Version bump only for package @rocket.chat/message-parser
|
||||
|
||||
# [0.28.0](https://github.com/RocketChat/fuselage/compare/v0.27.0...v0.28.0) (2021-07-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **onboarding-ui:** Administrator information form and Organization information form ([#489](https://github.com/RocketChat/fuselage/issues/489)) ([b289f68](https://github.com/RocketChat/fuselage/commit/b289f68676954b91c792d8d97680314178bf2c60))
|
||||
- styled API; monorepo grooming ([#482](https://github.com/RocketChat/fuselage/issues/482)) ([1b6b70c](https://github.com/RocketChat/fuselage/commit/1b6b70cf67ec16927b1566adc2350295a8927223))
|
||||
|
||||
# [0.27.0](https://github.com/RocketChat/fuselage/compare/v0.26.0...v0.27.0) (2021-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **eslint:** Add missing ESLint rule for TypeScript ([#470](https://github.com/RocketChat/fuselage/issues/470)) ([cc0d498](https://github.com/RocketChat/fuselage/commit/cc0d4989bf37f7602d1d58d051824f1dd6c096b3))
|
||||
|
||||
# [0.26.0](https://github.com/RocketChat/fuselage/compare/v0.25.0...v0.26.0) (2021-05-28)
|
||||
|
||||
**Note:** Version bump only for package @rocket.chat/message-parser
|
||||
|
||||
# [0.25.0](https://github.com/RocketChat/fuselage/compare/v0.24.0...v0.25.0) (2021-05-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **fuselage:** fix duplicated values on paginated multi select ([#456](https://github.com/RocketChat/fuselage/issues/456)) ([4518a4e](https://github.com/RocketChat/fuselage/commit/4518a4e661cb525d957f6140d59a641a50fc7b20))
|
||||
- **message-parser:** Big emoji ([#451](https://github.com/RocketChat/fuselage/issues/451)) ([6d65343](https://github.com/RocketChat/fuselage/commit/6d653433d07edabaee821bd25ad07a5878b59a86))
|
||||
- **message-parser:** URL issues ([#448](https://github.com/RocketChat/fuselage/issues/448)) ([8ce6b91](https://github.com/RocketChat/fuselage/commit/8ce6b9110547b5adf3633e87d6bc655114d4cfb4))
|
||||
- message-parser Unordered List definition ([#445](https://github.com/RocketChat/fuselage/issues/445)) ([6c659b8](https://github.com/RocketChat/fuselage/commit/6c659b821fd6294eb8033dfe03e42db2dba1ca06))
|
||||
|
||||
### Features
|
||||
|
||||
- [@rocket](https://github.com/rocket).chat/message-parser ([#443](https://github.com/RocketChat/fuselage/issues/443)) ([4722cdf](https://github.com/RocketChat/fuselage/commit/4722cdff46f5987f335d989be59649c7652bb12a))
|
||||
- Peggy loader ([#450](https://github.com/RocketChat/fuselage/issues/450)) ([0496cad](https://github.com/RocketChat/fuselage/commit/0496cad457d76f8a4d6a217209e4a55e315e8365))
|
||||
105
packages/message-parser/README.md
Normal file
105
packages/message-parser/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
<!--header-->
|
||||
|
||||
<p align="center">
|
||||
<a href="https://rocket.chat" title="Rocket.Chat">
|
||||
<img src="https://github.com/RocketChat/Rocket.Chat.Artwork/raw/master/Logos/2020/png/logo-horizontal-red.png" alt="Rocket.Chat" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# `@rocket.chat/message-parser`
|
||||
|
||||
> Rocket.Chat parser for messages
|
||||
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/@rocket.chat/message-parser/v/latest) [](https://www.npmjs.com/package/@rocket.chat/message-parser/v/next)  
|
||||
|
||||
 
|
||||
|
||||
<!--/header-->
|
||||
|
||||
## Description
|
||||
|
||||
Rocket.Chat grammar with the purpose of parsing the messages of the rocket chat, converting text to an AST tree.
|
||||
|
||||
The grammar provides support for markdown, mentions and emojis.
|
||||
|
||||
## Supported markup
|
||||
|
||||
- quotes
|
||||
- bold/italic/strike
|
||||
- ordered lists
|
||||
- unordered lists
|
||||
- task lists
|
||||
- phone numbers
|
||||
- mentions
|
||||
- emoji
|
||||
- colors
|
||||
- URI's
|
||||
- mentions users/channels
|
||||
- timestamps
|
||||
- tables
|
||||
|
||||
## Tables
|
||||
|
||||
GFM-style tables. A leading and trailing pipe is required on every row.
|
||||
Alignment is taken from the delimiter row:
|
||||
|
||||
| Marker | Alignment |
|
||||
| ------ | --------- |
|
||||
| `:---` | left |
|
||||
| `:--:` | center |
|
||||
| `---:` | right |
|
||||
| `---` | none |
|
||||
|
||||
```md
|
||||
| Header 1 | Header 2 |
|
||||
| -------- | :------: |
|
||||
| Cell 1 | Cell 2 |
|
||||
```
|
||||
|
||||
A literal pipe inside a cell must be escaped as `\|`. Cell content supports
|
||||
inline markup (bold, italic, links, emoji, …).
|
||||
|
||||
## Timestamps
|
||||
|
||||
The timestamp tag is a special tag that allows you to convert a Unix timestamp to a human-readable date and time.
|
||||
|
||||
Timestamps are allowed inside strike elements.
|
||||
|
||||
### Usage
|
||||
|
||||
Pattern: <t:{timestamp}:?{format}>
|
||||
|
||||
- {timestamp} is a Unix timestamp
|
||||
- {format} is an optional parameter that can be used to customize the date and time format.
|
||||
|
||||
#### Formats
|
||||
|
||||
| Format | Description | Example |
|
||||
| ------ | ------------------------- | --------------------------------------- |
|
||||
| `t` | Short time | 12:00 AM |
|
||||
| `T` | Long time | 12:00:00 AM |
|
||||
| `d` | Short date | 12/31/2020 |
|
||||
| `D` | Long date | Thursday, December 31, 2020 |
|
||||
| `f` | Full date and time | Thursday, December 31, 2020 12:00 AM |
|
||||
| `F` | Full date and time (long) | Thursday, December 31, 2020 12:00:00 AM |
|
||||
| `R` | Relative time | 1 year ago |
|
||||
|
||||
## Contributing
|
||||
|
||||
<!--contributing(msg)-->
|
||||
|
||||
Contributions, issues, and feature requests are welcome!<br />
|
||||
Feel free to check the [issues](https://github.com/RocketChat/fuselage/issues).
|
||||
|
||||
<!--/contributing(msg)-->
|
||||
|
||||
Whenever you find a grammar-related bug, start by inserting the test case.
|
||||
|
||||
We are open to other tags/markups, as long as they don't generate unexpected behavior.
|
||||
|
||||
## Observations and known issues
|
||||
|
||||
- Nested lists are unsupported
|
||||
- `URL` rule doesn't allow whitespace, `(`, or `)`
|
||||
231
packages/message-parser/benchmarks/parser.bench.ts
Normal file
231
packages/message-parser/benchmarks/parser.bench.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env npx ts-node
|
||||
/**
|
||||
* Benchmark suite for @rocket.chat/message-parser
|
||||
*
|
||||
* Measures parsing performance (ops/sec) across various message categories.
|
||||
* Run with: `yarn bench` from packages/message-parser/
|
||||
*
|
||||
* Uses a custom loader (pegjs-register.js) to compile .pegjs at runtime — no build needed.
|
||||
*/
|
||||
|
||||
import { Bench, type Task } from 'tinybench';
|
||||
|
||||
import { parse } from '../src';
|
||||
import type { Options } from '../src';
|
||||
|
||||
// ── Options presets ────────────────────────────────────────────────────────
|
||||
|
||||
const fullOptions: Options = {
|
||||
colors: true,
|
||||
emoticons: true,
|
||||
katex: {
|
||||
dollarSyntax: true,
|
||||
parenthesisSyntax: true,
|
||||
},
|
||||
};
|
||||
|
||||
// ── Fixture type ───────────────────────────────────────────────────────────
|
||||
|
||||
type Fixture = {
|
||||
name: string;
|
||||
input: string;
|
||||
options?: Options;
|
||||
};
|
||||
|
||||
type BenchCategory = {
|
||||
name: string;
|
||||
time?: number;
|
||||
warmupTime?: number;
|
||||
fixtures: Fixture[];
|
||||
};
|
||||
|
||||
// ── Categories ─────────────────────────────────────────────────────────────
|
||||
|
||||
const categories: BenchCategory[] = [
|
||||
{
|
||||
name: 'Plain Text',
|
||||
fixtures: [
|
||||
{ name: 'short', input: 'Hello world' },
|
||||
{
|
||||
name: 'medium',
|
||||
input: 'The quick brown fox jumps over the lazy dog. This is a typical message one might send in a chat application.',
|
||||
},
|
||||
{ name: 'long', input: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20).trim() },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Emphasis / Formatting',
|
||||
fixtures: [
|
||||
{ name: 'bold', input: '**Hello world**' },
|
||||
{ name: 'italic', input: '_Hello world_' },
|
||||
{ name: 'strike', input: '~~Hello world~~' },
|
||||
{ name: 'nested', input: '**bold _italic_ and ~~strike~~**' },
|
||||
{ name: 'deep nesting', input: '**bold _italic ~~strike _deep italic_~~_**' },
|
||||
{ name: 'bold + italic mixed', input: '*Bold text* and _italic text_ in a message' },
|
||||
{ name: 'deeply nested', input: '*bold _italic ~~strike~~ italic_ bold*' },
|
||||
{ name: 'multiple', input: '**bold** normal _italic_ normal ~~strike~~ **more bold** _more italic_' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'URLs & Links',
|
||||
fixtures: [
|
||||
{ name: 'single', input: 'Check out https://rocket.chat for more info' },
|
||||
{ name: 'multiple', input: 'Visit https://rocket.chat or https://github.com/RocketChat/Rocket.Chat or https://open.rocket.chat' },
|
||||
{ name: 'markdown link', input: '[Rocket.Chat](https://rocket.chat)' },
|
||||
{ name: 'autolinked domain', input: 'Visit rocket.chat for more info' },
|
||||
{ name: 'with path', input: 'See https://github.com/RocketChat/Rocket.Chat/tree/develop/packages/message-parser for details' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Emoji',
|
||||
fixtures: [
|
||||
{ name: 'single shortcode', input: ':smile:', options: fullOptions },
|
||||
{ name: 'triple shortcode (BigEmoji)', input: ':smile::heart::rocket:', options: fullOptions },
|
||||
{ name: 'single unicode', input: '😀', options: fullOptions },
|
||||
{ name: 'triple unicode (BigEmoji)', input: '😀🚀🌈', options: fullOptions },
|
||||
{ name: 'in text', input: 'Hello :smile: world :heart: test :rocket: done', options: fullOptions },
|
||||
{ name: 'mixed', input: 'Great job :thumbsup: 🎉 keep going :rocket:', options: fullOptions },
|
||||
{ name: 'emoji heavy', input: ':smile: :wave: :rocket: :fire: :heart: :100:', options: fullOptions },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Mentions',
|
||||
fixtures: [
|
||||
{ name: 'single user', input: '@admin' },
|
||||
{ name: 'multiple users', input: '@admin @user1 @moderator' },
|
||||
{ name: 'channel', input: '#general' },
|
||||
{ name: 'mixed', input: 'Hey @admin check #general and @user1' },
|
||||
{ name: 'mentions (suggested)', input: 'Hey @john and @jane, check #general' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Code',
|
||||
fixtures: [
|
||||
{ name: 'inline', input: 'Use `console.log()` for debugging' },
|
||||
{ name: 'block', input: '```javascript\nconst x = 1;\nconsole.log(x);\n```' },
|
||||
{ name: 'multi inline', input: 'Use `Array.map()` and `Array.filter()` and `Array.reduce()`' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Structured Blocks',
|
||||
fixtures: [
|
||||
{ name: 'ordered list', input: '1. First item\n2. Second item\n3. Third item' },
|
||||
{ name: 'unordered list', input: '- First item\n- Second item\n- Third item' },
|
||||
{ name: 'task list', input: '- [x] Done task\n- [ ] Pending task\n- [x] Another done' },
|
||||
{ name: 'blockquote', input: '> This is a quoted message\n> with multiple lines' },
|
||||
{ name: 'heading', input: '# Hello World' },
|
||||
{ name: 'heading multi-level', input: '# H1\n## H2\n### H3\n#### H4' },
|
||||
{ name: 'spoiler', input: '||This is a spoiler||' },
|
||||
{ name: 'spoiler with formatting', input: '||**bold** and _italic_ spoiler||' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'KaTeX (Math)',
|
||||
fixtures: [
|
||||
{ name: 'inline', input: 'The formula is $E = mc^2$ in physics', options: fullOptions },
|
||||
{ name: 'block', input: '$$\\sum_{i=1}^{n} x_i = x_1 + x_2 + ... + x_n$$', options: fullOptions },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Adversarial / Stress',
|
||||
time: 2000,
|
||||
warmupTime: 500,
|
||||
fixtures: [
|
||||
{
|
||||
name: 'adversarial emphasis',
|
||||
input: '**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__',
|
||||
},
|
||||
{
|
||||
name: 'adversarial mixed',
|
||||
input:
|
||||
'This a message designed to stress test !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;\'\\[], numbers too 1234567890-= let it call s o s ok~',
|
||||
},
|
||||
{ name: 'repeated specials', input: '****____~~~~||||````####>>>>' },
|
||||
{
|
||||
name: 'long with formatting',
|
||||
input: '**bold** _italic_ ~~strike~~ `code` @user #channel :smile: https://example.com '.repeat(10).trim(),
|
||||
},
|
||||
{
|
||||
name: 'unmatched markers (pathological)',
|
||||
input: '*_~*_~*_~*_~*_~ hello world absolutely no closing markers anywhere at all',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Real-World Messages',
|
||||
fixtures: [
|
||||
{ name: 'simple', input: 'Hey team, the deploy is done ✅' },
|
||||
{
|
||||
name: 'medium',
|
||||
input:
|
||||
'@admin I pushed the fix to `develop` branch. Check https://github.com/RocketChat/Rocket.Chat/pull/12345 for details. :thumbsup:',
|
||||
},
|
||||
{
|
||||
name: 'complex',
|
||||
input:
|
||||
'**Release Notes v7.0**\n- [x] Fix #12345\n- [ ] Update docs\n\n> Important: check https://docs.rocket.chat\n\ncc @admin @devlead #releases :rocket:',
|
||||
options: fullOptions,
|
||||
},
|
||||
{
|
||||
name: 'realistic chat message',
|
||||
input: 'Hello @team, please review the *important* update:\n\n1. Run `yarn build`\n2. Check #deployments\n\n*Thanks!* :rocket:',
|
||||
options: fullOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Timestamps',
|
||||
fixtures: [{ name: 'unix format', input: '<t:1630360800:f>' }],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatResults(tasks: Task[]) {
|
||||
return tasks.map((task) => ({
|
||||
'Task': task.name,
|
||||
'ops/sec': Math.round(task.result?.hz ?? 0).toLocaleString(),
|
||||
'Avg (ms)': ((task.result?.mean ?? 0) * 1000).toFixed(4),
|
||||
'Min (ms)': ((task.result?.min ?? 0) * 1000).toFixed(4),
|
||||
'Max (ms)': ((task.result?.max ?? 0) * 1000).toFixed(4),
|
||||
'P99 (ms)': ((task.result?.p99 ?? 0) * 1000).toFixed(4),
|
||||
'Samples': task.result?.samples?.length ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Runner ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function run() {
|
||||
console.log('='.repeat(72));
|
||||
console.log(' @rocket.chat/message-parser — Performance Benchmark Suite');
|
||||
console.log('='.repeat(72));
|
||||
console.log();
|
||||
|
||||
// Benchmarks must run sequentially to avoid interference
|
||||
|
||||
for (const category of categories) {
|
||||
const bench = new Bench({
|
||||
time: category.time ?? 1000,
|
||||
warmupTime: category.warmupTime ?? 200,
|
||||
});
|
||||
|
||||
for (const fixture of category.fixtures) {
|
||||
bench.add(fixture.name, () => parse(fixture.input, fixture.options));
|
||||
}
|
||||
|
||||
await bench.run();
|
||||
|
||||
console.log(`── ${category.name} ${'─'.repeat(Math.max(0, 56 - category.name.length))}`);
|
||||
console.table(formatResults(bench.tasks));
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('='.repeat(72));
|
||||
console.log(' Done.');
|
||||
console.log('='.repeat(72));
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
18
packages/message-parser/jest.config.ts
Normal file
18
packages/message-parser/jest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import server from '@rocket.chat/jest-presets/server';
|
||||
import type { Config } from 'jest';
|
||||
|
||||
// Jest 30 loads this config via Node's native type stripping (Node 22.18+),
|
||||
// which treats the file as ESM where __dirname is not defined as a global.
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default {
|
||||
preset: server.preset,
|
||||
transform: {
|
||||
'\\.pegjs$': resolve(__dirname, './loaders/pegtransform.js'),
|
||||
},
|
||||
moduleFileExtensions: ['js', 'ts', 'pegjs'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '\\.bench\\.ts$'],
|
||||
} satisfies Config;
|
||||
13
packages/message-parser/loaders/pegjs-register.js
Normal file
13
packages/message-parser/loaders/pegjs-register.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const fs = require('fs');
|
||||
const Module = require('module');
|
||||
|
||||
const peggy = require('peggy');
|
||||
|
||||
Module._extensions['.pegjs'] = function (mod, filename) {
|
||||
const content = fs.readFileSync(filename, 'utf-8');
|
||||
const code = peggy.generate(content, {
|
||||
output: 'source',
|
||||
format: 'commonjs',
|
||||
});
|
||||
mod._compile(code, filename);
|
||||
};
|
||||
10
packages/message-parser/loaders/pegtransform.js
Normal file
10
packages/message-parser/loaders/pegtransform.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const pegjs = require('peggy');
|
||||
|
||||
module.exports = {
|
||||
process: (content) => ({
|
||||
code: pegjs.generate(content, {
|
||||
output: 'source',
|
||||
format: 'commonjs',
|
||||
}),
|
||||
}),
|
||||
};
|
||||
72
packages/message-parser/package.json
Normal file
72
packages/message-parser/package.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "@rocket.chat/message-parser",
|
||||
"version": "0.32.0",
|
||||
"description": "Rocket.Chat parser for messages",
|
||||
"homepage": "https://github.com/RocketChat/Rocket.Chat#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/RocketChat/Rocket.Chat/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/RocketChat/Rocket.Chat.git",
|
||||
"directory": "packages/message-parser"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Rocket.Chat",
|
||||
"url": "https://rocket.chat/"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./dist/messageParser.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"main": "dist/messageParser.js",
|
||||
"module": "dist/messageParser.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"scripts": {
|
||||
".:build:bundle": "webpack-cli",
|
||||
".:build:clean": "rimraf dist",
|
||||
"build": "run-s .:build:clean .:build:bundle",
|
||||
"bench": "ts-node --compiler-options '{\"module\":\"commonjs\"}' -r ./loaders/pegjs-register.js benchmarks/parser.bench.ts",
|
||||
"bench:size": "node -e \"const fs=require('fs'),zlib=require('zlib'),p='dist/messageParser.js';try{const s=fs.statSync(p),c=fs.readFileSync(p),g=zlib.gzipSync(c);console.log('Bundle:',p);console.log('Raw:',s.size,'bytes ('+(s.size/1024).toFixed(1),'KB)');console.log('Gzip:',g.length,'bytes ('+(g.length/1024).toFixed(1),'KB)')}catch(e){console.error('Build first: yarn build');process.exit(1)}\"",
|
||||
"lint": "eslint .",
|
||||
"test": "jest",
|
||||
"testunit": "jest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"tldts": "~6.1.86"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rocket.chat/jest-presets": "workspace:~",
|
||||
"@rocket.chat/peggy-loader": "workspace:~",
|
||||
"@types/jest": "~30.0.0",
|
||||
"@types/node": "~24.13.2",
|
||||
"eslint": "~9.39.5",
|
||||
"fast-check": "^4.6.0",
|
||||
"jest": "~30.2.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"peggy": "4.1.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"tinybench": "^3.0.7",
|
||||
"ts-loader": "~9.5.7",
|
||||
"ts-node": "~10.9.2",
|
||||
"typescript": "~5.9.3",
|
||||
"webpack": "~5.104.1",
|
||||
"webpack-cli": "~5.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.22.3 <23 || >=24.15.0 <25"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
132
packages/message-parser/src/BlockSplitter.ts
Normal file
132
packages/message-parser/src/BlockSplitter.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
export enum BlockType {
|
||||
PARAGRAPH = 'PARAGRAPH',
|
||||
HEADING = 'HEADING',
|
||||
CODE = 'CODE',
|
||||
LIST = 'LIST',
|
||||
QUOTE = 'QUOTE',
|
||||
}
|
||||
|
||||
export type Block = {
|
||||
type: BlockType;
|
||||
content: string;
|
||||
level?: number;
|
||||
language?: string;
|
||||
ordered?: boolean;
|
||||
incomplete?: boolean;
|
||||
};
|
||||
|
||||
export class BlockSplitter {
|
||||
public static split(input: string): Block[] {
|
||||
const lines = input.split(/\r?\n/);
|
||||
const blocks: Block[] = [];
|
||||
let currentBlock: Block | null = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (headingMatch) {
|
||||
this.flush(blocks, currentBlock);
|
||||
currentBlock = {
|
||||
type: BlockType.HEADING,
|
||||
content: headingMatch[2],
|
||||
level: headingMatch[1].length,
|
||||
};
|
||||
this.flush(blocks, currentBlock);
|
||||
currentBlock = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('```')) {
|
||||
this.flush(blocks, currentBlock);
|
||||
const language = line.slice(3).trim();
|
||||
const codeLines = [];
|
||||
let closed = false;
|
||||
i++;
|
||||
while (i < lines.length && !lines[i].startsWith('```')) {
|
||||
codeLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
if (i < lines.length) {
|
||||
closed = true;
|
||||
}
|
||||
blocks.push({
|
||||
type: BlockType.CODE,
|
||||
content: codeLines.join('\n'),
|
||||
language,
|
||||
incomplete: !closed,
|
||||
});
|
||||
currentBlock = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.trim() === '') {
|
||||
if (currentBlock?.type === BlockType.LIST && /^\s+$/.test(line)) {
|
||||
currentBlock.content += `\n${line}`;
|
||||
continue;
|
||||
}
|
||||
this.flush(blocks, currentBlock);
|
||||
currentBlock = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s+(.+)$/);
|
||||
const isIndented = /^\s+/.test(line);
|
||||
|
||||
if (listMatch) {
|
||||
const isOrdered = /^\d+\./.test(listMatch[2]);
|
||||
if (currentBlock?.type !== BlockType.LIST) {
|
||||
this.flush(blocks, currentBlock);
|
||||
currentBlock = {
|
||||
type: BlockType.LIST,
|
||||
content: line,
|
||||
ordered: isOrdered,
|
||||
};
|
||||
} else {
|
||||
if (currentBlock.ordered !== undefined && currentBlock.ordered !== isOrdered) {
|
||||
currentBlock.ordered = undefined;
|
||||
}
|
||||
currentBlock.content += `\n${line}`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isIndented && currentBlock?.type === BlockType.LIST) {
|
||||
currentBlock.content += `\n${line}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('>')) {
|
||||
if (currentBlock?.type !== BlockType.QUOTE) {
|
||||
this.flush(blocks, currentBlock);
|
||||
currentBlock = {
|
||||
type: BlockType.QUOTE,
|
||||
content: line,
|
||||
};
|
||||
} else {
|
||||
currentBlock.content += `\n${line}`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentBlock?.type !== BlockType.PARAGRAPH) {
|
||||
this.flush(blocks, currentBlock);
|
||||
currentBlock = {
|
||||
type: BlockType.PARAGRAPH,
|
||||
content: line,
|
||||
};
|
||||
} else {
|
||||
currentBlock.content += `\n${line}`;
|
||||
}
|
||||
}
|
||||
|
||||
this.flush(blocks, currentBlock);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static flush(blocks: Block[], block: Block | null) {
|
||||
if (block) {
|
||||
blocks.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
118
packages/message-parser/src/asciiList.ts
Normal file
118
packages/message-parser/src/asciiList.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// Ported from emojione's asciiList (same mapping the legacy renderer used)
|
||||
export const asciiList: Record<string, string> = {
|
||||
'*\\0/*': '🙆',
|
||||
'*\\O/*': '🙆',
|
||||
'-___-': '😑',
|
||||
":'-)": '😂',
|
||||
"':-)": '😅',
|
||||
"':-D": '😅',
|
||||
'>:-)': '😆',
|
||||
"':-(": '😓',
|
||||
'>:-(': '😠',
|
||||
":'-(": '😢',
|
||||
'O:-)': '😇',
|
||||
'0:-3': '😇',
|
||||
'0:-)': '😇',
|
||||
'0;^)': '😇',
|
||||
'O;-)': '😇',
|
||||
'0;-)': '😇',
|
||||
'O:-3': '😇',
|
||||
'-__-': '😑',
|
||||
':-Þ': '😛',
|
||||
'</3': '💔',
|
||||
":')": '😂',
|
||||
':-D': '😃',
|
||||
"':)": '😅',
|
||||
"'=)": '😅',
|
||||
"':D": '😅',
|
||||
"'=D": '😅',
|
||||
'>:)': '😆',
|
||||
'>;)': '😆',
|
||||
'>=)': '😆',
|
||||
';-)': '😉',
|
||||
'*-)': '😉',
|
||||
';-]': '😉',
|
||||
';^)': '😉',
|
||||
"':(": '😓',
|
||||
"'=(": '😓',
|
||||
':-*': '😘',
|
||||
':^*': '😘',
|
||||
'>:P': '😜',
|
||||
'X-P': '😜',
|
||||
'>:[': '😞',
|
||||
':-(': '😞',
|
||||
':-[': '😞',
|
||||
'>:(': '😠',
|
||||
":'(": '😢',
|
||||
';-(': '😢',
|
||||
'>.<': '😣',
|
||||
'#-)': '😵',
|
||||
'%-)': '😵',
|
||||
'X-)': '😵',
|
||||
'\\0/': '🙆',
|
||||
'\\O/': '🙆',
|
||||
'0:3': '😇',
|
||||
'0:)': '😇',
|
||||
'O:)': '😇',
|
||||
'O=)': '😇',
|
||||
'O:3': '😇',
|
||||
'B-)': '😎',
|
||||
'8-)': '😎',
|
||||
'B-D': '😎',
|
||||
'8-D': '😎',
|
||||
'-_-': '😑',
|
||||
'>:\\': '😕',
|
||||
'>:/': '😕',
|
||||
':-/': '😕',
|
||||
':-.': '😕',
|
||||
':-P': '😛',
|
||||
':Þ': '😛',
|
||||
':-b': '😛',
|
||||
':-O': '😮',
|
||||
'O_O': '😮',
|
||||
'>:O': '😮',
|
||||
':-X': '😶',
|
||||
':-#': '😶',
|
||||
':-)': '🙂',
|
||||
'(y)': '👍',
|
||||
'<3': '❤',
|
||||
'=D': '😃',
|
||||
';)': '😉',
|
||||
'*)': '😉',
|
||||
';]': '😉',
|
||||
';D': '😉',
|
||||
':*': '😘',
|
||||
'=*': '😘',
|
||||
':(': '😞',
|
||||
':[': '😞',
|
||||
'=(': '😞',
|
||||
':@': '😠',
|
||||
';(': '😢',
|
||||
'D:': '😨',
|
||||
':$': '😳',
|
||||
'=$': '😳',
|
||||
'#)': '😵',
|
||||
'%)': '😵',
|
||||
'X)': '😵',
|
||||
'B)': '😎',
|
||||
'8)': '😎',
|
||||
':/': '😕',
|
||||
':\\': '😕',
|
||||
'=/': '😕',
|
||||
'=\\': '😕',
|
||||
':L': '😕',
|
||||
'=L': '😕',
|
||||
':P': '😛',
|
||||
'=P': '😛',
|
||||
':b': '😛',
|
||||
':O': '😮',
|
||||
':X': '😶',
|
||||
':#': '😶',
|
||||
'=X': '😶',
|
||||
'=#': '😶',
|
||||
':)': '🙂',
|
||||
'=]': '🙂',
|
||||
'=)': '🙂',
|
||||
':]': '🙂',
|
||||
':D': '😄',
|
||||
};
|
||||
294
packages/message-parser/src/definitions.ts
Normal file
294
packages/message-parser/src/definitions.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
export type Blockquote = {
|
||||
type: 'BLOCKQUOTE';
|
||||
value: Paragraph[];
|
||||
};
|
||||
|
||||
export type OrderedList = {
|
||||
type: 'ORDERED_LIST';
|
||||
value: ListItem[];
|
||||
};
|
||||
|
||||
export type UnorderedList = {
|
||||
type: 'UNORDERED_LIST';
|
||||
value: ListItem[];
|
||||
};
|
||||
|
||||
export type ListItem = {
|
||||
type: 'LIST_ITEM';
|
||||
value: Inlines[];
|
||||
number?: number;
|
||||
};
|
||||
|
||||
export type Tasks = {
|
||||
type: 'TASKS';
|
||||
value: Task[];
|
||||
};
|
||||
export type Task = {
|
||||
type: 'TASK';
|
||||
status: boolean;
|
||||
value: Inlines[];
|
||||
};
|
||||
|
||||
export type CodeLine = {
|
||||
type: 'CODE_LINE';
|
||||
value: Plain;
|
||||
};
|
||||
|
||||
export type TableCellAlignment = 'left' | 'center' | 'right' | undefined;
|
||||
|
||||
export type TableCell = {
|
||||
type: 'TABLE_CELL';
|
||||
align: TableCellAlignment;
|
||||
value: Inlines[];
|
||||
};
|
||||
|
||||
export type TableRow = {
|
||||
type: 'TABLE_ROW';
|
||||
value: TableCell[];
|
||||
};
|
||||
|
||||
export type Table = {
|
||||
type: 'TABLE';
|
||||
value: {
|
||||
header: TableCell[];
|
||||
rows: TableRow[];
|
||||
};
|
||||
// New form is a `[start, end]` offset span; the `Plain` form is kept in the
|
||||
// type only to tolerate previously-persisted data at runtime.
|
||||
fallback?: SourceRange | Plain;
|
||||
};
|
||||
|
||||
export type Color = {
|
||||
type: 'COLOR';
|
||||
value: {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type BigEmoji = {
|
||||
type: 'BIG_EMOJI';
|
||||
value: [Emoji] | [Emoji, Emoji] | [Emoji, Emoji, Emoji];
|
||||
};
|
||||
|
||||
export type Emoji =
|
||||
| {
|
||||
type: 'EMOJI';
|
||||
value: Plain;
|
||||
shortCode: string;
|
||||
}
|
||||
| {
|
||||
type: 'EMOJI';
|
||||
value: undefined;
|
||||
unicode: string;
|
||||
};
|
||||
|
||||
export type Code = {
|
||||
type: 'CODE';
|
||||
language: string | undefined;
|
||||
value: CodeLine[];
|
||||
};
|
||||
|
||||
export type InlineCode = {
|
||||
type: 'INLINE_CODE';
|
||||
value: Plain;
|
||||
};
|
||||
|
||||
export type Heading = {
|
||||
type: 'HEADING';
|
||||
level: 1 | 2 | 3 | 4;
|
||||
value: Inlines[];
|
||||
};
|
||||
|
||||
export type Quote = {
|
||||
type: 'QUOTE';
|
||||
value: Paragraph[];
|
||||
};
|
||||
|
||||
export type Spoiler = {
|
||||
type: 'SPOILER';
|
||||
value: Array<
|
||||
Link | Emoji | UserMention | ChannelMention | InlineCode | Italic | Bold | Strike | Timestamp | Color | Image | InlineKaTeX | Plain
|
||||
>;
|
||||
};
|
||||
|
||||
export type SpoilerBlock = {
|
||||
type: 'SPOILER_BLOCK';
|
||||
value: Paragraph[];
|
||||
};
|
||||
|
||||
export type Markup = Italic | Strike | Bold | Plain | ChannelMention;
|
||||
export type MarkupExcluding<T extends Markup> = Exclude<Markup, T>;
|
||||
|
||||
export type Bold = {
|
||||
type: 'BOLD';
|
||||
value: Array<MarkupExcluding<Bold> | Link | Emoji | UserMention | ChannelMention | InlineCode>;
|
||||
};
|
||||
|
||||
export type Italic = {
|
||||
type: 'ITALIC';
|
||||
value: Array<MarkupExcluding<Italic> | Link | Emoji | UserMention | ChannelMention | InlineCode>;
|
||||
};
|
||||
|
||||
export type Strike = {
|
||||
type: 'STRIKE';
|
||||
value: Array<MarkupExcluding<Strike> | Link | Emoji | UserMention | ChannelMention | InlineCode | Italic | Timestamp>;
|
||||
};
|
||||
|
||||
export type Plain = {
|
||||
type: 'PLAIN_TEXT';
|
||||
value: string;
|
||||
};
|
||||
|
||||
// [start, end] offsets into the original parsed source. Consumers that don't
|
||||
// implement a renderer for a node can slice the source to show the raw markup,
|
||||
// without duplicating the text into the AST.
|
||||
export type SourceRange = [number, number];
|
||||
|
||||
export type LineBreak = {
|
||||
type: 'LINE_BREAK';
|
||||
value: undefined;
|
||||
};
|
||||
|
||||
export type HorizontalRule = {
|
||||
type: 'HORIZONTAL_RULE';
|
||||
value: undefined;
|
||||
// New form is a `[start, end]` offset span; the `Plain` form is kept in the
|
||||
// type only to tolerate previously-persisted data at runtime.
|
||||
fallback?: SourceRange | Plain;
|
||||
};
|
||||
|
||||
export type KaTeX = {
|
||||
type: 'KATEX';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type InlineKaTeX = {
|
||||
type: 'INLINE_KATEX';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type Paragraph = {
|
||||
type: 'PARAGRAPH';
|
||||
value: Array<Exclude<Inlines, Paragraph>>;
|
||||
};
|
||||
|
||||
export type Image = {
|
||||
type: 'IMAGE';
|
||||
value: {
|
||||
src: Plain;
|
||||
label: Markup;
|
||||
};
|
||||
};
|
||||
|
||||
export type Link = {
|
||||
type: 'LINK';
|
||||
value: {
|
||||
src: Plain;
|
||||
label: Markup | Markup[];
|
||||
};
|
||||
};
|
||||
|
||||
export type UserMention = {
|
||||
type: 'MENTION_USER';
|
||||
value: Plain;
|
||||
};
|
||||
|
||||
export type ChannelMention = {
|
||||
type: 'MENTION_CHANNEL';
|
||||
value: Plain;
|
||||
};
|
||||
|
||||
export type Timestamp = {
|
||||
type: 'TIMESTAMP';
|
||||
value: {
|
||||
timestamp: string;
|
||||
format: 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R';
|
||||
};
|
||||
// New form is a `[start, end]` offset span; the `Plain` form is kept in the
|
||||
// type only to tolerate previously-persisted data at runtime.
|
||||
fallback?: SourceRange | Plain;
|
||||
};
|
||||
|
||||
export type Types = {
|
||||
BOLD: Bold;
|
||||
SPOILER: Spoiler;
|
||||
PARAGRAPH: Paragraph;
|
||||
PLAIN_TEXT: Plain;
|
||||
ITALIC: Italic;
|
||||
STRIKE: Strike;
|
||||
CODE: Code;
|
||||
CODE_LINE: CodeLine;
|
||||
INLINE_CODE: InlineCode;
|
||||
HEADING: Heading;
|
||||
QUOTE: Quote;
|
||||
LINK: Link;
|
||||
MENTION_USER: UserMention;
|
||||
MENTION_CHANNEL: ChannelMention;
|
||||
EMOJI: Emoji;
|
||||
BIG_EMOJI: BigEmoji;
|
||||
COLOR: Color;
|
||||
TASKS: Tasks;
|
||||
TASK: Task;
|
||||
UNORDERED_LIST: UnorderedList;
|
||||
ORDERED_LIST: OrderedList;
|
||||
LIST_ITEM: ListItem;
|
||||
IMAGE: Image;
|
||||
LINE_BREAK: LineBreak;
|
||||
HORIZONTAL_RULE: HorizontalRule;
|
||||
KATEX: KaTeX;
|
||||
INLINE_KATEX: InlineKaTeX;
|
||||
TIMESTAMP: Timestamp;
|
||||
SPOILER_BLOCK: SpoilerBlock;
|
||||
TABLE: Table;
|
||||
TABLE_ROW: TableRow;
|
||||
TABLE_CELL: TableCell;
|
||||
};
|
||||
|
||||
export type ASTNode =
|
||||
| BigEmoji
|
||||
| Bold
|
||||
| Spoiler
|
||||
| Paragraph
|
||||
| Plain
|
||||
| Italic
|
||||
| Strike
|
||||
| Code
|
||||
| CodeLine
|
||||
| InlineCode
|
||||
| Heading
|
||||
| Quote
|
||||
| SpoilerBlock
|
||||
| Link
|
||||
| UserMention
|
||||
| ChannelMention
|
||||
| Emoji
|
||||
| Color
|
||||
| Tasks
|
||||
| HorizontalRule
|
||||
| Table;
|
||||
|
||||
export type TypesKeys = keyof Types;
|
||||
|
||||
export type Inlines =
|
||||
| Timestamp
|
||||
| Bold
|
||||
| Spoiler
|
||||
| Plain
|
||||
| Italic
|
||||
| Strike
|
||||
| InlineCode
|
||||
| Image
|
||||
| Link
|
||||
| UserMention
|
||||
| ChannelMention
|
||||
| Emoji
|
||||
| Color
|
||||
| InlineKaTeX;
|
||||
|
||||
export type Blocks =
|
||||
Code | Heading | Quote | SpoilerBlock | ListItem | Tasks | OrderedList | UnorderedList | LineBreak | KaTeX | HorizontalRule | Table;
|
||||
|
||||
export type Root = Array<Paragraph | Blocks> | [BigEmoji];
|
||||
938
packages/message-parser/src/grammar.pegjs
Normal file
938
packages/message-parser/src/grammar.pegjs
Normal file
@@ -0,0 +1,938 @@
|
||||
{{
|
||||
const {
|
||||
autoEmail,
|
||||
autoLink,
|
||||
bigEmoji,
|
||||
bold,
|
||||
code,
|
||||
codeLine,
|
||||
color,
|
||||
emoji,
|
||||
emojiUnicode,
|
||||
emoticon,
|
||||
heading,
|
||||
image,
|
||||
inlineCode,
|
||||
inlineKatex,
|
||||
horizontalRule,
|
||||
italic,
|
||||
katex,
|
||||
lineBreak,
|
||||
link,
|
||||
listItem,
|
||||
mentionChannel,
|
||||
mentionUser,
|
||||
orderedList,
|
||||
paragraph,
|
||||
phoneChecker,
|
||||
plain,
|
||||
quote,
|
||||
reducePlainTexts,
|
||||
spoiler,
|
||||
spoilerBlock,
|
||||
strike,
|
||||
table,
|
||||
task,
|
||||
tasks,
|
||||
unorderedList,
|
||||
timestamp,
|
||||
timestampFromHours,
|
||||
timestampFromIsoTime,
|
||||
} = require('./utils');
|
||||
|
||||
let skipBold = false;
|
||||
let skipItalic = false;
|
||||
let skipStrikethrough = false;
|
||||
let skipReferences = false;
|
||||
}}
|
||||
|
||||
Start
|
||||
= @BigEmoji !.
|
||||
/ (Blocks / Paragraph / EndOfLine { return paragraph([plain('')]); })+
|
||||
|
||||
/**
|
||||
*
|
||||
* Blocks
|
||||
*
|
||||
*/
|
||||
Blocks
|
||||
= Blockquote
|
||||
/ BlockSpoiler
|
||||
/ Code
|
||||
/ HorizontalRule
|
||||
/ Table
|
||||
/ Heading
|
||||
/ Tasks
|
||||
/ OrderedList
|
||||
/ UnorderedList
|
||||
/ Katex
|
||||
/ LineBreak
|
||||
|
||||
/**
|
||||
*
|
||||
* Blockquote
|
||||
* e.g: > This is a blockquote
|
||||
*
|
||||
*/
|
||||
Blockquote = b:BlockquoteLine+ { return quote(b); }
|
||||
|
||||
BlockquoteLine
|
||||
= ">" [ \t]* EndOfLine { return paragraph([plain('')]); }
|
||||
/ ">" [ \t]* @Paragraph
|
||||
|
||||
/**
|
||||
* Block Spoiler
|
||||
* e.g:
|
||||
* ||
|
||||
* line one
|
||||
* line two
|
||||
* ||
|
||||
*/
|
||||
BlockSpoiler = "||" EndOfLine first:(&(! "||") @Paragraph) rest:(&(! "||") @Paragraph)* EndOfLine? "||" { return spoilerBlock([first, ...rest]); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Table (GFM)
|
||||
* e.g:
|
||||
* | Header 1 | Header 2 |
|
||||
* | -------- | :------: |
|
||||
* | Cell 1 | Cell 2 |
|
||||
*
|
||||
* v1 requires a leading and trailing pipe on every row. Alignment comes from
|
||||
* the delimiter row: `:---` left, `:--:` center, `---:` right, `---` none.
|
||||
* A literal pipe inside a cell must be escaped as `\|`.
|
||||
*/
|
||||
Table = header:TableRowLine aligns:TableDelimiterRow body:TableRowLine* { return table(header, aligns, body, [range().start, range().end]); }
|
||||
|
||||
TableRowLine = "|" cells:(@TableCell "|")+ EndOfLine? { return cells; }
|
||||
|
||||
TableCell = items:TableCellItem* { return reducePlainTexts(items); }
|
||||
|
||||
TableCellItem
|
||||
= "\\|" { return plain('|'); }
|
||||
/ !"|" !EndOfLine @(InlineItemPattern / Any)
|
||||
|
||||
TableDelimiterRow = "|" aligns:(@TableDelimiterCell "|")+ EndOfLine? { return aligns; }
|
||||
|
||||
TableDelimiterCell = [ \t]* left:":"? "-"+ right:":"? [ \t]* {
|
||||
if (left && right) { return 'center'; }
|
||||
if (right) { return 'right'; }
|
||||
if (left) { return 'left'; }
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// <t:1630360800:?{format}>
|
||||
// <t:2025-07-22T10:00:00.000Z?:?{format}>
|
||||
// <t:2025-07-22T10:00:00:?{format}>
|
||||
// <t:00:00:?{format}>
|
||||
|
||||
TimestampType = "t" / "T" / "d" / "D" / "f" / "F" / "R"
|
||||
|
||||
Unixtime = $(Digit |10|)
|
||||
|
||||
TimestampHoursMinutesSeconds = hours:$(Digit |2|) ":" minutes:$(Digit |2|) ":" seconds:$(Digit |2|) tz:Timezone? { return timestampFromHours(hours, minutes, seconds, tz); }
|
||||
|
||||
TimestampHoursMinutes = hours:$(Digit |2|) ":" minutes:$(Digit |2|) tz:Timezone? { return timestampFromHours(hours, minutes, undefined, tz); }
|
||||
|
||||
|
||||
Timestamp = TimestampHoursMinutesSeconds / TimestampHoursMinutes
|
||||
|
||||
Timezone = offset:('+'/'-') tzHour:$(Digit |2|) ":" tzMinute:$(Digit |2|) { return offset + tzHour + ':' + tzMinute; }
|
||||
|
||||
ISO8601Date = year:$(Digit |4|) "-" month:$(Digit |2|) "-" day:$(Digit |2|) "T" hours:$(Digit |2|) ":" minutes:$(Digit |2|) ":" seconds:$(Digit |2|) "." milliseconds:$(Digit |3|) tz:Timezone? { return timestampFromIsoTime({ year, month, day, hours, minutes, seconds, milliseconds, timezone: tz }); }
|
||||
|
||||
ISO8601DateWithoutMilliseconds = year:$(Digit |4|) "-" month:$(Digit |2|) "-" day:$(Digit |2|) "T" hours:$(Digit |2|) ":" minutes:$(Digit |2|) ":" seconds:$(Digit |2|) tz:Timezone? { return timestampFromIsoTime({ year, month, day, hours, minutes, seconds, timezone: tz }); }
|
||||
|
||||
|
||||
TimestampRules = "<t:" date:(Unixtime / ISO8601Date / ISO8601DateWithoutMilliseconds / Timestamp) ":" format:TimestampType ">" { return timestamp(date, format, [range().start, range().end]); } / "<t:" date:(Unixtime / ISO8601Date / ISO8601DateWithoutMilliseconds / Timestamp) ">" { return timestamp(date, undefined, [range().start, range().end]); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Code Chunk
|
||||
* e.g:
|
||||
* ```js
|
||||
* console.log('hello world');
|
||||
* ```
|
||||
*/
|
||||
Code = "```" language:CodeLanguage? EndOfLine lines:CodeLine+ EndOfLine "```" { return code(lines, language); }
|
||||
|
||||
CodeLanguage = $[a-zA-Z0-9 \_\-.]+
|
||||
|
||||
CodeLine
|
||||
= chunk:CodeChunk { return codeLine(chunk); }
|
||||
/ "\n" chunk:CodeChunk { return codeLine(chunk); }
|
||||
/ "\n" !"```" { return codeLine(plain('')); }
|
||||
|
||||
// Charclass avoids per-char lookahead; never consume start of "```".
|
||||
// Trailing 1-2 backticks before a line end (or EOF) are content, not a fence.
|
||||
CodeChunkChar = [^\r\n`] / "`" [^`\r\n] / "`" "`" [^`\r\n] / "`" "`" &("\r" / "\n" / !.) / "`" &("\r" / "\n" / !.)
|
||||
CodeChunk = text:$(CodeChunkChar)+ { return plain(text); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Heading: h1, h2, h3, h4
|
||||
* e.g:
|
||||
* # Heading 1
|
||||
* ## Heading 2
|
||||
* ### Heading 3
|
||||
* #### Heading 4
|
||||
*
|
||||
*/
|
||||
Heading = count:HeadingStart [ \t]+ text:HeadingChunk EndOfLine? { return heading(text, count); }
|
||||
|
||||
HeadingStart = value:"#" |1..4| { return value.length; }
|
||||
|
||||
HeadingChunk = items:HeadingInlineItem+ { return reducePlainTexts(items); }
|
||||
|
||||
HeadingInlineItem = InlineItemPattern / !EndOfLine @Any
|
||||
|
||||
/**
|
||||
*
|
||||
* Tasks
|
||||
* e.g:
|
||||
* - [x] Task One (checked)
|
||||
* - [ ] Task two
|
||||
* - [x] Task three (checked)
|
||||
*
|
||||
*/
|
||||
Tasks = items:Task+ { return tasks(items); }
|
||||
|
||||
Task = "- [" flag:TaskFlag "]" [ \t]+ text:Inline { return task(text, flag); }
|
||||
|
||||
TaskFlag = "x" { return true; } / " " { return false; }
|
||||
|
||||
/**
|
||||
*
|
||||
* Ordered List
|
||||
* e.g:
|
||||
* 1. Item One
|
||||
* 2. Item Two
|
||||
* 3. Item Three
|
||||
*
|
||||
*/
|
||||
OrderedList = items:OrderedListItem+ { return orderedList(items); }
|
||||
|
||||
OrderedListItem = number:Digits "." [ \t]+ text:Inline { return listItem(text, parseInt(number, 10)); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Unordered List
|
||||
* e.g:
|
||||
* - Item One
|
||||
* - Item Two
|
||||
* * Item Three
|
||||
* * Item Four
|
||||
*
|
||||
*/
|
||||
UnorderedList = items:(UnorderedListHyphenItem+ / UnorderedListAsteriskItem+) { return unorderedList(items); }
|
||||
|
||||
UnorderedListHyphenItem = "-" [ \t]+ text:Inline { return listItem(text); }
|
||||
|
||||
UnorderedListAsteriskItem = "*" [ \t]+ text:UnorderedListItemContent { return listItem(text); }
|
||||
|
||||
UnorderedListItemContent = value:UnorderedListItemContentItem+ !"*" EndOfLine? { return reducePlainTexts(value); }
|
||||
|
||||
UnorderedListItemContentItem = InlineItemPattern / !"*" @Any
|
||||
|
||||
/**
|
||||
*
|
||||
* KaTex
|
||||
* e.g: \[ KATEX_HERE \] OR $$ KATEX_HERE $$
|
||||
* $$x = \begin{cases}
|
||||
* a &\text{if } b \\
|
||||
* c &\text{if } d
|
||||
* \end{cases}$$
|
||||
*
|
||||
*/
|
||||
Katex = KatexStart content:$([^$\\] / !KatexEnd .)* KatexEnd { return katex(content); }
|
||||
|
||||
KatexStart
|
||||
= & { return options.katex?.parenthesisSyntax; } "\\["
|
||||
/ & { return options.katex?.dollarSyntax; } "$$"
|
||||
|
||||
KatexEnd
|
||||
= & { return options.katex?.parenthesisSyntax; } "\\]"
|
||||
/ & { return options.katex?.dollarSyntax; } "$$"
|
||||
|
||||
KatexInline
|
||||
= KatexInlineStart content:$([^$\\\r\n] / !KatexInlineEnd .)* KatexInlineEnd {
|
||||
return inlineKatex(content);
|
||||
}
|
||||
|
||||
KatexInlineStart
|
||||
= & { return options.katex?.parenthesisSyntax; } "\\("
|
||||
/ & { return options.katex?.dollarSyntax; } "$"
|
||||
|
||||
KatexInlineEnd
|
||||
= & { return options.katex?.parenthesisSyntax; } "\\)"
|
||||
/ & { return options.katex?.dollarSyntax; } "$"
|
||||
|
||||
/**
|
||||
*
|
||||
* LineBreak
|
||||
* e.g: \n
|
||||
*
|
||||
*/
|
||||
LineBreak = Space* EndOfLine { return lineBreak(); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Horizontal Rule (thematic break)
|
||||
* e.g: ---, ----------
|
||||
*
|
||||
* A line made up of 3+ contiguous dashes, with nothing else on the line
|
||||
* (leading/trailing spaces allowed). Only `-` is accepted: CommonMark also
|
||||
* allows `*` and `_`, but those collide with emphasis and with censored words
|
||||
* (bad-words masks a term as a run of `*`), so a bare `***` / `_______` line
|
||||
* stays as text/emphasis instead of turning into a divider.
|
||||
*
|
||||
*/
|
||||
HorizontalRule = [ \t]* loc:HorizontalRuleMarkers [ \t]* (EndOfLine / !.) { return horizontalRule(loc); }
|
||||
|
||||
HorizontalRuleMarkers
|
||||
= "-" |3..| { return [range().start, range().end]; }
|
||||
|
||||
/**
|
||||
*
|
||||
* Paragraph
|
||||
* e.g: This is a paragraph
|
||||
*/
|
||||
Paragraph = value:Inline { return paragraph(value); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Inline
|
||||
*
|
||||
*/
|
||||
Inline = value:(InlineItemPattern / Any)+ EndOfLine? { return reducePlainTexts(value); }
|
||||
|
||||
InlineEmoji = emo:Emoji { return emo; }
|
||||
|
||||
InlineEmoticon = emo:Emoticon & (EmoticonNeighbor / InlineItemPattern) { return emo; }
|
||||
|
||||
// Match "-" only when "-_-" is followed by more (so "-_-italic" → plain+italic); don't match when "-_-" is the full emoticon
|
||||
PlainRunBeforeEmoticon = "-" &("_" "-" .) { return plain('-'); }
|
||||
|
||||
PlainRun = run:$[^*_~`:\n<\[\]! \t()\\|]+ { return plain(run); }
|
||||
|
||||
EscapedTimestampRules
|
||||
= "\\" "<t:" rawDate:$(Unixtime / ISO8601Date / ISO8601DateWithoutMilliseconds / Timestamp) ":" format:TimestampType ">" {
|
||||
return plain(`<t:${rawDate}:${format}>`);
|
||||
}
|
||||
/ "\\" "<t:" rawDate:$(Unixtime / ISO8601Date / ISO8601DateWithoutMilliseconds / Timestamp) ">" {
|
||||
return plain(`<t:${rawDate}>`);
|
||||
}
|
||||
|
||||
// First-character dispatch: skip the full alternative chain by routing
|
||||
// each character to only the rules that can start with it.
|
||||
InlineItemPattern
|
||||
= & [ \t] @Whitespace
|
||||
/ & "\\" @(EscapedTimestampRules / KatexInline / Escaped)
|
||||
/ & "[" @MaybeReferences
|
||||
/ & "<" @(TimestampRules / MaybeReferences / InlineEmoticon)
|
||||
/ & "!" @Image
|
||||
/ & "|" @Spoiler
|
||||
/ & "@" @UserMentionDirect
|
||||
/ & "`" @InlineCode
|
||||
/ & "+" @AutolinkedPhone
|
||||
/ & "$" @KatexInline
|
||||
/ & ":" @(InlineEmoji / InlineEmoticon)
|
||||
/ & "*" @(EmphasisWithWhitespace / Emphasis / InlineEmoticon)
|
||||
/ & "~" @(EmphasisWithWhitespace / Emphasis)
|
||||
/ & "-" @(PlainRunBeforeEmoticon / InlineEmoticon)
|
||||
/ InlineItemSlowPath
|
||||
|
||||
// Non-dispatched chars: emphasis with _, URLs, emails, emoticons, color, plain text
|
||||
// Preserves original rule ordering to maintain parsing behavior
|
||||
InlineItemSlowPath
|
||||
= AutolinkedEmail
|
||||
/ PlainUnderscoreThenDomain
|
||||
/ AutolinkedURL
|
||||
/ EmphasisWithWhitespace
|
||||
/ Emphasis
|
||||
/ ChannelMention
|
||||
/ InlineEmoji
|
||||
/ PlainRunBeforeEmoticon
|
||||
/ InlineEmoticon
|
||||
/ Color
|
||||
/ PlainRun
|
||||
|
||||
// Letters/digits: try email/URL (with cheap guards), then plain text
|
||||
InlineItemAlphaPath
|
||||
= AutolinkedEmail
|
||||
/ AutolinkedURL
|
||||
/ PlainRun
|
||||
|
||||
/**
|
||||
*
|
||||
* Spoiler
|
||||
* e.g: ||spoiler||, ||spoiler **bold**||
|
||||
*
|
||||
*/
|
||||
Spoiler = "||" text:SpoilerContentItems "||" { return spoiler(text); }
|
||||
|
||||
SpoilerContentItems = text:SpoilerContentItem+ { return reducePlainTexts(text); }
|
||||
|
||||
// Ensure we consume at least one character and do not accidentally match the closing "||"
|
||||
SpoilerContentItem = !"||" @InlineItemPattern / !"||" @Any
|
||||
|
||||
/**
|
||||
*
|
||||
* URL
|
||||
* e.g:
|
||||
* Reference: [Rocket.Chat Website](https://rocket.chat), [](https://rocket.chat), <rocket.chat|Rocket.Chat Website>
|
||||
* Image: 
|
||||
*
|
||||
*/
|
||||
References
|
||||
= "[" title:LinkTitle* "](" href:MarkdownLinkRef ")" { return title.length ? link(href, reducePlainTexts(title)) : link(href); }
|
||||
/ "<" href:LinkRef "|" title:LinkTitle2 ">" { return link(href, [plain(title)]); }
|
||||
|
||||
// Fast-path: bulk consume chars that can't start ]( or ] [ and aren't emphasis markers
|
||||
LinkTitle
|
||||
= (Whitespace / Emphasis)
|
||||
/ anyTitle:$[^\]()*_~ \t\r\n]+ { return plain(anyTitle) }
|
||||
/ anyTitle:$(!("](" .) !("] [" [^\]]* "](") .) { return plain(anyTitle) }
|
||||
|
||||
LinkTitle2 = $([\x20-\x3B\x3D\x3F-\x60\x61-\x7B\x7D-\xFF] / NonASCII)+
|
||||
|
||||
MarkdownLinkRef = MarkdownLinkURL / MarkdownLinkFilePath / p:Phone { return 'tel:' + p.number; }
|
||||
|
||||
// LinkRef is used for non-markdown link contexts (like <url|title> syntax) where parentheses aren't balanced
|
||||
LinkRef = URL / FilePath / p:Phone { return 'tel:' + p.number; }
|
||||
|
||||
FilePath = $(URLScheme URLBody+)
|
||||
|
||||
MarkdownLinkFilePath = $(URLScheme MarkdownLinkURLBody+)
|
||||
|
||||
// MarkdownLinkURL allows parentheses in URLs when inside markdown link syntax [title](url)
|
||||
MarkdownLinkURL
|
||||
= head:($(URLScheme URLAuthority) / $(URLAuthorityHost)) tail:$(MarkdownLinkURLBody*) { return head + tail; }
|
||||
|
||||
MarkdownLinkURLBody
|
||||
= (
|
||||
!(MarkdownLinkExtra+ (Whitespace / EndOfLine) / Whitespace)
|
||||
!")" // Don't consume closing paren
|
||||
(AnyText / [*\[\/\]\^_`{}~] / "(" MarkdownLinkURLBodyParen* ")")
|
||||
)+
|
||||
|
||||
// Match content inside parentheses within URL
|
||||
MarkdownLinkURLBodyParen = !(Whitespace / EndOfLine / ")") (AnyText / [*\[\/\]\^_`{}~(])
|
||||
|
||||
MarkdownLinkExtra = [.,!%*\"':;=]
|
||||
|
||||
Image = "" { return title ? image(href, title) : image(href); }
|
||||
|
||||
URL
|
||||
= head:($(URLScheme URLAuthority) / $(URLAuthorityHost)) tail:$(URLBody*) { return head + tail; }
|
||||
|
||||
URLScheme = $([A-Za-z0-9+-] |1..32| ":")
|
||||
|
||||
URLBody
|
||||
= (
|
||||
!(Extra+ (Whitespace / EndOfLine / !.) / Whitespace)
|
||||
(AnyText / [*\[\/\]\^_`{}~(])
|
||||
)+
|
||||
|
||||
URLAuthority = $("//" URLAuthorityUserInfo? URLAuthorityHost)
|
||||
|
||||
URLAuthorityUserInfo = $(URLAuthorityUser (":" URLAuthorityPassword)? "@")
|
||||
|
||||
URLAuthorityUser = $(AlphaDigit / ![@/] Safe)+
|
||||
|
||||
URLAuthorityPassword = $(AlphaDigit / ![@/] Safe)+
|
||||
|
||||
URLAuthorityHost = URLAuthorityHostName (":" URLAuthorityPort)?
|
||||
|
||||
URLAuthorityHostName
|
||||
= DomainName
|
||||
/ $(Digits |4, "."|) // TODO: IPv4 and IPv6
|
||||
|
||||
URLAuthorityPort
|
||||
= Digits // TODO: from "0" to "65535"
|
||||
|
||||
DomainName
|
||||
= "localhost"
|
||||
/ $(![\x5F] DomainNameLabel ("." DomainChar DomainNameLabel*)+)
|
||||
|
||||
DomainNameLabel = $(DomainChar+ ("-" DomainChar+)*)
|
||||
|
||||
DomainChar = [a-zA-Z0-9] / !Extra ([\__-] / !Safe) !EndOfLine !Space ![\\/|><%`\[\]] .
|
||||
|
||||
/**
|
||||
*
|
||||
* Phone
|
||||
* e.g: 075-63546725
|
||||
*
|
||||
*/
|
||||
Phone = "+" p:PhoneNumber { return { text: '+' + p.text, number: p.number }; }
|
||||
|
||||
PhoneNumber
|
||||
= p:PhonePrefix "-" d:Digits {
|
||||
return { text: p.text + '-' + d, number: p.number + d };
|
||||
}
|
||||
/ p:PhonePrefix d1:Digits "-" d2:Digits {
|
||||
return { text: p.text + d1 + '-' + d2, number: p.number + d1 + d2 };
|
||||
}
|
||||
/ p:PhonePrefix d:Digits {
|
||||
return { text: p.text + d, number: p.number + d };
|
||||
}
|
||||
/ d:Digits { return { text: d, number: d }; }
|
||||
|
||||
PhonePrefix
|
||||
= d:Digits { return { text: d, number: d }; }
|
||||
/ "(" d:Digits ")" { return { text: '(' + d + ')', number: d }; }
|
||||
|
||||
AutolinkedPhone = p:Phone { return phoneChecker(p.text, p.number); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Email
|
||||
* e.g: contact@rocket.chat
|
||||
*
|
||||
*/
|
||||
Email = "mailto:"? @$(LocalPart "@" DomainName)
|
||||
|
||||
LocalPart = $(LocalPartChar+ ("." LocalPartChar+)*)
|
||||
|
||||
LocalPartChar = AlphaNumericOrMarkChar+ LocalPartSpecialChars*
|
||||
|
||||
LocalPartSpecialChars = [!#$%&'*+/=?^_\`{|}~-]
|
||||
|
||||
// Guard: only attempt email parse if @ exists ahead on this line
|
||||
AutolinkedEmail = &([^ \t\r\n@]* "@") e:Email { return autoEmail(e); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Auto Link URL
|
||||
* e.g: rocket.chat, https://rocket.chat,
|
||||
* with customDomains options as intranet: protocol://internaltool.intranet
|
||||
*
|
||||
*/
|
||||
// _example.com (underscore + domain without closing _) → plain
|
||||
PlainUnderscoreThenDomain = "_" d:DomainName &(EndOfLine / !. / [^\x5F]) { return plain('_' + d); }
|
||||
|
||||
// Guard: only attempt URL parse if :// or . exists ahead on this line
|
||||
AutolinkedURL = &([^ \t\r\n:./@]* ("://" / ".")) u:AutoLinkURL { return autoLink(u, options.customDomains); }
|
||||
|
||||
AutoLinkURL
|
||||
= head:($(URLScheme URLAuthority) / $(URLAuthorityHost)) tail:$(AutoLinkURLBody*) { return head + tail; }
|
||||
|
||||
AutoLinkURLBody
|
||||
= [^ \t\r\n.,!%~*\"':;()=~]
|
||||
/ !(Extra* (Whitespace / EndOfLine / !.)) .
|
||||
|
||||
/**
|
||||
*
|
||||
* Emphasis
|
||||
*
|
||||
*/
|
||||
Emphasis = MaybeBold / MaybeItalic / MaybeStrikethrough
|
||||
|
||||
/**
|
||||
*
|
||||
* Italic, Bold and Strike
|
||||
* e.g: __italic__, _italic_, **bold**, __*bold italic*__, ~~strikethrough~~
|
||||
*
|
||||
*/
|
||||
|
||||
// Prevent re-entrant emphasis (infinite recursion); reset on backtrack via second branch
|
||||
BlockedByJavascript = 'unreachable'
|
||||
|
||||
MaybeBold
|
||||
= & { if (skipBold) { return false; } skipBold = true; return true; }
|
||||
@( text:Bold { skipBold = false; return text; }
|
||||
/ & { skipBold = false; return false; } BlockedByJavascript )
|
||||
|
||||
MaybeStrikethrough
|
||||
= & { if (skipStrikethrough) { return false; } skipStrikethrough = true; return true; }
|
||||
@( text:Strikethrough { skipStrikethrough = false; return text; }
|
||||
/ & { skipStrikethrough = false; return false; } BlockedByJavascript )
|
||||
|
||||
MaybeItalic
|
||||
= & { if (skipItalic) { return false; } skipItalic = true; return true; }
|
||||
@( text:Italic { skipItalic = false; return text; }
|
||||
/ & { skipItalic = false; return false; } BlockedByJavascript )
|
||||
|
||||
MaybeReferences
|
||||
= & { if (skipReferences) { return false; } skipReferences = true; return true; }
|
||||
@( text:References { skipReferences = false; return text; }
|
||||
/ & { skipReferences = false; return false; } BlockedByJavascript )
|
||||
|
||||
/* Italic */
|
||||
Italic
|
||||
= value:$([a-zA-Z0-9]+ [\x5F] [\x5F]?) { return plain(value); }
|
||||
/ [\x5F] [\x5F] i:ItalicContentItems [\x5F] [\x5F] t:$[a-zA-Z0-9]+ {
|
||||
return reducePlainTexts([plain('__'), ...i, plain('__'), plain(t)]);
|
||||
}
|
||||
/ [\x5F] i:ItalicContentItems [\x5F] t:$[a-zA-Z]+ {
|
||||
return reducePlainTexts([plain('_'), ...i, plain('_'), plain(t)]);
|
||||
}
|
||||
/ [\x5F] [\x5F] @ItalicContent [\x5F] [\x5F]
|
||||
/ [\x5F] @ItalicContent [\x5F]
|
||||
|
||||
ItalicContent = text:ItalicContentItems { return italic(text); }
|
||||
|
||||
ItalicContentItems = text:ItalicContentItem+ { return reducePlainTexts(text); }
|
||||
|
||||
ItalicContentItem = ItalicContentPreferentialItem / ItalicPlainRun / AnyItalic / Line
|
||||
|
||||
ItalicContentPreferentialItem = Whitespace
|
||||
/ InlineCode
|
||||
/ TimestampRules
|
||||
/ MaybeReferences
|
||||
/ UserMention
|
||||
/ ChannelMention
|
||||
/ MaybeBold
|
||||
/ MaybeStrikethrough
|
||||
/ ItalicEmoji
|
||||
/ ItalicEmoticon
|
||||
|
||||
ItalicEmoji = emo:Emoji { return emo; }
|
||||
|
||||
ItalicEmoticon = emo:Emoticon & (EmoticonNeighbor / ItalicContentPreferentialItem / [\x5F]) { return emo; }
|
||||
|
||||
/* Bold */
|
||||
Bold = [\x2A] [\x2A] @BoldContent [\x2A] [\x2A] / [\x2A] @BoldContent [\x2A]
|
||||
|
||||
BoldContent = text:BoldContentItem+ { return bold(reducePlainTexts(text)); }
|
||||
|
||||
BoldContentPreferentialItem = Whitespace / InlineCode / TimestampRules / MaybeReferences / UserMention / ChannelMention / MaybeItalic / MaybeStrikethrough / BoldEmoji / BoldEmoticon
|
||||
|
||||
BoldContentItem = BoldContentPreferentialItem / BoldPlainRun / AnyBold / Line
|
||||
|
||||
BoldEmoji = emo:Emoji { return emo; }
|
||||
|
||||
BoldEmoticon = emo:Emoticon & (EmoticonNeighbor / BoldContentPreferentialItem) { return emo; }
|
||||
|
||||
/* Strike */
|
||||
Strikethrough = [\x7E] [\x7E] @StrikethroughContent [\x7E] [\x7E] / [\x7E] @StrikethroughContent [\x7E]
|
||||
|
||||
StrikethroughContent = text:(StrikePlainRunFull / EscapedTimestampRules / TimestampRules / Whitespace / InlineCode / MaybeReferences / UserMention / ChannelMention / MaybeItalic / MaybeBold / Emoji / Emoticon / AnyStrike / Line)+ {
|
||||
return strike(reducePlainTexts(text));
|
||||
}
|
||||
|
||||
// Like StrikePlainRun but also excludes chars that start inline rules inside strike
|
||||
StrikePlainRunFull = run:$[^\x0a\~ *_:`@#\[\]\\<!\x7C\uD83C\uD83D\uD83E\u2300-\u27BF\u2600-\u26FF]+ { return plain(run); }
|
||||
|
||||
// Exclude _ and ~ so nested italic/strike can be parsed
|
||||
BoldPlainRun = run:$[^\x0a\* _~ ]+ { return plain(run); }
|
||||
AnyBold = t:[^\x0a\* ] { return plain(t); }
|
||||
|
||||
// Exclude * and ~ so nested bold/strike can be parsed
|
||||
ItalicPlainRun = run:$[^\x0a\_ *~]+ { return plain(run); }
|
||||
AnyItalic = t:[^\x0a\_ ] { return plain(t); }
|
||||
|
||||
// Exclude * and _ so nested bold/italic can be parsed
|
||||
StrikePlainRun = run:$[^\x0a\~ *_]+ { return plain(run); }
|
||||
AnyStrike = t:[^\x0a\~ ] { return plain(t); }
|
||||
|
||||
/**
|
||||
* Emphasis with only whitespaces return plain text
|
||||
* e.g: __ __, _ _, ** **, * *, ** *, ~~ ~~
|
||||
*/
|
||||
EmphasisWithWhitespace = AsteriskWithWhitespace / UnderscoreWithWhitespace / TildeWithWhitespace
|
||||
|
||||
AsteriskWithWhitespace = a:$"*"+ w:$Space+ b:$"*"+ { return plain(a + w + b); }
|
||||
|
||||
UnderscoreWithWhitespace = a:$"_"+ w:$Space+ b:$"_"+ { return plain(a + w + b); }
|
||||
|
||||
TildeWithWhitespace = a:$"~"+ w:$Space+ b:$"~"+ { return plain(a + w + b); }
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Mentions
|
||||
* e.g: @user, #channel
|
||||
*
|
||||
*/
|
||||
// Direct form: starts with @, used by dispatch
|
||||
UserMentionDirect
|
||||
= "@"+ user:$(UTF8NamesValidation ([:@] UTF8NamesValidation)?) & { return !user.endsWith('__'); } {
|
||||
return mentionUser(user);
|
||||
}
|
||||
|
||||
// Full form: includes text@user fallback, used by SlowPath
|
||||
UserMention
|
||||
= t:Text "@"+ user:AlphaNumericChar {
|
||||
return reducePlainTexts([t, plain('@' + user)])[0];
|
||||
}
|
||||
/ UserMentionDirect
|
||||
|
||||
ChannelMention
|
||||
= t:Text "#" channel:AlphaNumericChar {
|
||||
return reducePlainTexts([t, plain('#' + channel)])[0];
|
||||
}
|
||||
/ "#" channel:UTF8NamesValidation { return mentionChannel(channel); }
|
||||
|
||||
/**
|
||||
*
|
||||
* EMOJIS
|
||||
* e.g:
|
||||
* BigEmoji (min: 1 | max: 3 without text) and ShortCode :smile: :+1: :heart:
|
||||
* Unicode: 😀 🚀 🌈
|
||||
* Emoticon: :D :P :( :/
|
||||
*
|
||||
*/
|
||||
BigEmoji = (EndOfLine / Space)* es:(@(Emoji / Emoticon) (EndOfLine / Space)*) |1..3| { return [bigEmoji(es)]; }
|
||||
|
||||
Emoji = EmojiShortCode / ch:UnicodeEmoji { return emojiUnicode(ch); }
|
||||
|
||||
EmojiShortCode = ":" shortCode:EmojiShortCodeName ":" { return emoji(shortCode); }
|
||||
|
||||
EmojiShortCodeName = $[0-9a-zA-Z-_+.]+
|
||||
|
||||
/* Emoticons */
|
||||
Emoticon = & { return options.emoticons; } @EmoticonPattern
|
||||
|
||||
EmoticonNeighbor = EndOfLine / Whitespace / [\x2A] / !.
|
||||
|
||||
// Emoticon first-char dispatch: group alternatives by starting character
|
||||
EmoticonPattern
|
||||
= & ":" @EmoticonColon
|
||||
/ & ";" @EmoticonSemicolon
|
||||
/ & "<" @EmoticonLT
|
||||
/ & ">" @EmoticonGT
|
||||
/ & "=" @EmoticonEquals
|
||||
/ & "D" @EmoticonD
|
||||
/ & "B" @EmoticonB
|
||||
/ & "8" @Emoticon8
|
||||
/ & "'" @EmoticonApostrophe
|
||||
/ & "O" @EmoticonO
|
||||
/ & "0" @EmoticonZero
|
||||
/ & "*" @EmoticonAsterisk
|
||||
/ & "X" @EmoticonX
|
||||
/ & "#" @EmoticonHash
|
||||
/ & "%" @EmoticonPercent
|
||||
/ & "(" @EmoticonParen
|
||||
/ & "-" @EmoticonDash
|
||||
/ & "\\" @EmoticonBackslash
|
||||
|
||||
EmoticonLT
|
||||
= e:$"</3" { return emoticon(e, 'broken_heart'); }
|
||||
/ e:$"<3" { return emoticon(e, 'heart'); }
|
||||
|
||||
EmoticonColon
|
||||
= e:$(":-D" / ":D") { return emoticon(e, 'smiley'); }
|
||||
/ e:$(":')" / ":'-)") { return emoticon(e, 'joy'); }
|
||||
/ e:$(":-)" / ":)" / ":]") { return emoticon(e, 'slight_smile'); }
|
||||
/ e:$(":-*" / ":*" / ":^*") { return emoticon(e, 'kissing_heart'); }
|
||||
/ e:$(":-P" / ":P" / ":-\u00de" / ":\u00de" / ":-b" / ":b") { return emoticon(e, 'stuck_out_tongue'); }
|
||||
/ e:$(":-(" / ":(" / ":-[" / ":[") { return emoticon(e, 'disappointed'); }
|
||||
/ e:$(":-/" / ":-." / ":/" / ":\\" / ":L") { return emoticon(e, 'confused'); }
|
||||
/ e:$(":'(" / "':-(") { return emoticon(e, 'cry'); }
|
||||
/ e:$":@" { return emoticon(e, 'angry'); }
|
||||
/ e:$(":$") { return emoticon(e, 'flushed'); }
|
||||
/ e:$(":-X" / ":X" / ":-#" / ":#") { return emoticon(e, 'no_mouth'); }
|
||||
/ e:$(":-O" / ":O") { return emoticon(e, 'open_mouth'); }
|
||||
|
||||
EmoticonEquals
|
||||
= e:$"=D" { return emoticon(e, 'smiley'); }
|
||||
/ e:$("=]" / "=)") { return emoticon(e, 'slight_smile'); }
|
||||
/ e:$"=*" { return emoticon(e, 'kissing_heart'); }
|
||||
/ e:$"=P" { return emoticon(e, 'stuck_out_tongue'); }
|
||||
/ e:$"=(" { return emoticon(e, 'disappointed'); }
|
||||
/ e:$("=/" / "=\\" / "=L") { return emoticon(e, 'confused'); }
|
||||
/ e:$("=$") { return emoticon(e, 'flushed'); }
|
||||
/ e:$("=X" / "=#") { return emoticon(e, 'no_mouth'); }
|
||||
|
||||
EmoticonGT
|
||||
= e:$(">:)" / ">;)" / ">:-)" / ">=)") { return emoticon(e, 'laughing'); }
|
||||
/ e:$">.<" { return emoticon(e, 'persevere'); }
|
||||
/ e:$">:P" { return emoticon(e, 'stuck_out_tongue_winking_eye'); }
|
||||
/ e:$(">:\\" / ">;/") { return emoticon(e, 'confused'); }
|
||||
/ e:$(">:(" / ">:-(") { return emoticon(e, 'angry'); }
|
||||
/ e:$(">:[") { return emoticon(e, 'disappointed'); }
|
||||
/ e:$">:O" { return emoticon(e, 'open_mouth'); }
|
||||
|
||||
EmoticonApostrophe
|
||||
= e:$("':)" / "':-)" / "'=)" / "':D" / "':-D" / "'=D") { return emoticon(e, 'sweat_smile'); }
|
||||
/ e:$("':(" / "':-(" / "'=(") { return emoticon(e, 'sweat'); }
|
||||
|
||||
EmoticonO
|
||||
= e:$("O:-)" / "O:)" / "O;-)" / "O=)" / "O:-3" / "O:3") { return emoticon(e, 'innocent'); }
|
||||
/ e:$"O_O" { return emoticon(e, 'open_mouth'); }
|
||||
|
||||
EmoticonZero
|
||||
= e:$("0:-3" / "0:3" / "0:-)" / "0:)" / "0;^)" / "0;-)") { return emoticon(e, 'innocent'); }
|
||||
|
||||
EmoticonSemicolon
|
||||
= e:$(";)" / ";-)" / ";-]" / ";]" / ";D" / ";^)") { return emoticon(e, 'wink'); }
|
||||
/ e:$(";(" / ";-(") { return emoticon(e, 'cry'); }
|
||||
|
||||
EmoticonAsterisk
|
||||
= e:$("*-)" / "*)") { return emoticon(e, 'wink'); }
|
||||
/ e:$("*\\0\/*" / "*\\O\/*") { return emoticon(e, 'person_gesturing_ok'); }
|
||||
|
||||
EmoticonB
|
||||
= e:$("B-)" / "B)" / "B-D") { return emoticon(e, 'sunglasses'); }
|
||||
|
||||
Emoticon8
|
||||
= e:$("8)" / "8-)" / "8-D") { return emoticon(e, 'sunglasses'); }
|
||||
|
||||
EmoticonD
|
||||
= e:$"D:" { return emoticon(e, 'fearful'); }
|
||||
|
||||
EmoticonX
|
||||
= e:$"X-P" { return emoticon(e, 'stuck_out_tongue_winking_eye'); }
|
||||
/ e:$("X)" / "X-)") { return emoticon(e, 'dizzy_face'); }
|
||||
|
||||
EmoticonHash
|
||||
= e:$("#-)" / "#)") { return emoticon(e, 'dizzy_face'); }
|
||||
|
||||
EmoticonPercent
|
||||
= e:$("%-)" / "%)") { return emoticon(e, 'dizzy_face'); }
|
||||
|
||||
EmoticonParen
|
||||
= e:$"(y)" { return emoticon(e, 'thumbsup'); }
|
||||
|
||||
EmoticonDash
|
||||
= e:$("-___-" / "-__-" / "-_-") { return emoticon(e, 'expressionless'); }
|
||||
|
||||
EmoticonBackslash
|
||||
= e:$("\\0\\/" / "\\O\\/") { return emoticon(e, 'person_gesturing_ok'); }
|
||||
|
||||
/* Unicode emojis */
|
||||
UnicodeEmoji
|
||||
= UnicodeEmojiTagSequence
|
||||
/ UnicodeEmojiKeycapSequence
|
||||
/ $(
|
||||
(UnicodeEmojiZwjComponent [\u200D])*
|
||||
UnicodeEmojiZwjComponent
|
||||
)
|
||||
/ UnicodeEmojiEmoticon
|
||||
/ UnicodeEmojiTransportAndMapSymbols
|
||||
/ UnicodeEmojiMiscellaneousTechnical
|
||||
/ UnicodeEmojiMiscellaneousSymbols
|
||||
/ UnicodeEmojiDingbats
|
||||
/ UnicodeEmojiGeometricSquares
|
||||
/ UnicodeEmojiEnclosedBadges
|
||||
/ UnicodeEmojiTextPresentation
|
||||
/ UnicodeEmojiFlags
|
||||
|
||||
UnicodeEmojiEmoticon = $([\uD83D] [\uDE00-\uDE4F] [︀-️]?)
|
||||
|
||||
UnicodeEmojiSupplementalSymbolsAndPictographs = $([\uD83E] [\uDD00-\uDFFF])
|
||||
|
||||
UnicodeEmojiZwjComponent
|
||||
= ( UnicodeEmojiSupplementalSymbolsAndPictographs
|
||||
/ UnicodeEmojiMiscellaneousSymbolsAndPictographs
|
||||
/ UnicodeEmojiEmoticon
|
||||
/ UnicodeEmojiTransportAndMapSymbols
|
||||
/ UnicodeEmojiDingbats
|
||||
/ UnicodeEmojiMiscellaneousSymbols
|
||||
/ UnicodeEmojiArrows
|
||||
/ UnicodeEmojiGeometricSquares
|
||||
) UnicodeEmojiMiscellaneousSymbolsAndPictographsFitzpatrickModifiers?
|
||||
|
||||
/* Emoji tag sequence: Black Flag + tag characters (U+E0020-U+E007E) + Cancel Tag (U+E007F), e.g. England/Scotland/Wales flags */
|
||||
UnicodeEmojiTagSequence = $([\uD83C] [\uDFF4] ([\uDB40] [\uDC20-\uDC7E])+ [\uDB40] [\uDC7F])
|
||||
|
||||
UnicodeEmojiKeycapSequence = $([0-9#*] [️]? [⃣])
|
||||
|
||||
UnicodeEmojiMiscellaneousSymbolsAndPictographs = $([\uD83C] [\uDF00-\uDFFF] [\uFE00-\uFE0F]?) / $([\uD83D] [\uDC00-\uDDFF] [\uFE00-\uFE0F]?)
|
||||
|
||||
UnicodeEmojiMiscellaneousSymbolsAndPictographsFitzpatrickModifiers = $([\uD83C] [\uDFFB-\uDFFF])
|
||||
|
||||
UnicodeEmojiTransportAndMapSymbols = $([\uD83D] [\uDE80-\uDEFF] [︀-️]?)
|
||||
|
||||
UnicodeEmojiMiscellaneousTechnical = $([\u2300-\u23FF] [\uFE00-\uFE0F]?)
|
||||
|
||||
UnicodeEmojiMiscellaneousSymbols = $([\u2600-\u26FF] [\uFE00-\uFE0F]?)
|
||||
|
||||
UnicodeEmojiDingbats = $([\u2700-\u27BF] [\uFE00-\uFE0F]?)
|
||||
|
||||
/* U+2194/U+2195 only; kept narrow so bare prose arrows (U+2190..U+2193, U+21D2) aren't matched */
|
||||
UnicodeEmojiArrows = $([\u2194-\u2195] [\uFE00-\uFE0F]?)
|
||||
|
||||
UnicodeEmojiGeometricSquares = $([\u2B1B-\u2B1C] [\uFE00-\uFE0F]?) / $([\uD83D] ([\uDFE0-\uDFEB] / [\uDFF0]) [\uFE00-\uFE0F]?)
|
||||
|
||||
/* Tight ranges — these enclosed-alphanumeric/ideographic and playing-card blocks are mostly non-emoji */
|
||||
UnicodeEmojiEnclosedBadges = $([\uD83C] ([\uDCCF] / [\uDD8E] / [\uDD91-\uDD9A] / [\uDE01] / [\uDE32-\uDE3A] / [\uDE50-\uDE51]) [︀-️]?)
|
||||
|
||||
/* Default-text chars that are emoji ONLY with a required trailing VS16, so a bare U+00A9/U+2122/U+25B6 in prose stays text */
|
||||
UnicodeEmojiTextPresentation
|
||||
= $([©®‼⁉™ℹ↖-↙↩-↪Ⓜ▪-▫▶◀◻-◾⤴-⤵⬅-⬇⭐⭕〰〽㊗㊙] [️])
|
||||
/ $([\uD83C] ([\uDC04] / [\uDD70-\uDD71] / [\uDD7E-\uDD7F] / [\uDE02] / [\uDE1A] / [\uDE2F]) [️])
|
||||
|
||||
/* Two regional indicators combine into a flag (e.g. U + S = US flag); a single one alone is not an emoji. Narrowed from U+1F100-1F1FF so squared badges aren't mis-grouped as flags. */
|
||||
UnicodeEmojiFlags = $([\uD83C] [\uDDE6-\uDDFF] [\uD83C] [\uDDE6-\uDDFF])
|
||||
|
||||
/**
|
||||
*
|
||||
* Inline Code
|
||||
* e.g: `console.log('hello world')`
|
||||
*
|
||||
*/
|
||||
InlineCode = "`" text:$([^`\n]+) "`" { return inlineCode(plain(text)); }
|
||||
|
||||
/**
|
||||
*
|
||||
* Colors
|
||||
* e.g: color:#ff0000 , color:#ff0
|
||||
*
|
||||
*/
|
||||
Color = & { return options.colors; } "color:#" rgba:ColorRGBATuple !AnyText {
|
||||
return color(...rgba);
|
||||
}
|
||||
|
||||
ColorRGBATuple = HexByte|3..4| / HexNible|3..4|
|
||||
|
||||
/**
|
||||
*
|
||||
* Macros
|
||||
*
|
||||
*/
|
||||
Whitespace = w:$Space+ { return plain(w); }
|
||||
|
||||
EndOfLine = "\r\n" / "\n" / "\r"
|
||||
|
||||
Space = " " / "\t"
|
||||
|
||||
Escaped = "\\" t:[*_~`#.] { return plain(t); }
|
||||
|
||||
Any = t:[^\r\n] { return plain(t); }
|
||||
|
||||
AnyText = [\x20-\x27\x2B-\x40\x41-\x5A\x61-\x7A] / NonASCII
|
||||
|
||||
Text = text:AnyText { return plain(text); }
|
||||
|
||||
Line = t:LineStructure { return plain(t); }
|
||||
|
||||
LineStructure = head:$Space* text:$AnyText+ tail:$Space* { return head + text + tail; }
|
||||
|
||||
UTF8NamesValidation = $([-_.] / AlphaNumericChar)+
|
||||
|
||||
NonASCII = [\x80-\uFFFF]
|
||||
|
||||
Unicode = "\\" Digits:$(HexDigit |1..6|) ("\r\n" / [ \t\r\n\f])? { return String.fromCharCode(parseInt(Digits, 16)); }
|
||||
|
||||
Digit = [0-9]
|
||||
|
||||
Digits = $Digit+
|
||||
|
||||
Safe = [$@&+\__#?-]
|
||||
|
||||
Extra = [.,!%~*\"':;()=~]
|
||||
|
||||
HexDigit = [0-9A-Fa-f]
|
||||
|
||||
HexNible = a:HexDigit { return parseInt(a + a, 16); }
|
||||
|
||||
HexByte = a:HexDigit b:HexDigit { return parseInt(a + b, 16); }
|
||||
|
||||
AlphaDigit = [a-zA-Z0-9]
|
||||
|
||||
AlphaNumericOrMarkChar = AlphaOrMarkChar / DecimalNumberChar
|
||||
|
||||
AlphaOrMarkChar = AlphaChar / EmojiChar / MarkChar
|
||||
|
||||
AlphaNumericChar = AlphaChar / DecimalNumberChar
|
||||
|
||||
AlphaChar = [A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]
|
||||
|
||||
DecimalNumberChar = [0-9\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F\u0DE6-\u0DEF\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29\u1040-\u1049\u1090-\u1099\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\uA620-\uA629\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uA9F0-\uA9F9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19]
|
||||
|
||||
EmojiChar = [\u2700-\u27bf\udde6-\uddff\ud800-\udbff\udc00-\udfff\ufe0e\ufe0f\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0\ud83c\udffb-\udfff\u200d\u3299\u3297\u303d\u3030\u24c2\ud83c\udd70-\udd71\udd7e-\udd7f\udd8e\udd91-\udd9a\udde6-\uddff\ude01-\ude02\ude1a\ude2f\ude32-\ude3a\ude50-\ude51\u203c\u2049\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u00a9\u00ae\u2122\u2139\udc04\u2600-\u26FF\u2b05\u2b06\u2b07\u2b1b\u2b1c\u2b50\u2b55\u231a\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\udccf\u2935\u2934\u2190-\u21ff]
|
||||
|
||||
MarkChar = [\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D4-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]
|
||||
4
packages/message-parser/src/guards.ts
Normal file
4
packages/message-parser/src/guards.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { Types, TypesKeys } from './definitions';
|
||||
|
||||
export const isNodeOfType = <T extends TypesKeys>(value: unknown, type: T): value is Types[T] =>
|
||||
typeof value === 'object' && value !== null && 'type' in value && (value as { type: unknown }).type === type;
|
||||
23
packages/message-parser/src/index.ts
Normal file
23
packages/message-parser/src/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Root } from './definitions';
|
||||
import * as grammar from './grammar.pegjs';
|
||||
|
||||
export type * from './definitions';
|
||||
|
||||
export { isNodeOfType } from './guards';
|
||||
|
||||
export { asciiList } from './asciiList';
|
||||
|
||||
export type Options = {
|
||||
colors?: boolean;
|
||||
emoticons?: boolean;
|
||||
katex?: {
|
||||
dollarSyntax?: boolean;
|
||||
parenthesisSyntax?: boolean;
|
||||
};
|
||||
customDomains?: string[];
|
||||
};
|
||||
|
||||
export const parse = (input: string, options?: Options): Root => grammar.parse(input, options);
|
||||
|
||||
export type { Root as MarkdownAST };
|
||||
export { parse as parser };
|
||||
7
packages/message-parser/src/typings/peg.d.ts
vendored
Normal file
7
packages/message-parser/src/typings/peg.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module '*.pegjs' {
|
||||
import type { ParserOptions } from 'peggy';
|
||||
|
||||
import type { ASTMessage } from '../definitions';
|
||||
|
||||
export const parse: (input: string, options?: ParserOptions) => ASTMessage;
|
||||
}
|
||||
393
packages/message-parser/src/utils.ts
Normal file
393
packages/message-parser/src/utils.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
import { parse as tldParse } from 'tldts';
|
||||
|
||||
import type {
|
||||
BigEmoji,
|
||||
Code,
|
||||
Color,
|
||||
Heading,
|
||||
Markup,
|
||||
Paragraph,
|
||||
Types,
|
||||
Task,
|
||||
ListItem,
|
||||
Inlines,
|
||||
LineBreak,
|
||||
Emoji,
|
||||
KaTeX,
|
||||
InlineKaTeX,
|
||||
Link,
|
||||
Timestamp,
|
||||
SourceRange,
|
||||
HorizontalRule,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from './definitions';
|
||||
|
||||
const generate =
|
||||
<Type extends keyof Types>(type: Type) =>
|
||||
(value: Types[Type]['value']): Types[Type] =>
|
||||
({ type, value }) as any;
|
||||
|
||||
export const paragraph = generate('PARAGRAPH');
|
||||
|
||||
export const bold = generate('BOLD');
|
||||
|
||||
export const color = (r: number, g: number, b: number, a = 255): Color => ({
|
||||
type: 'COLOR',
|
||||
value: { r, g, b, a },
|
||||
});
|
||||
|
||||
export const heading = (value: Heading['value'], level: Heading['level'] = 1): Heading => ({
|
||||
type: 'HEADING',
|
||||
level,
|
||||
value,
|
||||
});
|
||||
|
||||
export const code = (value: Code['value'], language?: Code['language']): Code => ({
|
||||
type: 'CODE',
|
||||
language: language || 'none',
|
||||
value,
|
||||
});
|
||||
|
||||
export const bigEmoji = (value: BigEmoji['value']): BigEmoji => ({
|
||||
type: 'BIG_EMOJI',
|
||||
value,
|
||||
});
|
||||
|
||||
export const task = (value: Task['value'], status: boolean): Task => ({
|
||||
type: 'TASK',
|
||||
status,
|
||||
value,
|
||||
});
|
||||
|
||||
export const inlineCode = generate('INLINE_CODE');
|
||||
export const tasks = generate('TASKS');
|
||||
|
||||
export const italic = generate('ITALIC');
|
||||
export const spoiler = generate('SPOILER');
|
||||
|
||||
export const plain = generate('PLAIN_TEXT');
|
||||
export const strike = generate('STRIKE');
|
||||
|
||||
export const codeLine = generate('CODE_LINE');
|
||||
|
||||
const isValidLink = (link: string) => URL.canParse(link);
|
||||
|
||||
const hasAbsoluteSchemePrefix = (src: string) => /^[A-Za-z][A-Za-z0-9+.-]{0,31}:\/\//.test(src);
|
||||
|
||||
export const link = (src: string, label?: Markup[]): Link => ({
|
||||
type: 'LINK',
|
||||
value: { src: plain(src), label: label ?? [plain(src)] },
|
||||
});
|
||||
|
||||
let cachedAutoLinkDomains: string[] | undefined | null = null;
|
||||
let cachedAutoLinkOptions: { detectIp: boolean; allowPrivateDomains: boolean; validHosts: string[] };
|
||||
|
||||
export const autoLink = (src: string, customDomains?: string[]) => {
|
||||
if (cachedAutoLinkDomains !== customDomains) {
|
||||
cachedAutoLinkDomains = customDomains;
|
||||
cachedAutoLinkOptions = {
|
||||
detectIp: true,
|
||||
allowPrivateDomains: true,
|
||||
validHosts: ['localhost', ...(customDomains ?? [])],
|
||||
};
|
||||
}
|
||||
const { validHosts } = cachedAutoLinkOptions;
|
||||
const { isIcann, isIp, isPrivate, domain } = tldParse(src, cachedAutoLinkOptions);
|
||||
|
||||
if (!(isIcann || isIp || isPrivate || (domain && validHosts.includes(domain)))) {
|
||||
return plain(src);
|
||||
}
|
||||
|
||||
const href = isValidLink(src) || src.startsWith('//') || hasAbsoluteSchemePrefix(src) ? src : `//${src}`;
|
||||
|
||||
return link(href, [plain(src)]);
|
||||
};
|
||||
|
||||
const autoEmailTldOptions = { detectIp: false, allowPrivateDomains: true } as const;
|
||||
|
||||
export const autoEmail = (src: string) => {
|
||||
const href = `mailto:${src}`;
|
||||
|
||||
const { isIcann, isIp, isPrivate } = tldParse(href, autoEmailTldOptions);
|
||||
|
||||
if (!(isIcann || isIp || isPrivate)) {
|
||||
return plain(src);
|
||||
}
|
||||
|
||||
return link(href, [plain(src)]);
|
||||
};
|
||||
|
||||
export const image = (() => {
|
||||
const fn = generate('IMAGE');
|
||||
return (src: string, label?: Markup) => fn({ src: plain(src), label: label || plain(src) });
|
||||
})();
|
||||
|
||||
export const quote = generate('QUOTE');
|
||||
export const spoilerBlock = generate('SPOILER_BLOCK');
|
||||
|
||||
export const mentionChannel = (() => {
|
||||
const fn = generate('MENTION_CHANNEL');
|
||||
return (value: string) => fn(plain(value));
|
||||
})();
|
||||
|
||||
export const orderedList = generate('ORDERED_LIST');
|
||||
|
||||
export const unorderedList = generate('UNORDERED_LIST');
|
||||
|
||||
export const listItem = (text: Inlines[], number?: number): ListItem => ({
|
||||
type: 'LIST_ITEM',
|
||||
value: text,
|
||||
...(number !== undefined && { number }),
|
||||
});
|
||||
|
||||
// GFM trims leading/trailing whitespace of each table cell's content
|
||||
const trimCellContent = (value: Inlines[]): Inlines[] => {
|
||||
const result = value.slice();
|
||||
|
||||
const first = result[0];
|
||||
if (first?.type === 'PLAIN_TEXT') {
|
||||
const trimmed = first.value.replace(/^\s+/, '');
|
||||
if (trimmed === '') {
|
||||
result.shift();
|
||||
} else {
|
||||
result[0] = { type: 'PLAIN_TEXT', value: trimmed };
|
||||
}
|
||||
}
|
||||
|
||||
const last = result[result.length - 1];
|
||||
if (last?.type === 'PLAIN_TEXT') {
|
||||
const trimmed = last.value.replace(/\s+$/, '');
|
||||
if (trimmed === '') {
|
||||
result.pop();
|
||||
} else {
|
||||
result[result.length - 1] = { type: 'PLAIN_TEXT', value: trimmed };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const tableCell = (value: Inlines[], align: TableCell['align']): TableCell => ({
|
||||
type: 'TABLE_CELL',
|
||||
align,
|
||||
value: trimCellContent(value),
|
||||
});
|
||||
|
||||
export const table = (header: Inlines[][], aligns: Array<TableCell['align']>, rows: Inlines[][][], fallback?: SourceRange): Table => ({
|
||||
type: 'TABLE',
|
||||
value: {
|
||||
header: header.map((cell, index) => tableCell(cell, aligns[index])),
|
||||
rows: rows.map((cells): TableRow => ({
|
||||
type: 'TABLE_ROW',
|
||||
// Normalize each row to the header's column count: pad missing cells and
|
||||
// drop extras, so ragged GFM rows stay aligned with the header/delimiter.
|
||||
value: header.map((_, index) => tableCell(cells[index] ?? [], aligns[index])),
|
||||
})),
|
||||
},
|
||||
...(fallback !== undefined && { fallback }),
|
||||
});
|
||||
|
||||
export const mentionUser = (() => {
|
||||
const fn = generate('MENTION_USER');
|
||||
return (value: string) => fn(plain(value));
|
||||
})();
|
||||
|
||||
export const emoji = (shortCode: string): Emoji => ({
|
||||
type: 'EMOJI',
|
||||
value: plain(shortCode),
|
||||
shortCode,
|
||||
});
|
||||
|
||||
export const emojiUnicode = (unicode: string): Emoji => ({
|
||||
type: 'EMOJI',
|
||||
value: undefined,
|
||||
unicode,
|
||||
});
|
||||
|
||||
export const emoticon = (emoticon: string, shortCode: string): Emoji => ({
|
||||
type: 'EMOJI',
|
||||
value: plain(emoticon),
|
||||
shortCode,
|
||||
});
|
||||
|
||||
const joinEmoji = (current: Inlines, previous: Inlines | undefined, next: Inlines | undefined): Inlines => {
|
||||
if (current.type !== 'EMOJI' || !current.value || (!previous && !next)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const hasEmojiAsNeighbor = previous?.type === current.type || current.type === next?.type;
|
||||
const hasPlainAsNeighbor =
|
||||
(previous?.type === 'PLAIN_TEXT' && previous.value.trim() !== '') || (next?.type === 'PLAIN_TEXT' && next.value.trim() !== '');
|
||||
const isEmoticon = current.shortCode !== current.value.value;
|
||||
|
||||
if (current.value && (hasEmojiAsNeighbor || hasPlainAsNeighbor)) {
|
||||
if (isEmoticon) {
|
||||
return current.value;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'PLAIN_TEXT',
|
||||
value: `:${current.value.value}:`,
|
||||
};
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
export const reducePlainTexts = (values: Paragraph['value']): Paragraph['value'] => {
|
||||
const flattenableValues = values as Array<Inlines | Inlines[]>;
|
||||
|
||||
// Fast path: no nested arrays and no emojis (the common case)
|
||||
let needsSlowPath = false;
|
||||
for (let i = 0; i < flattenableValues.length; i++) {
|
||||
const v = flattenableValues[i];
|
||||
if (Array.isArray(v) || v.type === 'EMOJI') {
|
||||
needsSlowPath = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsSlowPath) {
|
||||
const result: Paragraph['value'] = [];
|
||||
for (let i = 0; i < flattenableValues.length; i++) {
|
||||
const current = flattenableValues[i] as Inlines;
|
||||
const previous = result[result.length - 1];
|
||||
if (previous && current.type === 'PLAIN_TEXT' && previous.type === 'PLAIN_TEXT') {
|
||||
previous.value += current.value;
|
||||
} else {
|
||||
result.push(current);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Slow path: handles nested arrays and emoji joining
|
||||
const result: Paragraph['value'] = [];
|
||||
|
||||
let previousInline = undefined as Inlines | undefined;
|
||||
let pendingInline = undefined as Inlines | undefined;
|
||||
|
||||
const appendJoinedInline = (inline: Inlines, nextInline: Inlines | undefined): void => {
|
||||
const current = joinEmoji(inline, previousInline, nextInline);
|
||||
const previous = result[result.length - 1];
|
||||
|
||||
if (previous && current.type === 'PLAIN_TEXT' && previous.type === 'PLAIN_TEXT') {
|
||||
previous.value += current.value;
|
||||
} else {
|
||||
result.push(current);
|
||||
}
|
||||
|
||||
previousInline = inline;
|
||||
};
|
||||
|
||||
for (let index = 0; index < flattenableValues.length; index++) {
|
||||
const entry = flattenableValues[index];
|
||||
|
||||
if (Array.isArray(entry)) {
|
||||
for (let nestedIndex = 0; nestedIndex < entry.length; nestedIndex++) {
|
||||
const currentInline = entry[nestedIndex];
|
||||
|
||||
if (pendingInline === undefined) {
|
||||
pendingInline = currentInline;
|
||||
continue;
|
||||
}
|
||||
|
||||
appendJoinedInline(pendingInline, currentInline);
|
||||
pendingInline = currentInline;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pendingInline === undefined) {
|
||||
pendingInline = entry;
|
||||
continue;
|
||||
}
|
||||
|
||||
appendJoinedInline(pendingInline, entry);
|
||||
pendingInline = entry;
|
||||
}
|
||||
|
||||
if (pendingInline !== undefined) {
|
||||
appendJoinedInline(pendingInline, undefined);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
export const lineBreak = (): LineBreak => ({
|
||||
type: 'LINE_BREAK',
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
export const horizontalRule = (fallback?: SourceRange): HorizontalRule => ({
|
||||
type: 'HORIZONTAL_RULE',
|
||||
value: undefined,
|
||||
...(fallback !== undefined && { fallback }),
|
||||
});
|
||||
|
||||
export const katex = (content: string): KaTeX => ({
|
||||
type: 'KATEX',
|
||||
value: content,
|
||||
});
|
||||
|
||||
export const inlineKatex = (content: string): InlineKaTeX => ({
|
||||
type: 'INLINE_KATEX',
|
||||
value: content,
|
||||
});
|
||||
|
||||
export const phoneChecker = (text: string, number: string) => {
|
||||
if (number.length < 5) {
|
||||
return plain(text);
|
||||
}
|
||||
|
||||
return link(`tel:${number}`, [plain(text)]);
|
||||
};
|
||||
|
||||
export const timestamp = (value: string, type?: 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R', fallback?: SourceRange): Timestamp => {
|
||||
return {
|
||||
type: 'TIMESTAMP',
|
||||
value: {
|
||||
timestamp: value,
|
||||
format: type || 't',
|
||||
},
|
||||
...(fallback !== undefined && { fallback }),
|
||||
};
|
||||
};
|
||||
|
||||
export const timestampFromHours = (hours: string, minutes = '00', seconds = '00', timezone = '') => {
|
||||
const date = new Date();
|
||||
|
||||
const yearMonthDay = date.toISOString().split('T')[0];
|
||||
|
||||
const timestamp = (new Date(`${yearMonthDay}T${hours}:${minutes}:${seconds}${timezone}`).getTime() / 1000) | 0;
|
||||
|
||||
return timestamp.toString();
|
||||
};
|
||||
|
||||
export const timestampFromIsoTime = ({
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
milliseconds,
|
||||
timezone,
|
||||
}: {
|
||||
year: string;
|
||||
month: string;
|
||||
day: string;
|
||||
hours: string;
|
||||
minutes: string;
|
||||
seconds: string;
|
||||
milliseconds?: string;
|
||||
timezone?: string;
|
||||
}) => {
|
||||
const date =
|
||||
(new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds || '000'}${timezone ? `${timezone}` : ''}`).getTime() /
|
||||
1000) |
|
||||
0;
|
||||
return date.toString();
|
||||
};
|
||||
258
packages/message-parser/tests/abuse.test.ts
Normal file
258
packages/message-parser/tests/abuse.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { parse } from '../src';
|
||||
import { paragraph, plain, bold, italic, strike } from './helpers';
|
||||
|
||||
test.each([
|
||||
[
|
||||
`This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. , REPEATx2 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEAT x3 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEAT x4 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEATx 5 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. , REPEAT x6 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. this can go long for some time, repeat x7 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. ,repeat x8 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some.`,
|
||||
[
|
||||
paragraph([
|
||||
plain(
|
||||
'This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&',
|
||||
),
|
||||
bold([
|
||||
plain('()'),
|
||||
italic([
|
||||
plain(`+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok`),
|
||||
strike([
|
||||
plain(
|
||||
`, from now on we repeat some. , REPEATx2 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok`,
|
||||
),
|
||||
]),
|
||||
plain(
|
||||
', from now on we repeat some. REPEAT x3 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()',
|
||||
),
|
||||
]),
|
||||
plain(`+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok`),
|
||||
strike([
|
||||
plain(
|
||||
', from now on we repeat some. REPEAT x4 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()',
|
||||
),
|
||||
italic([
|
||||
plain(
|
||||
`+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEATx 5 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()`,
|
||||
),
|
||||
]),
|
||||
plain(`+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok`),
|
||||
]),
|
||||
plain(
|
||||
`, from now on we repeat some. , REPEAT x6 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&`,
|
||||
),
|
||||
]),
|
||||
plain(`()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok`),
|
||||
strike([
|
||||
plain(
|
||||
`, from now on we repeat some. this can go long for some time, repeat x7 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()`,
|
||||
),
|
||||
italic([
|
||||
plain(
|
||||
`+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. ,repeat x8 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()`,
|
||||
),
|
||||
]),
|
||||
plain(`+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok`),
|
||||
]),
|
||||
plain(', from now on we repeat some.'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__',
|
||||
[
|
||||
paragraph([
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
italic([bold([plain('_')])]),
|
||||
bold([italic([plain('**')]), italic([plain('**')])]),
|
||||
plain('__'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
10
packages/message-parser/tests/any.test.ts
Normal file
10
packages/message-parser/tests/any.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { parse } from '../src';
|
||||
import { paragraph, plain } from './helpers';
|
||||
|
||||
test.each([
|
||||
['free text', [paragraph([plain('free text')])]],
|
||||
['free text, with comma', [paragraph([plain('free text, with comma')])]],
|
||||
['free text with unxpected/unfinished blocks *bold_', [paragraph([plain('free text with unxpected/unfinished blocks *bold_')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
141
packages/message-parser/tests/blockSplitter.spec.ts
Normal file
141
packages/message-parser/tests/blockSplitter.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { BlockSplitter, BlockType } from '../src/BlockSplitter';
|
||||
|
||||
describe('BlockSplitter', () => {
|
||||
it('should split simple paragraphs', () => {
|
||||
const input = 'Hello\nWorld';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.PARAGRAPH);
|
||||
expect(blocks[0].content).toBe('Hello\nWorld');
|
||||
});
|
||||
|
||||
it('should identify headings', () => {
|
||||
const input = '# Heading 1\n## Heading 2\nContent';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(3);
|
||||
expect(blocks[0].type).toBe(BlockType.HEADING);
|
||||
expect(blocks[0].level).toBe(1);
|
||||
expect(blocks[1].type).toBe(BlockType.HEADING);
|
||||
expect(blocks[1].level).toBe(2);
|
||||
});
|
||||
|
||||
it('should identify code blocks', () => {
|
||||
const input = 'Pre\n```javascript\nconst a = 1;\n```\nPost';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(3);
|
||||
expect(blocks[1].type).toBe(BlockType.CODE);
|
||||
expect(blocks[1].language).toBe('javascript');
|
||||
expect(blocks[1].content).toBe('const a = 1;');
|
||||
});
|
||||
|
||||
it('should handle list splitting and preserve full syntax', () => {
|
||||
const input = '- item 1\n* item 2\n1. item 3';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.LIST);
|
||||
expect(blocks[0].content).toBe('- item 1\n* item 2\n1. item 3');
|
||||
});
|
||||
|
||||
it('should handle nested lists via indentation', () => {
|
||||
const input = '- Level 1\n - Level 2\n - Level 3';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].content).toBe('- Level 1\n - Level 2\n - Level 3');
|
||||
});
|
||||
|
||||
it('should allow indented blank lines to continue a list', () => {
|
||||
const input = '- item 1\n \n- item 2';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].content).toBe('- item 1\n \n- item 2');
|
||||
});
|
||||
|
||||
it('should correctly detect boundaries: list followed by heading', () => {
|
||||
const input = '- list item\n\n# Heading';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(2);
|
||||
expect(blocks[0].type).toBe(BlockType.LIST);
|
||||
expect(blocks[1].type).toBe(BlockType.HEADING);
|
||||
});
|
||||
|
||||
it('should identify blockquotes and preserve markers', () => {
|
||||
const input = '> quote line 1\n> quote line 2';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.QUOTE);
|
||||
expect(blocks[0].content).toBe('> quote line 1\n> quote line 2');
|
||||
});
|
||||
|
||||
it('should support nested blockquotes', () => {
|
||||
const input = '> outer\n>> inner';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.QUOTE);
|
||||
expect(blocks[0].content).toBe('> outer\n>> inner');
|
||||
});
|
||||
|
||||
it('should set ordered to undefined for mixed ordered and unordered list items', () => {
|
||||
const input = '- unordered\n1. ordered';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.LIST);
|
||||
expect(blocks[0].ordered).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should keep ordered=true for fully ordered lists', () => {
|
||||
const input = '1. first\n2. second';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.LIST);
|
||||
expect(blocks[0].ordered).toBe(true);
|
||||
});
|
||||
|
||||
it('should keep ordered=false for fully unordered lists', () => {
|
||||
const input = '- first\n* second';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.LIST);
|
||||
expect(blocks[0].ordered).toBe(false);
|
||||
});
|
||||
|
||||
it('should create a new paragraph block after a list block', () => {
|
||||
const input = '- list item\n\nParagraph text';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(2);
|
||||
expect(blocks[0].type).toBe(BlockType.LIST);
|
||||
expect(blocks[1].type).toBe(BlockType.PARAGRAPH);
|
||||
expect(blocks[1].content).toBe('Paragraph text');
|
||||
});
|
||||
|
||||
it('should create a new paragraph block after a quote block', () => {
|
||||
const input = '> blockquote\n\nParagraph text';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(2);
|
||||
expect(blocks[0].type).toBe(BlockType.QUOTE);
|
||||
expect(blocks[1].type).toBe(BlockType.PARAGRAPH);
|
||||
expect(blocks[1].content).toBe('Paragraph text');
|
||||
});
|
||||
|
||||
it('should handle empty input correctly', () => {
|
||||
const input = '';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should yield a CODE block with incomplete flag for an unclosed code fence', () => {
|
||||
const input = '```js\ncode';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.CODE);
|
||||
expect(blocks[0].content).toBe('code');
|
||||
expect(blocks[0].incomplete).toBe(true);
|
||||
});
|
||||
|
||||
it('should treat a heading without a space as a paragraph', () => {
|
||||
const input = '#NoSpace';
|
||||
const blocks = BlockSplitter.split(input);
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].type).toBe(BlockType.PARAGRAPH);
|
||||
expect(blocks[0].content).toBe('#NoSpace');
|
||||
});
|
||||
});
|
||||
47
packages/message-parser/tests/blockquotes.test.ts
Normal file
47
packages/message-parser/tests/blockquotes.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { parse } from '../src';
|
||||
import { paragraph, plain, quote, bold } from './helpers';
|
||||
|
||||
test.each([
|
||||
[
|
||||
`
|
||||
As Rocket Cat said:
|
||||
> meowww
|
||||
> grr.
|
||||
`.trim(),
|
||||
[paragraph([plain('As Rocket Cat said:')]), quote([paragraph([plain('meowww')]), paragraph([plain('grr.')])])],
|
||||
],
|
||||
[
|
||||
`
|
||||
As Rocket Cat said:
|
||||
> *meowww*
|
||||
> grr.
|
||||
`.trim(),
|
||||
[paragraph([plain('As Rocket Cat said:')]), quote([paragraph([bold([plain('meowww')])]), paragraph([plain('grr.')])])],
|
||||
],
|
||||
[
|
||||
`
|
||||
As Rocket Cat said:
|
||||
>meowww
|
||||
>grr.
|
||||
`.trim(),
|
||||
[paragraph([plain('As Rocket Cat said:')]), quote([paragraph([plain('meowww')]), paragraph([plain('grr.')])])],
|
||||
],
|
||||
[
|
||||
`
|
||||
> meowww
|
||||
>
|
||||
> grr.
|
||||
`.trim(),
|
||||
[quote([paragraph([plain('meowww')]), paragraph([plain('')]), paragraph([plain('grr.')])])],
|
||||
],
|
||||
[
|
||||
`
|
||||
> meowww
|
||||
>
|
||||
> grr.
|
||||
`.trim(),
|
||||
[quote([paragraph([plain('meowww')]), paragraph([plain('')]), paragraph([plain('grr.')])])],
|
||||
],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
92
packages/message-parser/tests/codeFence.test.ts
Normal file
92
packages/message-parser/tests/codeFence.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { parse } from '../src';
|
||||
import { paragraph, plain, codeLine, code } from './helpers';
|
||||
|
||||
const multiply = <T>(a: number, element: T): Array<T> => Array.from({ length: a }, () => element);
|
||||
|
||||
test.each([
|
||||
[
|
||||
`\`\`\`
|
||||
code
|
||||
|
||||
|
||||
|
||||
|
||||
code
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('code')), ...multiply(4, codeLine(plain(''))), codeLine(plain('code'))])],
|
||||
],
|
||||
[
|
||||
`\`\`\`
|
||||
code
|
||||
|
||||
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('code')), ...multiply(2, codeLine(plain('')))])],
|
||||
],
|
||||
[
|
||||
`\`\`\`
|
||||
code
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('code'))])],
|
||||
],
|
||||
[
|
||||
`\`\`\`
|
||||
var a = "teste";
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('var a = "teste";'))])],
|
||||
],
|
||||
[
|
||||
`\`\`\`javascript
|
||||
code
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('code'))], 'javascript')],
|
||||
],
|
||||
[
|
||||
`\`\`\`bash c
|
||||
code
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('code'))], 'bash c')],
|
||||
],
|
||||
[
|
||||
` \`\`\`
|
||||
code
|
||||
\`\`\``,
|
||||
[paragraph([plain(` \`\`\``)]), paragraph([plain(`code`)]), paragraph([plain(`\`\`\``)])],
|
||||
],
|
||||
[
|
||||
`\`\`\`
|
||||
code
|
||||
code
|
||||
\`\`\``,
|
||||
[code([codeLine(plain(`code`)), codeLine(plain(`code`))])],
|
||||
],
|
||||
[
|
||||
`\`\`\`
|
||||
# code
|
||||
**code**
|
||||
\`\`\``,
|
||||
[code([codeLine(plain(`# code`)), codeLine(plain(`**code**`))])],
|
||||
],
|
||||
// lines ending in inline code (trailing backtick) must not break the fence
|
||||
[
|
||||
`\`\`\`
|
||||
- **Node**: \`22.22.3\`
|
||||
- **Apps-Engine**: \`1.64.0\`
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('- **Node**: `22.22.3`')), codeLine(plain('- **Apps-Engine**: `1.64.0`'))])],
|
||||
],
|
||||
[
|
||||
`\`\`\`
|
||||
foo\`
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('foo`'))])],
|
||||
],
|
||||
[
|
||||
`\`\`\`
|
||||
foo\`\`
|
||||
\`\`\``,
|
||||
[code([codeLine(plain('foo``'))])],
|
||||
],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
15
packages/message-parser/tests/color.test.ts
Normal file
15
packages/message-parser/tests/color.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { parse } from '../src';
|
||||
import { color, paragraph, plain } from './helpers';
|
||||
|
||||
test.each([
|
||||
['color:#ccc', [paragraph([color(0xcc, 0xcc, 0xcc)])], [paragraph([plain('color:#ccc')])]],
|
||||
['color:#cccc', [paragraph([color(0xcc, 0xcc, 0xcc, 0xcc)])], [paragraph([plain('color:#cccc')])]],
|
||||
['color:#c7c7c7', [paragraph([color(0xc7, 0xc7, 0xc7)])], [paragraph([plain('color:#c7c7c7')])]],
|
||||
['color:#c7c7c7c7', [paragraph([color(0xc7, 0xc7, 0xc7, 0xc7)])], [paragraph([plain('color:#c7c7c7c7')])]],
|
||||
['color:#c7c7c7c7c7', [paragraph([plain('color:#c7c7c7c7c7')])], undefined],
|
||||
['color:#c7', [paragraph([plain('color:#c7')])], undefined],
|
||||
['color:#zzz', [paragraph([plain('color:#zzz')])], undefined],
|
||||
])('parses %p', (input, output, disabledOutput) => {
|
||||
expect(parse(input, { colors: true })).toEqual(output);
|
||||
expect(parse(input, { colors: false })).toEqual(disabledOutput ?? output);
|
||||
});
|
||||
51
packages/message-parser/tests/email.test.ts
Normal file
51
packages/message-parser/tests/email.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { parse } from '../src';
|
||||
import { link, paragraph, plain } from './helpers';
|
||||
|
||||
test.each([
|
||||
['joe@joe.com', [paragraph([link('mailto:joe@joe.com', [plain('joe@joe.com')])])]],
|
||||
["joe@joe.com is Joe's email", [paragraph([link('mailto:joe@joe.com', [plain('joe@joe.com')]), plain(" is Joe's email")])]],
|
||||
[
|
||||
"Joe's email is joe@joe.com because it is",
|
||||
[paragraph([plain("Joe's email is "), link('mailto:joe@joe.com', [plain('joe@joe.com')]), plain(' because it is')])],
|
||||
],
|
||||
["Joe's email is joe@joe.com", [paragraph([plain("Joe's email is "), link('mailto:joe@joe.com', [plain('joe@joe.com')])])]],
|
||||
[
|
||||
"Joe's email is joe@joe.com. Try emailing him",
|
||||
[paragraph([plain("Joe's email is "), link('mailto:joe@joe.com', [plain('joe@joe.com')]), plain('. Try emailing him')])],
|
||||
],
|
||||
[
|
||||
"Joe's email is joe.smith@joe.com",
|
||||
[paragraph([plain("Joe's email is "), link('mailto:joe.smith@joe.com', [plain('joe.smith@joe.com')])])],
|
||||
],
|
||||
["Joe's email is JOE@JOE.COM", [paragraph([plain("Joe's email is "), link('mailto:JOE@JOE.COM', [plain('JOE@JOE.COM')])])]],
|
||||
[
|
||||
"Joe's email is (joe@joe.com)",
|
||||
[paragraph([plain("Joe's email is ("), link('mailto:joe@joe.com', [plain('joe@joe.com')]), plain(')')])],
|
||||
],
|
||||
[
|
||||
"Joe's email is (joe_roe@joe.com)",
|
||||
[paragraph([plain("Joe's email is ("), link('mailto:joe_roe@joe.com', [plain('joe_roe@joe.com')]), plain(')')])],
|
||||
],
|
||||
[
|
||||
"Joe's email is (joe'roe@joe.com)",
|
||||
[paragraph([plain("Joe's email is ("), link("mailto:joe'roe@joe.com", [plain("joe'roe@joe.com")]), plain(')')])],
|
||||
],
|
||||
[
|
||||
"Joe's email is mañana@mañana.com",
|
||||
[paragraph([plain("Joe's email is "), link('mailto:mañana@mañana.com', [plain('mañana@mañana.com')])])],
|
||||
],
|
||||
[
|
||||
"Joe's email is Кириллица@Кириллица.com",
|
||||
[paragraph([plain("Joe's email is "), link('mailto:Кириллица@Кириллица.com', [plain('Кириллица@Кириллица.com')])])],
|
||||
],
|
||||
['Hi there@stuff', [paragraph([plain('Hi there@stuff')])]],
|
||||
[
|
||||
'My email is busueng.kim@aaa.com',
|
||||
[paragraph([plain('My email is '), link('mailto:busueng.kim@aaa.com', [plain('busueng.kim@aaa.com')])])],
|
||||
],
|
||||
['My email is mailto:asdf@asdf.com', [paragraph([plain('My email is '), link('mailto:asdf@asdf.com', [plain('asdf@asdf.com')])])]],
|
||||
['My email is fake@gmail.c', [paragraph([plain('My email is fake@gmail.c')])]],
|
||||
['My email is fake@gmail.comf', [paragraph([plain('My email is fake@gmail.comf')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
160
packages/message-parser/tests/emoji.test.ts
Normal file
160
packages/message-parser/tests/emoji.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { parse } from '../src';
|
||||
import { ALL_EMOJI, EMOJIBASE_VERSION } from './fixtures/allEmoji';
|
||||
import { emoji, bigEmoji, paragraph, plain, emojiUnicode } from './helpers';
|
||||
|
||||
test.each([
|
||||
[':smile: asd', [paragraph([emoji('smile'), plain(' asd')])]],
|
||||
[':smile:asd', [paragraph([plain(':smile:asd')])]],
|
||||
['text:inner:outer', [paragraph([plain('text:inner:outer')])]],
|
||||
['10:20:30', [paragraph([plain('10:20:30')])]],
|
||||
['10:20:30:', [paragraph([plain('10:20:30:')])]],
|
||||
['":smile:"', [paragraph([plain('":smile:"')])]],
|
||||
['":smile: "', [paragraph([plain('":smile: "')])]],
|
||||
['" :smile: "', [paragraph([plain('" '), emoji('smile'), plain(' "')])]],
|
||||
[
|
||||
`:smile:
|
||||
:smile:
|
||||
`,
|
||||
[bigEmoji([emoji('smile'), emoji('smile')])],
|
||||
],
|
||||
['asdas :smile: asd', [paragraph([plain('asdas '), emoji('smile'), plain(' asd')])]],
|
||||
[
|
||||
'normal emojis :smile: :smile: :smile:',
|
||||
[paragraph([plain('normal emojis '), emoji('smile'), plain(' '), emoji('smile'), plain(' '), emoji('smile')])],
|
||||
],
|
||||
[':smile::smile::smile:', [bigEmoji([emoji('smile'), emoji('smile'), emoji('smile')])]],
|
||||
[' :smile::smile::smile: ', [bigEmoji([emoji('smile'), emoji('smile'), emoji('smile')])]],
|
||||
['\n :smile::smile::smile: \n', [bigEmoji([emoji('smile'), emoji('smile'), emoji('smile')])]],
|
||||
[':smile: :smile: :smile:', [bigEmoji([emoji('smile'), emoji('smile'), emoji('smile')])]],
|
||||
[':smile::smile:', [bigEmoji([emoji('smile'), emoji('smile')])]],
|
||||
[':smile:a:smile:', [paragraph([plain(':smile:a:smile:')])]],
|
||||
[':smile:', [bigEmoji([emoji('smile')])]],
|
||||
['Hi :+1:', [paragraph([plain('Hi '), emoji('+1')])]],
|
||||
['Hi :+1_tone4:', [paragraph([plain('Hi '), emoji('+1_tone4')])]],
|
||||
['Hi :thumbsup_tone3:', [paragraph([plain('Hi '), emoji('thumbsup_tone3')])]],
|
||||
['Hi :yes_tone3:', [paragraph([plain('Hi '), emoji('yes_tone3')])]],
|
||||
['Hi :clapping_hands_tone2:', [paragraph([plain('Hi '), emoji('clapping_hands_tone2')])]],
|
||||
['Hi :pray_tone4:', [paragraph([plain('Hi '), emoji('pray_tone4')])]],
|
||||
['Hi :regional_indicator_a:', [paragraph([plain('Hi '), emoji('regional_indicator_a')])]],
|
||||
['Hi :digit_zero:', [paragraph([plain('Hi '), emoji('digit_zero')])]],
|
||||
['Hi :pound_symbol:', [paragraph([plain('Hi '), emoji('pound_symbol')])]],
|
||||
['Hi :asterisk_symbol:', [paragraph([plain('Hi '), emoji('asterisk_symbol')])]],
|
||||
['Hi :tone3:', [paragraph([plain('Hi '), emoji('tone3')])]],
|
||||
[':thumbsup_tone3:', [bigEmoji([emoji('thumbsup_tone3')])]],
|
||||
[':regional_indicator_a:', [bigEmoji([emoji('regional_indicator_a')])]],
|
||||
[':digit_zero:', [bigEmoji([emoji('digit_zero')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
|
||||
// Tests for unicode emojis
|
||||
test.each([
|
||||
['😀', [bigEmoji([emojiUnicode('😀')])]],
|
||||
['😃', [bigEmoji([emojiUnicode('😃')])]],
|
||||
['🥵', [bigEmoji([emojiUnicode('🥵')])]],
|
||||
['🧿', [bigEmoji([emojiUnicode('🧿')])]],
|
||||
['🐶', [bigEmoji([emojiUnicode('🐶')])]],
|
||||
['🍏', [bigEmoji([emojiUnicode('🍏')])]],
|
||||
['⚽', [bigEmoji([emojiUnicode('⚽')])]],
|
||||
['⚽️', [bigEmoji([emojiUnicode('⚽️')])]],
|
||||
['👨👩👧👦', [bigEmoji([emojiUnicode('👨👩👧👦')])]],
|
||||
['🚗', [bigEmoji([emojiUnicode('🚗')])]],
|
||||
['⌚️', [bigEmoji([emojiUnicode('⌚️')])]],
|
||||
['❤️', [bigEmoji([emojiUnicode('❤️')])]],
|
||||
['🏳️', [bigEmoji([emojiUnicode('🏳️')])]],
|
||||
['🧑🏾💻', [bigEmoji([emojiUnicode('🧑🏾💻')])]],
|
||||
['🧑🏾💻🧑🏾💻', [bigEmoji([emojiUnicode('🧑🏾💻'), emojiUnicode('🧑🏾💻')])]],
|
||||
['🧑🏾💻🧑🏾💻🧑🏾💻', [bigEmoji([emojiUnicode('🧑🏾💻'), emojiUnicode('🧑🏾💻'), emojiUnicode('🧑🏾💻')])]],
|
||||
['👆🏽', [bigEmoji([emojiUnicode('👆🏽')])]],
|
||||
['👆🏽👆🏽', [bigEmoji([emojiUnicode('👆🏽'), emojiUnicode('👆🏽')])]],
|
||||
['👆🏽👆🏽👆🏽', [bigEmoji([emojiUnicode('👆🏽'), emojiUnicode('👆🏽'), emojiUnicode('👆🏽')])]],
|
||||
['👆🏺', [bigEmoji([emojiUnicode('👆'), emojiUnicode('🏺')])]],
|
||||
['✋🏽', [bigEmoji([emojiUnicode('✋🏽')])]],
|
||||
['✌🏽', [bigEmoji([emojiUnicode('✌🏽')])]],
|
||||
['✍🏽', [bigEmoji([emojiUnicode('✍🏽')])]],
|
||||
['☝🏽', [bigEmoji([emojiUnicode('☝🏽')])]],
|
||||
['⛹🏽', [bigEmoji([emojiUnicode('⛹🏽')])]],
|
||||
['✋🏽✌🏽', [bigEmoji([emojiUnicode('✋🏽'), emojiUnicode('✌🏽')])]],
|
||||
['✋🏺', [bigEmoji([emojiUnicode('✋'), emojiUnicode('🏺')])]],
|
||||
['Hi ✋🏽', [paragraph([plain('Hi '), emojiUnicode('✋🏽')])]],
|
||||
['Hi 👍', [paragraph([plain('Hi '), emojiUnicode('👍')])]],
|
||||
// Symbols and Pictographs Extended-A (U+1FA00–U+1FAFF) — Unicode 13+/14+
|
||||
['🫠', [bigEmoji([emojiUnicode('🫠')])]],
|
||||
['🫡', [bigEmoji([emojiUnicode('🫡')])]],
|
||||
['🫶', [bigEmoji([emojiUnicode('🫶')])]],
|
||||
['Hi 🫠', [paragraph([plain('Hi '), emojiUnicode('🫠')])]],
|
||||
// ZWJ sequences involving Dingbats (❤️ U+2764) — couple/kiss emojis
|
||||
['👨❤️💋👨', [bigEmoji([emojiUnicode('👨❤️💋👨')])]],
|
||||
['👩❤️💋👩', [bigEmoji([emojiUnicode('👩❤️💋👩')])]],
|
||||
['👨❤️👨', [bigEmoji([emojiUnicode('👨❤️👨')])]],
|
||||
['💑', [bigEmoji([emojiUnicode('💑')])]],
|
||||
['Hi 👨❤️💋👨', [paragraph([plain('Hi '), emojiUnicode('👨❤️💋👨')])]],
|
||||
// ZWJ sequences involving Misc Symbols (⚕️, ⚖️, ✈️) — profession emojis
|
||||
['👨⚕️', [bigEmoji([emojiUnicode('👨⚕️')])]],
|
||||
['👩⚖️', [bigEmoji([emojiUnicode('👩⚖️')])]],
|
||||
['👨✈️', [bigEmoji([emojiUnicode('👨✈️')])]],
|
||||
['🩷', [bigEmoji([emojiUnicode('🩷')])]], // v15
|
||||
['🫱🏿🫲🏻', [bigEmoji([emojiUnicode('🫱🏿🫲🏻')])]], // v14
|
||||
['', [bigEmoji([emojiUnicode('')])]], // v16
|
||||
['🇨🇶', [bigEmoji([emojiUnicode('🇨🇶')])]], // v16
|
||||
// ZWJ sequences anchored by an Emoticon (U+1F600-U+1F64F)
|
||||
['😶🌫️', [bigEmoji([emojiUnicode('😶🌫️')])]],
|
||||
['😮💨', [bigEmoji([emojiUnicode('😮💨')])]],
|
||||
['😵💫', [bigEmoji([emojiUnicode('😵💫')])]],
|
||||
['Hi 😶🌫️', [paragraph([plain('Hi '), emojiUnicode('😶🌫️')])]],
|
||||
['😶🌫️😮💨', [bigEmoji([emojiUnicode('😶🌫️'), emojiUnicode('😮💨')])]],
|
||||
['🙋🏽♂️', [bigEmoji([emojiUnicode('🙋🏽♂️')])]],
|
||||
['😀', [bigEmoji([emojiUnicode('😀')])]], // standalone emoticon still matches
|
||||
// Emoji tag sequences — England/Scotland/Wales flags
|
||||
['🏴', [bigEmoji([emojiUnicode('🏴')])]],
|
||||
['🏴', [bigEmoji([emojiUnicode('🏴')])]],
|
||||
['🏴', [bigEmoji([emojiUnicode('🏴')])]],
|
||||
['Hi 🏴', [paragraph([plain('Hi '), emojiUnicode('🏴')])]],
|
||||
['🏴', [bigEmoji([emojiUnicode('🏴')])]], // standalone black flag still matches
|
||||
['🏴☠️', [bigEmoji([emojiUnicode('🏴☠️')])]], // pirate flag (ZWJ, not tag sequence) still matches
|
||||
// Consecutive sequence emojis — each sequence must be consumed whole, without stealing from its neighbor
|
||||
['🏴🏴🏴', [bigEmoji([emojiUnicode('🏴'), emojiUnicode('🏴'), emojiUnicode('🏴')])]],
|
||||
['🏴🏴', [bigEmoji([emojiUnicode('🏴'), emojiUnicode('🏴')])]],
|
||||
['🏴🏴', [bigEmoji([emojiUnicode('🏴'), emojiUnicode('🏴')])]],
|
||||
['😶🌫️😮💨😵💫', [bigEmoji([emojiUnicode('😶🌫️'), emojiUnicode('😮💨'), emojiUnicode('😵💫')])]],
|
||||
['😀😶🌫️', [bigEmoji([emojiUnicode('😀'), emojiUnicode('😶🌫️')])]],
|
||||
['😶🌫️🏴🏴☠️', [bigEmoji([emojiUnicode('😶🌫️'), emojiUnicode('🏴'), emojiUnicode('🏴☠️')])]],
|
||||
// More than 3 emojis: no BigEmoji, plain paragraph of emoji tokens
|
||||
['😶🌫️😮💨😵💫👍', [paragraph([emojiUnicode('😶🌫️'), emojiUnicode('😮💨'), emojiUnicode('😵💫'), emojiUnicode('👍')])]],
|
||||
['England 🏴 and Scotland 🏴', [paragraph([plain('England '), emojiUnicode('🏴'), plain(' and Scotland '), emojiUnicode('🏴')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
|
||||
// KNOWN_UNSUPPORTED: the 26 regional indicators. Alone they're just letters, not emojis; combined
|
||||
// in a pair they form a flag (🇺 + 🇸 = 🇺🇸). Real letter emojis (🅰️ Ⓜ️) are supported. Stale check
|
||||
// below keeps the list honest.
|
||||
// prettier-ignore
|
||||
const KNOWN_UNSUPPORTED = new Set('🇦 🇧 🇨 🇩 🇪 🇫 🇬 🇭 🇮 🇯 🇰 🇱 🇲 🇳 🇴 🇵 🇶 🇷 🇸 🇹 🇺 🇻 🇼 🇽 🇾 🇿'.split(' '));
|
||||
|
||||
function parsesWhole(input: string): boolean {
|
||||
const nodes = parse(input);
|
||||
|
||||
if (!Array.isArray(nodes) || nodes.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [node] = nodes;
|
||||
if (node.type !== 'BIG_EMOJI' || node.value.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [child] = node.value;
|
||||
if (child.type !== 'EMOJI' || !('unicode' in child)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return child.unicode === input;
|
||||
}
|
||||
|
||||
test(`every emoji parses as one token, except documented components (emojibase@${EMOJIBASE_VERSION})`, () => {
|
||||
expect(ALL_EMOJI.length).toBeGreaterThan(3000);
|
||||
const inFixture = new Set(ALL_EMOJI);
|
||||
expect(ALL_EMOJI.filter((e) => !KNOWN_UNSUPPORTED.has(e) && !parsesWhole(e))).toEqual([]);
|
||||
expect([...KNOWN_UNSUPPORTED].filter((e) => !inFixture.has(e) || parsesWhole(e))).toEqual([]);
|
||||
});
|
||||
70
packages/message-parser/tests/emoticons.test.ts
Normal file
70
packages/message-parser/tests/emoticons.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { parse } from '../src';
|
||||
import { bigEmoji, paragraph, plain, emoticon } from './helpers';
|
||||
|
||||
test.each([
|
||||
// Should render normal Emojis
|
||||
[
|
||||
`test
|
||||
:) test`,
|
||||
[paragraph([plain('test')]), paragraph([plain(' '), emoticon(':)', 'slight_smile'), plain(' test')])],
|
||||
],
|
||||
[':) asd', [paragraph([emoticon(':)', 'slight_smile'), plain(' asd')])]],
|
||||
[' :) asd', [paragraph([plain(' '), emoticon(':)', 'slight_smile'), plain(' asd')])]],
|
||||
['Hi :)', [paragraph([plain('Hi '), emoticon(':)', 'slight_smile')])]],
|
||||
['asdas :) asd', [paragraph([plain('asdas '), emoticon(':)', 'slight_smile'), plain(' asd')])]],
|
||||
[
|
||||
':) :) :) :)',
|
||||
[
|
||||
paragraph([
|
||||
emoticon(':)', 'slight_smile'),
|
||||
plain(' '),
|
||||
emoticon(':)', 'slight_smile'),
|
||||
plain(' '),
|
||||
emoticon(':)', 'slight_smile'),
|
||||
plain(' '),
|
||||
emoticon(':)', 'slight_smile'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
|
||||
// Should render BigEmojis
|
||||
[
|
||||
`:)
|
||||
:)
|
||||
`,
|
||||
[bigEmoji([emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile')])],
|
||||
],
|
||||
[':):):)', [bigEmoji([emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile')])]],
|
||||
|
||||
[' :):):) ', [bigEmoji([emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile')])]],
|
||||
|
||||
['\n :):):) \n', [bigEmoji([emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile')])]],
|
||||
[':) :) :)', [bigEmoji([emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile')])]],
|
||||
|
||||
[':) :)', [bigEmoji([emoticon(':)', 'slight_smile'), emoticon(':)', 'slight_smile')])]],
|
||||
[':)', [bigEmoji([emoticon(':)', 'slight_smile')])]],
|
||||
[' :)', [bigEmoji([emoticon(':)', 'slight_smile')])]],
|
||||
[':) ', [bigEmoji([emoticon(':)', 'slight_smile')])]],
|
||||
[' :) ', [bigEmoji([emoticon(':)', 'slight_smile')])]],
|
||||
['D:', [bigEmoji([emoticon('D:', 'fearful')])]],
|
||||
['D: D:', [bigEmoji([emoticon('D:', 'fearful'), emoticon('D:', 'fearful')])]],
|
||||
[' D: D: D: ', [bigEmoji([emoticon('D:', 'fearful'), emoticon('D:', 'fearful'), emoticon('D:', 'fearful')])]],
|
||||
['Hi D:', [paragraph([plain('Hi '), emoticon('D:', 'fearful')])]],
|
||||
|
||||
// Should not render Emojis or BigEmojis if they are not surrounded by spaces
|
||||
['normal emojis :):):)', [paragraph([plain('normal emojis :):):)')])]],
|
||||
[':)10:30', [paragraph([plain(':)10:30')])]],
|
||||
[':smile::)text', [paragraph([plain(':smile::)text')])]],
|
||||
['text:):smile:', [paragraph([plain('text:):smile:')])]],
|
||||
['text:):)', [paragraph([plain('text:):)')])]],
|
||||
[':):):) normal emojis', [paragraph([plain(':):):) normal emojis')])]],
|
||||
[':):):):)', [paragraph([plain(':):):):)')])]],
|
||||
['10:30', [paragraph([plain('10:30')])]],
|
||||
['he:)llo', [paragraph([plain('he:)llo')])]],
|
||||
[':)Hi', [paragraph([plain(':)Hi')])]],
|
||||
['Hi:) Hi', [paragraph([plain('Hi:) Hi')])]],
|
||||
['Hi:)', [paragraph([plain('Hi:)')])]],
|
||||
['@#@#! :)@!@', [paragraph([plain('@#@#! :)@!@')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input, { emoticons: true })).toEqual(output);
|
||||
});
|
||||
130
packages/message-parser/tests/emphasis.test.ts
Normal file
130
packages/message-parser/tests/emphasis.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { parse } from '../src';
|
||||
import {
|
||||
italic,
|
||||
paragraph,
|
||||
plain,
|
||||
strike,
|
||||
bold,
|
||||
emoji,
|
||||
link,
|
||||
bigEmoji,
|
||||
emojiUnicode,
|
||||
mentionChannel,
|
||||
mentionUser,
|
||||
inlineCode,
|
||||
} from './helpers';
|
||||
|
||||
test.each([
|
||||
['_:smile:_', [paragraph([italic([emoji('smile')])])]],
|
||||
['_:slight_smile:_', [paragraph([italic([emoji('slight_smile')])])]],
|
||||
['_test :smile: test_', [paragraph([italic([plain('test '), emoji('smile'), plain(' test')])])]],
|
||||
['_test :slight_smile: test_', [paragraph([italic([plain('test '), emoji('slight_smile'), plain(' test')])])]],
|
||||
['_😀_', [paragraph([italic([emojiUnicode('😀')])])]],
|
||||
['_test 😀_', [paragraph([italic([plain('test '), emojiUnicode('😀')])])]],
|
||||
['_test @guilherme.gazzo test_', [paragraph([italic([plain('test '), mentionUser('guilherme.gazzo'), plain(' test')])])]],
|
||||
['_test #GENERAL test_', [paragraph([italic([plain('test '), mentionChannel('GENERAL'), plain(' test')])])]],
|
||||
[
|
||||
'_[A brand new Gist](https://gist.github.com/24dddfa97bef58f46ac2ce0f80c58ba4)_',
|
||||
[paragraph([italic([link('https://gist.github.com/24dddfa97bef58f46ac2ce0f80c58ba4', [plain('A brand new Gist')])])])],
|
||||
],
|
||||
['__italic__', [paragraph([italic([plain('italic')])])]],
|
||||
['__italic__non', [paragraph([plain('__italic__non')])]],
|
||||
['__test__test__', [paragraph([plain('__test__test__')])]],
|
||||
['pre__italic__post', [paragraph([plain('pre__italic__post')])]],
|
||||
[' pre__italic__post', [paragraph([plain(' pre__italic__post')])]],
|
||||
[' pre__**~~boldstrikeitalic~~**__post ', [paragraph([plain(' pre__'), bold([strike([plain('boldstrikeitalic')])]), plain('__post ')])]],
|
||||
['__', [paragraph([plain('__')])]],
|
||||
['_ _', [paragraph([plain('_ _')])]],
|
||||
['__ _', [paragraph([plain('__ _')])]],
|
||||
['__ __', [paragraph([plain('__ __')])]],
|
||||
['_ Hello_', [paragraph([italic([plain(' Hello')])])]],
|
||||
['_Hello _', [paragraph([italic([plain('Hello ')])])]],
|
||||
[':custom_emoji_case:', [bigEmoji([emoji('custom_emoji_case')])]],
|
||||
['_Hel lo_', [paragraph([italic([plain('Hel lo')])])]],
|
||||
['_Hello_', [paragraph([italic([plain('Hello')])])]],
|
||||
['__Hello__', [paragraph([italic([plain('Hello')])])]],
|
||||
['__Hello_', [paragraph([plain('_'), italic([plain('Hello')])])]],
|
||||
['_Hello__', [paragraph([italic([plain('Hello')]), plain('_')])]],
|
||||
['_Hello', [paragraph([plain('_Hello')])]],
|
||||
['Hello_', [paragraph([plain('Hello_')])]],
|
||||
['He_llo', [paragraph([plain('He_llo')])]],
|
||||
['___Hello___', [paragraph([plain('_'), italic([plain('Hello')]), plain('_')])]],
|
||||
['___Hello__', [paragraph([plain('_'), italic([plain('Hello')])])]],
|
||||
['_Hello_ this is dog', [paragraph([italic([plain('Hello')]), plain(` this is dog`)])]],
|
||||
['Rocket cat says _Hello_', [paragraph([plain(`Rocket cat says `), italic([plain('Hello')])])]],
|
||||
['He said _Hello_ to her', [paragraph([plain(`He said `), italic([plain('Hello')]), plain(` to her`)])]],
|
||||
['__Hello__ this is dog', [paragraph([italic([plain('Hello')]), plain(` this is dog`)])]],
|
||||
['Rocket cat says __Hello__', [paragraph([plain(`Rocket cat says `), italic([plain('Hello')])])]],
|
||||
['He said __Hello__ to her', [paragraph([plain(`He said `), italic([plain('Hello')]), plain(` to her`)])]],
|
||||
['text_hello_text', [paragraph([plain('text_hello_text')])]],
|
||||
['_hello_text', [paragraph([plain('_hello_text')])]],
|
||||
['text_hello_', [paragraph([plain('text_hello_')])]],
|
||||
['_italic@test_', [paragraph([italic([plain('italic@test')])])]],
|
||||
['_italic#test_', [paragraph([italic([plain('italic#test')])])]],
|
||||
['paragraph@test__', [paragraph([plain('paragraph@test__')])]],
|
||||
['_ @guilherme_gazzo_ _', [paragraph([italic([plain(' '), mentionUser('guilherme_gazzo_'), plain(' ')])])]],
|
||||
['_ @guilherme.gazzo _', [paragraph([italic([plain(' '), mentionUser('guilherme.gazzo'), plain(' ')])])]],
|
||||
['_9797.76_', [paragraph([italic([plain('9797.76')])])]],
|
||||
['_3.14_', [paragraph([italic([plain('3.14')])])]],
|
||||
['_example.com_', [paragraph([italic([plain('example.com')])])]],
|
||||
['_rocket.chat_', [paragraph([italic([plain('rocket.chat')])])]],
|
||||
['_example.com', [paragraph([plain('_example.com')])]],
|
||||
[
|
||||
'**reference link inside [emphasis with more [references](https://rocket.chat)](https://rocket.chat)**',
|
||||
[
|
||||
paragraph([
|
||||
bold([
|
||||
plain('reference link inside '),
|
||||
link('https://rocket.chat', [plain('emphasis with more [references')]),
|
||||
plain('](https://rocket.chat)'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
['_ouch_ouch', [paragraph([plain('_ouch_ouch')])]],
|
||||
[`_@mention _gone`, [paragraph([plain('_'), mentionUser('mention'), plain(' _gone')])]],
|
||||
['_nothing `should` be _gone', [paragraph([plain('_nothing '), inlineCode(plain('should')), plain(' be _gone')])]],
|
||||
[
|
||||
'**bold ~~and strike~~** **not bold ~~but strike** ~~ not strike~~',
|
||||
[
|
||||
paragraph([
|
||||
bold([plain('bold '), strike([plain('and strike')])]),
|
||||
plain(' **not bold '),
|
||||
strike([plain('but strike** ')]),
|
||||
plain(' not strike~~'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'**bold** **another bold** ~~strike~~ ~~another strike~~ **bold ~~and strike~~** **not bold ~~but strike** ~~ not strike~~',
|
||||
[
|
||||
paragraph([
|
||||
bold([plain('bold')]),
|
||||
plain(' '),
|
||||
bold([plain('another bold')]),
|
||||
plain(' '),
|
||||
strike([plain('strike')]),
|
||||
plain(' '),
|
||||
strike([plain('another strike')]),
|
||||
plain(' '),
|
||||
bold([plain('bold '), strike([plain('and strike')])]),
|
||||
plain(' **not bold '),
|
||||
strike([plain('but strike** ')]),
|
||||
plain(' not strike~~'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
['some_snake_case_text and even_more', [paragraph([plain('some_snake_case_text and even_more')])]],
|
||||
[
|
||||
'some_snake_case_text and some __italic__ text',
|
||||
[paragraph([plain('some_snake_case_text and some '), italic([plain('italic')]), plain(' text')])],
|
||||
],
|
||||
['some__double__snake__case__text and even_more', [paragraph([plain('some__double__snake__case__text and even_more')])]],
|
||||
[
|
||||
'some__double__snake__case__text and some __italic__ text',
|
||||
[paragraph([plain('some__double__snake__case__text and some '), italic([plain('italic')]), plain(' text')])],
|
||||
],
|
||||
['something__ __and italic__', [paragraph([plain('something__ '), italic([plain('and italic')])])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input, { emoticons: false })).toEqual(output);
|
||||
});
|
||||
58
packages/message-parser/tests/emphasisWithEmoticons.test.ts
Normal file
58
packages/message-parser/tests/emphasisWithEmoticons.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { parse } from '../src';
|
||||
import { paragraph, plain, bold, italic, emoticon } from './helpers';
|
||||
|
||||
test.each([
|
||||
['*test:*', [paragraph([bold([plain('test:')])])]],
|
||||
['_test:_', [paragraph([italic([plain('test:')])])]],
|
||||
[
|
||||
// :* is an emoticon, but it shouldn't be parsed as one if its glued to a word
|
||||
'*bold ending with colon:*',
|
||||
[paragraph([bold([plain('bold ending with colon:')])])],
|
||||
],
|
||||
['*bold ending with colon:* and some more text', [paragraph([bold([plain('bold ending with colon:')]), plain(' and some more text')])]],
|
||||
['*bold ending with colon :*and some more text', [paragraph([bold([plain('bold ending with colon :')]), plain('and some more text')])]],
|
||||
[
|
||||
'*bold with a kissing emoji :* *',
|
||||
[paragraph([bold([plain('bold with a kissing emoji '), emoticon(':*', 'kissing_heart'), plain(' ')])])],
|
||||
],
|
||||
['*bold with a broken kissing emoji:**', [paragraph([bold([plain('bold with a broken kissing emoji:')]), plain('*')])]],
|
||||
[
|
||||
'*bold with a broken kissing emoji:*:*',
|
||||
[paragraph([bold([plain('bold with a broken kissing emoji:')]), emoticon(':*', 'kissing_heart')])],
|
||||
],
|
||||
['*bold with a broken kissing emoji :*text *', [paragraph([bold([plain('bold with a broken kissing emoji :')]), plain('text *')])]],
|
||||
['*broken bold with a kissing emoji :*', [paragraph([plain('*broken bold with a kissing emoji '), emoticon(':*', 'kissing_heart')])]],
|
||||
[
|
||||
'*broken bold with a kissing emoji :* ',
|
||||
[paragraph([plain('*broken bold with a kissing emoji '), emoticon(':*', 'kissing_heart'), plain(' ')])],
|
||||
],
|
||||
['*two bolds**second*', [paragraph([bold([plain('two bolds')]), bold([plain('second')])])]],
|
||||
[
|
||||
'*two bolds*:**separated by kissing emoji*',
|
||||
[paragraph([bold([plain('two bolds')]), emoticon(':*', 'kissing_heart'), bold([plain('separated by kissing emoji')])])],
|
||||
],
|
||||
['_test-_-', [paragraph([italic([plain('test-')]), plain('-')])]],
|
||||
['_test -_- _', [paragraph([italic([plain('test '), emoticon('-_-', 'expressionless'), plain(' ')])])]],
|
||||
['_test -_-_', [paragraph([italic([plain('test '), emoticon('-_-', 'expressionless')])])]],
|
||||
['_two italics__second_', [paragraph([italic([plain('two italics')]), italic([plain('second')])])]],
|
||||
['_italic with broken emoticon-_-_', [paragraph([italic([plain('italic with broken emoticon-')]), plain('-_')])]],
|
||||
[
|
||||
'_italic with broken emoticon-_-_ and more text',
|
||||
[paragraph([italic([plain('italic with broken emoticon-')]), plain('-_ and more text')])],
|
||||
],
|
||||
[
|
||||
'_italic with broken emoticon -_-and more text',
|
||||
[paragraph([italic([plain('italic with broken emoticon -')]), plain('-and more text')])],
|
||||
],
|
||||
[
|
||||
'_italic with broken emoticon -_-and more text -_-',
|
||||
[paragraph([italic([plain('italic with broken emoticon -')]), plain('-and more text '), emoticon('-_-', 'expressionless')])],
|
||||
],
|
||||
[
|
||||
'_italic with broken emoticon -_-and more text -_-_',
|
||||
[paragraph([italic([plain('italic with broken emoticon -')]), plain('-and more text -'), italic([plain('-')])])],
|
||||
],
|
||||
['-_-italic content-_-', [paragraph([plain('-'), italic([plain('-italic content-')]), plain('-')])]],
|
||||
])('parses emphasisWithEmoticons %p', (input, output) => {
|
||||
expect(parse(input, { emoticons: true })).toEqual(output);
|
||||
});
|
||||
21
packages/message-parser/tests/escaped.test.ts
Normal file
21
packages/message-parser/tests/escaped.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { parse } from '../src';
|
||||
import { paragraph, plain, bold } from './helpers';
|
||||
|
||||
test.each([
|
||||
['¯\\\\_(ツ)_/¯', [paragraph([plain('¯\\_(ツ)_/¯')])]],
|
||||
['\\*escaped as*bold*escaped*', [paragraph([plain('*escaped as'), bold([plain('bold')]), plain('escaped*')])]],
|
||||
['\\*not bold*', [paragraph([plain('*not bold*')])]],
|
||||
['*_~`#.'.split('').join('\\'), [paragraph([plain('*_~`#.')])]],
|
||||
['\\*not emphasized*', [paragraph([plain('*not emphasized*')])]],
|
||||
['\\<br/> tag plain text', [paragraph([plain('\\<br/> tag plain text')])]],
|
||||
['\\[it is not a link](/foo)', [paragraph([plain('\\[it is not a link](/foo)')])]],
|
||||
['\\`not code`', [paragraph([plain('`not code`')])]],
|
||||
['1\\. not a list', [paragraph([plain('1. not a list')])]],
|
||||
['\\* not a list', [paragraph([plain('* not a list')])]],
|
||||
['\\# not a heading', [paragraph([plain('# not a heading')])]],
|
||||
['\\[foo]: /url "not a reference"', [paragraph([plain('\\[foo]: /url "not a reference"')])]],
|
||||
['\\ö not a character entity', [paragraph([plain('\\ö not a character entity')])]],
|
||||
['\\<t:1708551317:R>', [paragraph([plain('<t:1708551317:R>')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
8
packages/message-parser/tests/fixtures/allEmoji.ts
vendored
Normal file
8
packages/message-parser/tests/fixtures/allEmoji.ts
vendored
Normal file
File diff suppressed because one or more lines are too long
52
packages/message-parser/tests/fuzz.test.ts
Normal file
52
packages/message-parser/tests/fuzz.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import fc from 'fast-check';
|
||||
|
||||
import { parse } from '../src';
|
||||
|
||||
const plainTextArbitrary = fc.stringMatching(/[a-zA-Z0-9 ]{1,20}/).filter((value) => value.length > 0);
|
||||
|
||||
const timestampArbitrary = fc.integer({ min: 1_000_000_000, max: 2_000_000_000 }).map((value) => `<t:${value}>`);
|
||||
|
||||
const relativeTimestampArbitrary = fc.integer({ min: 1_000_000_000, max: 2_000_000_000 }).map((value) => `<t:${value}:R>`);
|
||||
|
||||
const mentionArbitrary = fc.stringMatching(/[a-z0-9._-]{1,12}/).map((value) => `@${value}`);
|
||||
|
||||
const boldArbitrary = plainTextArbitrary.map((value) => `*${value}*`);
|
||||
|
||||
const strikeArbitrary = fc.oneof(timestampArbitrary, relativeTimestampArbitrary, plainTextArbitrary).map((value) => `~${value}~`);
|
||||
|
||||
const inlineCodeArbitrary = plainTextArbitrary.map((value) => `\`${value}\``);
|
||||
|
||||
const parserInputArbitrary = fc
|
||||
.array(
|
||||
fc.oneof(
|
||||
plainTextArbitrary,
|
||||
timestampArbitrary,
|
||||
relativeTimestampArbitrary,
|
||||
mentionArbitrary,
|
||||
boldArbitrary,
|
||||
strikeArbitrary,
|
||||
inlineCodeArbitrary,
|
||||
),
|
||||
{ minLength: 1, maxLength: 8 },
|
||||
)
|
||||
.map((parts) => parts.join(' '));
|
||||
|
||||
describe('parser fuzz tests', () => {
|
||||
test('parses valid parser-like inputs without throwing', () => {
|
||||
fc.assert(
|
||||
fc.property(parserInputArbitrary, (input) => {
|
||||
expect(() => parse(input)).not.toThrow();
|
||||
}),
|
||||
{ numRuns: 300 },
|
||||
);
|
||||
});
|
||||
|
||||
test('is deterministic for valid parser-like inputs', () => {
|
||||
fc.assert(
|
||||
fc.property(parserInputArbitrary, (input) => {
|
||||
expect(parse(input)).toEqual(parse(input));
|
||||
}),
|
||||
{ numRuns: 300 },
|
||||
);
|
||||
});
|
||||
});
|
||||
62
packages/message-parser/tests/guards.test.ts
Normal file
62
packages/message-parser/tests/guards.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { isNodeOfType, parse } from '../src';
|
||||
import type { Image, OrderedList, Timestamp } from '../src/definitions';
|
||||
import { timestamp } from '../src/utils';
|
||||
|
||||
describe('isNodeOfType', () => {
|
||||
it('narrows timestamp nodes', () => {
|
||||
const value: unknown = timestamp('1708551317');
|
||||
|
||||
expect(isNodeOfType(value, 'TIMESTAMP')).toBe(true);
|
||||
|
||||
if (!isNodeOfType(value, 'TIMESTAMP')) {
|
||||
throw new Error('expected TIMESTAMP node');
|
||||
}
|
||||
|
||||
const narrowed: Timestamp = value;
|
||||
expect(value.value.timestamp).toBe('1708551317');
|
||||
expect(value.value.format).toBe('t');
|
||||
expect(narrowed.value.format).toBe('t');
|
||||
});
|
||||
|
||||
it('narrows ordered list nodes from parser output', () => {
|
||||
const value: unknown = parse('1. one')[0];
|
||||
|
||||
expect(isNodeOfType(value, 'ORDERED_LIST')).toBe(true);
|
||||
|
||||
if (!isNodeOfType(value, 'ORDERED_LIST')) {
|
||||
throw new Error('expected ORDERED_LIST node');
|
||||
}
|
||||
|
||||
const narrowed: OrderedList = value;
|
||||
expect(value.value[0].number).toBe(1);
|
||||
expect(narrowed.value[0].number).toBe(1);
|
||||
});
|
||||
|
||||
it('narrows image nodes from parser output', () => {
|
||||
const paragraph: unknown = parse('')[0];
|
||||
|
||||
expect(isNodeOfType(paragraph, 'PARAGRAPH')).toBe(true);
|
||||
|
||||
if (!isNodeOfType(paragraph, 'PARAGRAPH')) {
|
||||
throw new Error('expected PARAGRAPH node');
|
||||
}
|
||||
|
||||
const imageNode: unknown = paragraph.value[0];
|
||||
|
||||
expect(isNodeOfType(imageNode, 'IMAGE')).toBe(true);
|
||||
|
||||
if (!isNodeOfType(imageNode, 'IMAGE')) {
|
||||
throw new Error('expected IMAGE node');
|
||||
}
|
||||
|
||||
const narrowed: Image = imageNode;
|
||||
expect(imageNode.value.src.value).toBe('https://rocket.chat/logo.png');
|
||||
expect(narrowed.value.src.value).toBe('https://rocket.chat/logo.png');
|
||||
});
|
||||
|
||||
it('returns false for mismatched types', () => {
|
||||
const value: unknown = timestamp('1708551317');
|
||||
|
||||
expect(isNodeOfType(value, 'ORDERED_LIST')).toBe(false);
|
||||
});
|
||||
});
|
||||
52
packages/message-parser/tests/heading.test.ts
Normal file
52
packages/message-parser/tests/heading.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { parse } from '../src';
|
||||
import { emoji, heading, link, mentionChannel, paragraph, plain } from './helpers';
|
||||
|
||||
test.each([
|
||||
['# h1', [heading([plain('h1')], 1)]],
|
||||
['# Hello', [heading([plain('Hello')], 1)]],
|
||||
['# Rocket.Cat', [heading([link('//Rocket.Cat', [plain('Rocket.Cat')])], 1)]],
|
||||
['# Hi', [heading([plain('Hi')], 1)]],
|
||||
['# Hello this is dog', [heading([plain('Hello this is dog')], 1)]],
|
||||
['# Rocket cat says Hello', [heading([plain('Rocket cat says Hello')], 1)]],
|
||||
['# He said Hello to her', [heading([plain('He said Hello to her')], 1)]],
|
||||
['#Hello', [paragraph([mentionChannel('Hello')])]],
|
||||
['#Hello#', [paragraph([mentionChannel('Hello'), plain('#')])]],
|
||||
['He#llo', [paragraph([plain('He#llo')])]],
|
||||
|
||||
['## Hello', [heading([plain('Hello')], 2)]],
|
||||
['## Rocket.Cat', [heading([link('//Rocket.Cat', [plain('Rocket.Cat')])], 2)]],
|
||||
['## Hi', [heading([plain('Hi')], 2)]],
|
||||
['## Hello this is dog', [heading([plain('Hello this is dog')], 2)]],
|
||||
['## Rocket cat says Hello', [heading([plain('Rocket cat says Hello')], 2)]],
|
||||
['## He said Hello to her', [heading([plain('He said Hello to her')], 2)]],
|
||||
['##Hello', [paragraph([plain('##Hello')])]],
|
||||
['##Hello##', [paragraph([plain('##Hello##')])]],
|
||||
['He##llo', [paragraph([plain('He##llo')])]],
|
||||
|
||||
['### Hello', [heading([plain('Hello')], 3)]],
|
||||
['### Rocket.Cat', [heading([link('//Rocket.Cat', [plain('Rocket.Cat')])], 3)]],
|
||||
['### Hi', [heading([plain('Hi')], 3)]],
|
||||
['### Hello this is dog', [heading([plain('Hello this is dog')], 3)]],
|
||||
['### Rocket cat says Hello', [heading([plain('Rocket cat says Hello')], 3)]],
|
||||
['### He said Hello to her', [heading([plain('He said Hello to her')], 3)]],
|
||||
['###Hello', [paragraph([plain('###Hello')])]],
|
||||
['###Hello###', [paragraph([plain('###Hello###')])]],
|
||||
['He###llo', [paragraph([plain('He###llo')])]],
|
||||
|
||||
['#### Hello', [heading([plain('Hello')], 4)]],
|
||||
['#### Rocket.Cat', [heading([link('//Rocket.Cat', [plain('Rocket.Cat')])], 4)]],
|
||||
['#### Hi', [heading([plain('Hi')], 4)]],
|
||||
['#### Hello this is dog', [heading([plain('Hello this is dog')], 4)]],
|
||||
['#### Rocket cat says Hello', [heading([plain('Rocket cat says Hello')], 4)]],
|
||||
['#### He said Hello to her', [heading([plain('He said Hello to her')], 4)]],
|
||||
['####Hello', [paragraph([plain('####Hello')])]],
|
||||
['####Hello####', [paragraph([plain('####Hello####')])]],
|
||||
['He####llo', [paragraph([plain('He####llo')])]],
|
||||
['# Hello\n', [heading([plain('Hello')], 1)]],
|
||||
['# # Hello\n', [heading([plain('# Hello')], 1)]],
|
||||
['# :newspaper: Headline', [heading([emoji('newspaper'), plain(' Headline')], 1)]],
|
||||
['## Hello :smile:', [heading([plain('Hello '), emoji('smile')], 2)]],
|
||||
['### :smile: text :newspaper:', [heading([emoji('smile'), plain(' text '), emoji('newspaper')], 3)]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
127
packages/message-parser/tests/helpers.ts
Normal file
127
packages/message-parser/tests/helpers.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
const generate =
|
||||
<Type extends string>(type: Type) =>
|
||||
<Value>(value: Value) =>
|
||||
({ type, value }) as const;
|
||||
|
||||
export const paragraph = generate('PARAGRAPH');
|
||||
export const bold = generate('BOLD');
|
||||
|
||||
export const color = (r: number, g: number, b: number, a = 255) => ({
|
||||
type: 'COLOR' as const,
|
||||
value: { r, g, b, a },
|
||||
});
|
||||
|
||||
export const heading = (value: unknown[], level = 1) => ({
|
||||
type: 'HEADING' as const,
|
||||
level,
|
||||
value,
|
||||
});
|
||||
|
||||
export const code = (value: unknown[], language = 'none') => ({
|
||||
type: 'CODE' as const,
|
||||
language,
|
||||
value,
|
||||
});
|
||||
|
||||
export const bigEmoji = generate('BIG_EMOJI');
|
||||
export const task = (value: unknown[], status: boolean) => ({ type: 'TASK' as const, status, value });
|
||||
export const inlineCode = generate('INLINE_CODE');
|
||||
export const tasks = generate('TASKS');
|
||||
|
||||
export const italic = generate('ITALIC');
|
||||
export const spoiler = generate('SPOILER');
|
||||
|
||||
export const plain = generate('PLAIN_TEXT');
|
||||
export const strike = generate('STRIKE');
|
||||
|
||||
export const codeLine = generate('CODE_LINE');
|
||||
|
||||
export const link = (src: string, label?: unknown[]) => ({
|
||||
type: 'LINK' as const,
|
||||
value: { src: plain(src), label: label ?? [plain(src)] },
|
||||
});
|
||||
|
||||
export const image = (src: string, label?: unknown) => ({
|
||||
type: 'IMAGE' as const,
|
||||
value: { src: plain(src), label: label ?? plain(src) },
|
||||
});
|
||||
|
||||
export const quote = generate('QUOTE');
|
||||
export const spoilerBlock = generate('SPOILER_BLOCK');
|
||||
|
||||
export const mentionChannel = (value: string) => ({
|
||||
type: 'MENTION_CHANNEL' as const,
|
||||
value: plain(value),
|
||||
});
|
||||
|
||||
export const orderedList = generate('ORDERED_LIST');
|
||||
export const unorderedList = generate('UNORDERED_LIST');
|
||||
|
||||
export const listItem = (value: unknown[], number?: number) => ({
|
||||
type: 'LIST_ITEM' as const,
|
||||
value,
|
||||
...(number !== undefined ? { number } : {}),
|
||||
});
|
||||
|
||||
export const mentionUser = (value: string) => ({
|
||||
type: 'MENTION_USER' as const,
|
||||
value: plain(value),
|
||||
});
|
||||
|
||||
export const emoji = (shortCode: string) => ({
|
||||
type: 'EMOJI' as const,
|
||||
value: plain(shortCode),
|
||||
shortCode,
|
||||
});
|
||||
|
||||
export const emojiUnicode = (unicode: string) => ({
|
||||
type: 'EMOJI' as const,
|
||||
value: undefined,
|
||||
unicode,
|
||||
});
|
||||
|
||||
export const emoticon = (value: string, shortCode: string) => ({
|
||||
type: 'EMOJI' as const,
|
||||
value: plain(value),
|
||||
shortCode,
|
||||
});
|
||||
|
||||
export const lineBreak = () => ({
|
||||
type: 'LINE_BREAK' as const,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
export const horizontalRule = (fallback?: [number, number]) => ({
|
||||
type: 'HORIZONTAL_RULE' as const,
|
||||
value: undefined,
|
||||
...(fallback !== undefined ? { fallback } : {}),
|
||||
});
|
||||
|
||||
type Align = 'left' | 'center' | 'right' | undefined;
|
||||
|
||||
export const tableCell = (value: unknown[], align: Align = undefined) => ({
|
||||
type: 'TABLE_CELL' as const,
|
||||
align,
|
||||
value,
|
||||
});
|
||||
|
||||
export const tableRow = (value: unknown[]) => ({
|
||||
type: 'TABLE_ROW' as const,
|
||||
value,
|
||||
});
|
||||
|
||||
export const table = (header: unknown[], rows: unknown[], fallback?: [number, number]) => ({
|
||||
type: 'TABLE' as const,
|
||||
value: { header, rows },
|
||||
...(fallback !== undefined ? { fallback } : {}),
|
||||
});
|
||||
|
||||
export const katex = (value: string) => ({
|
||||
type: 'KATEX' as const,
|
||||
value,
|
||||
});
|
||||
|
||||
export const inlineKatex = (value: string) => ({
|
||||
type: 'INLINE_KATEX' as const,
|
||||
value,
|
||||
});
|
||||
39
packages/message-parser/tests/horizontalRule.test.ts
Normal file
39
packages/message-parser/tests/horizontalRule.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { parse } from '../src';
|
||||
import { bold, horizontalRule, paragraph, plain } from './helpers';
|
||||
|
||||
// Only dashes form a horizontal rule; `fallback` is the [start, end] offset span of the run.
|
||||
test.each([
|
||||
['---', [0, 3] as [number, number]],
|
||||
['----------', [0, 10] as [number, number]],
|
||||
[' --- ', [3, 6] as [number, number]],
|
||||
])('parses %p as a horizontal rule', (input, range) => {
|
||||
expect(parse(input)).toEqual([horizontalRule(range)]);
|
||||
// the offsets slice back to the markers
|
||||
expect(input.slice(range[0], range[1])).toBe(input.trim());
|
||||
});
|
||||
|
||||
test('parses a horizontal rule between paragraphs', () => {
|
||||
expect(parse('above\n---\nbelow')).toEqual([paragraph([plain('above')]), horizontalRule([6, 9]), paragraph([plain('below')])]);
|
||||
});
|
||||
|
||||
// `*` and `_` are NOT horizontal rules: they collide with emphasis and with censored
|
||||
// words (bad-words masks a term as a run of `*`), so a bare `***` / `_______` / `*******`
|
||||
// line must stay text/emphasis rather than becoming a divider.
|
||||
test.each(['***', '___', '*******', '_______'])('does not treat %p as a horizontal rule', (input) => {
|
||||
const [node] = parse(input);
|
||||
expect(node.type).not.toBe('HORIZONTAL_RULE');
|
||||
});
|
||||
|
||||
test.each([
|
||||
// fewer than three markers -> plain text
|
||||
['--', [paragraph([plain('--')])]],
|
||||
['**', [paragraph([plain('**')])]],
|
||||
// emphasis must not be swallowed by the rule (parsed as a paragraph, not a rule)
|
||||
['***bold italic***', [paragraph([plain('*'), bold([plain('bold italic')]), plain('*')])]],
|
||||
// spaced markers are whitespace-emphasis (plain), not a rule
|
||||
['** **', [paragraph([plain('** **')])]],
|
||||
// list item, not a rule
|
||||
['- item', [{ type: 'UNORDERED_LIST', value: [{ type: 'LIST_ITEM', value: [plain('item')] }] }]],
|
||||
])('does not treat %p as a horizontal rule', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
12
packages/message-parser/tests/image.test.ts
Normal file
12
packages/message-parser/tests/image.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { parse } from '../src';
|
||||
import { image, paragraph, plain } from './helpers';
|
||||
|
||||
test.each([
|
||||
[
|
||||
'',
|
||||
[paragraph([image('https://rocket.chat/assets/img/header/logo.svg', plain('image'))])],
|
||||
],
|
||||
['', [paragraph([image('https://rocket.chat/assets/img/header/logo.svg')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
12
packages/message-parser/tests/inlineCode.test.ts
Normal file
12
packages/message-parser/tests/inlineCode.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { parse } from '../src';
|
||||
import { inlineCode, paragraph, plain } from './helpers';
|
||||
|
||||
test.each([
|
||||
['`[asd](https://localhost)`', [paragraph([inlineCode(plain('[asd](https://localhost)'))])]],
|
||||
[`\`code\``, [paragraph([inlineCode(plain('code'))])]],
|
||||
[`File extension (\`.mov\`)`, [paragraph([plain('File extension ('), inlineCode(plain('.mov')), plain(')')])]],
|
||||
['`@rocket.chat`', [paragraph([inlineCode(plain('@rocket.chat'))])]],
|
||||
['`@rocket.chat/message-parser`', [paragraph([inlineCode(plain('@rocket.chat/message-parser'))])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
14
packages/message-parser/tests/inlineCodeStrike.test.ts
Normal file
14
packages/message-parser/tests/inlineCodeStrike.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { parse } from '../src';
|
||||
import { bold, inlineCode, italic, paragraph, plain, strike } from './helpers';
|
||||
|
||||
test.each([
|
||||
['~~`Striking Inline Code`~~', [paragraph([strike([inlineCode(plain('Striking Inline Code'))])])]],
|
||||
['~~_`Striking Inline Code with Italics`_~~', [paragraph([strike([italic([inlineCode(plain('Striking Inline Code with Italics'))])])])]],
|
||||
['~~**`Striking Inline Code with Bold`**~~', [paragraph([strike([bold([inlineCode(plain('Striking Inline Code with Bold'))])])])]],
|
||||
[
|
||||
'~~__*`Striking Inline Code with Bold`*__~~',
|
||||
[paragraph([strike([italic([bold([inlineCode(plain('Striking Inline Code with Bold'))])])])])],
|
||||
],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
25
packages/message-parser/tests/joinEmoji.test.ts
Normal file
25
packages/message-parser/tests/joinEmoji.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { plain, emoji, reducePlainTexts } from '../src/utils';
|
||||
|
||||
describe('joinEmoji behavior through reducePlainTexts', () => {
|
||||
it('keeps emoji when alone', () => {
|
||||
const result = reducePlainTexts([emoji('smile')]);
|
||||
expect(result[0].type).toBe('EMOJI');
|
||||
});
|
||||
|
||||
it('merges consecutive plain texts', () => {
|
||||
const result = reducePlainTexts([plain('hello '), plain('world')]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({ type: 'PLAIN_TEXT', value: 'hello world' });
|
||||
});
|
||||
|
||||
it('converts emoji with plain text neighbors to shortCode and merges', () => {
|
||||
const result = reducePlainTexts([plain('hello'), emoji('smile'), plain('world')]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
type: 'PLAIN_TEXT',
|
||||
value: 'hello:smile:world',
|
||||
});
|
||||
});
|
||||
});
|
||||
42
packages/message-parser/tests/katex.test.ts
Normal file
42
packages/message-parser/tests/katex.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { parse } from '../src';
|
||||
import { inlineKatex, katex, paragraph, plain } from './helpers';
|
||||
|
||||
test.each([
|
||||
[
|
||||
`\\[
|
||||
\\f\\relax{x} = \\int_{-\\infty}^\\infty
|
||||
\\f\\hat\\xi\\,e^{2 \\pi i \\xi x}
|
||||
\\,d\\xi
|
||||
\\]`,
|
||||
[
|
||||
katex(`
|
||||
\\f\\relax{x} = \\int_{-\\infty}^\\infty
|
||||
\\f\\hat\\xi\\,e^{2 \\pi i \\xi x}
|
||||
\\,d\\xi
|
||||
`),
|
||||
],
|
||||
],
|
||||
['Easy as \\(E = mc^2\\), right?', [paragraph([plain('Easy as '), inlineKatex('E = mc^2'), plain(', right?')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input, { katex: { parenthesisSyntax: true } })).toEqual(output);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['$$E = mc^2$$', [katex('E = mc^2')]],
|
||||
['$E = mc^2$', [paragraph([inlineKatex('E = mc^2')])]],
|
||||
['Easy as $E = mc^2$, right?', [paragraph([plain('Easy as '), inlineKatex('E = mc^2'), plain(', right?')])]],
|
||||
['$$\n\\sum_{i=1}^{n} x_i\n$$', [katex('\n\\sum_{i=1}^{n} x_i\n')]],
|
||||
['$$\\frac{a}{b} + \\sqrt{c}$$', [katex('\\frac{a}{b} + \\sqrt{c}')]],
|
||||
['$a$ text $b$', [paragraph([inlineKatex('a'), plain(' text '), inlineKatex('b')])]],
|
||||
['$x$ and $y$', [paragraph([inlineKatex('x'), plain(' and '), inlineKatex('y')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input, { katex: { dollarSyntax: true } })).toMatchObject(output);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['$$E = mc^2$$', [paragraph([plain('$$E = mc^2$$')])]],
|
||||
['$E = mc^2$', [paragraph([plain('$E = mc^2$')])]],
|
||||
['\\(E = mc^2\\)', [paragraph([plain('\\(E = mc^2\\)')])]],
|
||||
])('parses KaTeX as plain text when syntax options are disabled: %p', (input, output) => {
|
||||
expect(parse(input)).toMatchObject(output);
|
||||
});
|
||||
29
packages/message-parser/tests/lineBreak.test.ts
Normal file
29
packages/message-parser/tests/lineBreak.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { parse } from '../src';
|
||||
import { lineBreak, paragraph, plain } from './helpers';
|
||||
|
||||
test.each([
|
||||
[
|
||||
`test
|
||||
|
||||
test2`,
|
||||
[paragraph([plain('test')]), lineBreak(), paragraph([plain('test2')])],
|
||||
],
|
||||
[
|
||||
`test
|
||||
|
||||
test2
|
||||
`,
|
||||
[paragraph([plain('test')]), lineBreak(), paragraph([plain('test2')])],
|
||||
],
|
||||
[
|
||||
`test
|
||||
|
||||
|
||||
|
||||
test2
|
||||
`,
|
||||
[paragraph([plain('test')]), lineBreak(), lineBreak(), lineBreak(), paragraph([plain('test2')])],
|
||||
],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
417
packages/message-parser/tests/link.test.ts
Normal file
417
packages/message-parser/tests/link.test.ts
Normal file
@@ -0,0 +1,417 @@
|
||||
import { parse } from '../src';
|
||||
import { link, paragraph, plain, bold, strike, italic, quote, lineBreak, unorderedList, listItem, orderedList } from './helpers';
|
||||
|
||||
test.each([
|
||||
['<https://domain.com|Test>', [paragraph([link('https://domain.com', [plain('Test')])])]],
|
||||
|
||||
[
|
||||
`<https://domain.com|Test
|
||||
>`,
|
||||
[paragraph([plain('<https://domain.com|Test')]), paragraph([plain('>')])],
|
||||
],
|
||||
[
|
||||
`<https://domain.com|Test
|
||||
> quote here`,
|
||||
[paragraph([plain('<https://domain.com|Test')]), quote([paragraph([plain('quote here')])])],
|
||||
],
|
||||
[
|
||||
'[Link](https://domain.com/link?a=%28node_filesystem_avail_bytes%29)',
|
||||
[paragraph([link('https://domain.com/link?a=%28node_filesystem_avail_bytes%29', [plain('Link')])])],
|
||||
],
|
||||
['[](https://rocket.chat)', [paragraph([link('https://rocket.chat')])]],
|
||||
['[ ](https://rocket.chat)', [paragraph([link('https://rocket.chat', [plain(' ')])])]],
|
||||
|
||||
['[ test](https://rocket.chat)', [paragraph([link('https://rocket.chat', [plain(' test')])])]],
|
||||
['[ test ](https://rocket.chat)', [paragraph([link('https://rocket.chat', [plain(' test ')])])]],
|
||||
['[title](https://rocket.chat)', [paragraph([link('https://rocket.chat', [plain('title')])])]],
|
||||
['[title](http://localhost)', [paragraph([link('http://localhost', [plain('title')])])]],
|
||||
['[title](http://localhost?testing=true)', [paragraph([link('http://localhost?testing=true', [plain('title')])])]],
|
||||
['[**title**](https://rocket.chat)', [paragraph([link('https://rocket.chat', [bold([plain('title')])])])]],
|
||||
['[~~title~~](https://rocket.chat)', [paragraph([link('https://rocket.chat', [strike([plain('title')])])])]],
|
||||
['[__title__](https://rocket.chat)', [paragraph([link('https://rocket.chat', [italic([plain('title')])])])]],
|
||||
['[__**~~title~~**__](https://rocket.chat)', [paragraph([link('https://rocket.chat', [italic([bold([strike([plain('title')])])])])])]],
|
||||
[
|
||||
'https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351',
|
||||
[paragraph([link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351')])],
|
||||
],
|
||||
[
|
||||
'<https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351|Test>',
|
||||
[paragraph([link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [plain('Test')])])],
|
||||
],
|
||||
[
|
||||
'[title](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[paragraph([link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [plain('title')])])],
|
||||
],
|
||||
[
|
||||
'[**title**](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [bold([plain('title')])]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[~~title~~](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [strike([plain('title')])]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[__title__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [italic([plain('title')])]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[__**~~title~~**__](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [
|
||||
italic([bold([strike([plain('title')])])]),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[title](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351?query=test12-34)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351?query=test12-34', [plain('title')]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[title](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do?query=test12-34#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do?query=test12-34#Cases/dv/413244000073043351', [plain('title')]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[title](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351?query=test12-34&query2=abc123)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351?query=test12-34&query2=abc123', [
|
||||
plain('title'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[title](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases?query=test12-34&query2=abcd!e/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases?query=test12-34&query2=abcd!e/dv/413244000073043351', [
|
||||
plain('title'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[title](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351?query=test12-34&query2=abcd!~-._%2B+)',
|
||||
[
|
||||
paragraph([
|
||||
link(
|
||||
'https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351?query=test12-34&query2=abcd!~-._%2B+',
|
||||
[plain('title')],
|
||||
),
|
||||
]),
|
||||
],
|
||||
],
|
||||
['google.com', [paragraph([link('//google.com', [plain('google.com')])])]],
|
||||
['www.google.com', [paragraph([link('//www.google.com', [plain('www.google.com')])])]],
|
||||
['rocket.chat:8080', [paragraph([link('rocket.chat:8080')])]],
|
||||
['ShouldNotBeALink', [paragraph([plain('ShouldNotBeALink')])]],
|
||||
['http:/ google.com', [paragraph([plain('http:/ '), link('//google.com', [plain('google.com')])])]],
|
||||
['[custom](custom://google.com)', [paragraph([link('custom://google.com', [plain('custom')])])]],
|
||||
[
|
||||
'[thing](https://www.thingiverse.com/thing:5451684)',
|
||||
[paragraph([link('https://www.thingiverse.com/thing:5451684', [plain('thing')])])],
|
||||
],
|
||||
['https://t.me/joinchat/chatexample', [paragraph([link('https://t.me/joinchat/chatexample')])]],
|
||||
[
|
||||
'[telegram invite](https://t.me/joinchat/chatexample)',
|
||||
[paragraph([link('https://t.me/joinchat/chatexample', [plain('telegram invite')])])],
|
||||
],
|
||||
[
|
||||
'[Github link with hash](https://github.com/RocketChat/Rocket.Chat/pull/26751/files#diff-c87b108ecf1ede549f8ede68eca840fbb330180b927df0b8a0b4df5d06cbd89b)',
|
||||
[
|
||||
paragraph([
|
||||
link(
|
||||
'https://github.com/RocketChat/Rocket.Chat/pull/26751/files#diff-c87b108ecf1ede549f8ede68eca840fbb330180b927df0b8a0b4df5d06cbd89b',
|
||||
[plain('Github link with hash')],
|
||||
),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[Github link with hash](https://github.com/RocketChat/Rocket.Chat/pull/26751/files#diff)',
|
||||
[paragraph([link('https://github.com/RocketChat/Rocket.Chat/pull/26751/files#diff', [plain('Github link with hash')])])],
|
||||
],
|
||||
[
|
||||
'[Github link without hash](https://github.com/RocketChat/Rocket.Chat/pull/26751/files)',
|
||||
[paragraph([link('https://github.com/RocketChat/Rocket.Chat/pull/26751/files', [plain('Github link without hash')])])],
|
||||
],
|
||||
[
|
||||
'[Link with special chars](https://github.com/RocketChat/Rocket.Chat*[/]^_`{}~)',
|
||||
[paragraph([link('https://github.com/RocketChat/Rocket.Chat*[/]^_`{}~', [plain('Link with special chars')])])],
|
||||
],
|
||||
[
|
||||
'[Google complex Link](https://www.google.com/url?rct=j&sa=t&url=https://ga.de/freizeit/region-erleben/bonn-und-region-tipps-fuers-wochenende-flohmarkt-rheinaue-weltkindertag-stadtfest_aid-53876987&ct=ga&cd=CAIyHDQ0NzEyYWE3MDA1MGNhNTQ6Y29tOmRlOkRFOlI&usg=AOvVaw3ySYrO9lM0iNSnk43gPVwZ)',
|
||||
[
|
||||
paragraph([
|
||||
link(
|
||||
'https://www.google.com/url?rct=j&sa=t&url=https://ga.de/freizeit/region-erleben/bonn-und-region-tipps-fuers-wochenende-flohmarkt-rheinaue-weltkindertag-stadtfest_aid-53876987&ct=ga&cd=CAIyHDQ0NzEyYWE3MDA1MGNhNTQ6Y29tOmRlOkRFOlI&usg=AOvVaw3ySYrO9lM0iNSnk43gPVwZ',
|
||||
[plain('Google complex Link')],
|
||||
),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[Rocket.Chat](https://rocket.chat) Inline Text',
|
||||
[paragraph([link('https://rocket.chat', [plain('Rocket.Chat')]), plain(' Inline Text')])],
|
||||
],
|
||||
[
|
||||
'https://analytics.zoho.com/open-view/123456789 Same Line',
|
||||
[
|
||||
paragraph([
|
||||
link('https://analytics.zoho.com/open-view/123456789', [plain('https://analytics.zoho.com/open-view/123456789')]),
|
||||
plain(' Same Line'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
`[Rocket.Chat](https://rocket.chat)
|
||||
Text after in a new line after link`,
|
||||
[paragraph([link('https://rocket.chat', [plain('Rocket.Chat')])]), paragraph([plain('Text after in a new line after link')])],
|
||||
],
|
||||
[
|
||||
`https://analytics.zoho.com/open-view/123456789
|
||||
Second line`,
|
||||
[
|
||||
paragraph([link('https://analytics.zoho.com/open-view/123456789', [plain('https://analytics.zoho.com/open-view/123456789')])]),
|
||||
paragraph([plain('Second line')]),
|
||||
],
|
||||
],
|
||||
[
|
||||
`[Rocket.Chat](https://rocket.chat)
|
||||
|
||||
Text after line break`,
|
||||
[paragraph([link('https://rocket.chat', [plain('Rocket.Chat')])]), lineBreak(), paragraph([plain('Text after line break')])],
|
||||
],
|
||||
[
|
||||
`
|
||||
[List Header Link](https://rocket.chat)
|
||||
- First item
|
||||
- Second item
|
||||
- Third item
|
||||
- *Fourth item*
|
||||
`.trim(),
|
||||
[
|
||||
paragraph([link('https://rocket.chat', [plain('List Header Link')])]),
|
||||
unorderedList([
|
||||
listItem([plain('First item')]),
|
||||
listItem([plain('Second item')]),
|
||||
listItem([plain('Third item')]),
|
||||
listItem([bold([plain('Fourth item')])]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
`[List Header Link](https://rocket.chat)
|
||||
7. First item
|
||||
2. Second item
|
||||
8. Third item
|
||||
4. *Fourth item*
|
||||
15. *Fifteenth item*
|
||||
`.trim(),
|
||||
[
|
||||
paragraph([link('https://rocket.chat', [plain('List Header Link')])]),
|
||||
orderedList([
|
||||
listItem([plain('First item')], 7),
|
||||
listItem([plain('Second item')], 2),
|
||||
listItem([plain('Third item')], 8),
|
||||
listItem([bold([plain('Fourth item')])], 4),
|
||||
listItem([bold([plain('Fifteenth item')])], 15),
|
||||
]),
|
||||
],
|
||||
],
|
||||
['[9gag](https://9gag.com/)', [paragraph([link('https://9gag.com/', [plain('9gag')])])]],
|
||||
['[9gag](9gag.com)', [paragraph([link('9gag.com', [plain('9gag')])])]],
|
||||
['<9gag.com|9gag>', [paragraph([link('9gag.com', [plain('9gag')])])]],
|
||||
['9gag.com', [paragraph([link('//9gag.com', [plain('9gag.com')])])]],
|
||||
[
|
||||
'[notes link](notes://Server/C3257116002CAD60/0/CCAF6BE2824A1F49432588D2001FA73E)',
|
||||
[paragraph([link('notes://Server/C3257116002CAD60/0/CCAF6BE2824A1F49432588D2001FA73E', [plain('notes link')])])],
|
||||
],
|
||||
[
|
||||
'[File Path](C:/Users/user1/Documents/projects/file.js)',
|
||||
[paragraph([link('C:/Users/user1/Documents/projects/file.js', [plain('File Path')])])],
|
||||
],
|
||||
[
|
||||
'[Test with **bold** element](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [
|
||||
plain('Test with '),
|
||||
bold([plain('bold')]),
|
||||
plain(' element'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[Test with *bold* element](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [
|
||||
plain('Test with '),
|
||||
bold([plain('bold')]),
|
||||
plain(' element'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[Test with _italic_ element](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [
|
||||
plain('Test with '),
|
||||
italic([plain('italic')]),
|
||||
plain(' element'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[Test with ~strike~ element](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [
|
||||
plain('Test with '),
|
||||
strike([plain('strike')]),
|
||||
plain(' element'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[Test with __**~~title~~**__ element](https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://desk.rocket.chat/support/rocketchat/ShowHomePage.do#Cases/dv/413244000073043351', [
|
||||
plain('Test with '),
|
||||
italic([bold([strike([plain('title')])])]),
|
||||
plain(' element'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'([Github Issue: #24929](https://github.com/RocketChat/Rocket.Chat/issues/24929))',
|
||||
[paragraph([plain('('), link('https://github.com/RocketChat/Rocket.Chat/issues/24929', [plain('Github Issue: #24929')]), plain(')')])],
|
||||
],
|
||||
[
|
||||
'the [audio_url and video_url for post message attachments](https://developer.rocket.chat/reference/api/rest-api/endpoints/core-endpoints/chat-endpoints/postmessage)',
|
||||
[
|
||||
paragraph([
|
||||
plain('the '),
|
||||
link('https://developer.rocket.chat/reference/api/rest-api/endpoints/core-endpoints/chat-endpoints/postmessage', [
|
||||
plain('audio_url and video_url for post message attachments'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'the [Jira [Task] parentheses not working](rocket.chat)',
|
||||
[paragraph([plain('the '), link('rocket.chat', [plain('Jira [Task] parentheses not working')])])],
|
||||
],
|
||||
[
|
||||
'the [Jira (Task) parentheses not working](rocket.chat)',
|
||||
[paragraph([plain('the '), link('rocket.chat', [plain('Jira (Task) parentheses not working')])])],
|
||||
],
|
||||
[
|
||||
'[Jira [Task] parentheses not working](rocket.chat)',
|
||||
[paragraph([link('rocket.chat', [plain('Jira [Task] parentheses not working')])])],
|
||||
],
|
||||
[
|
||||
'[Jira (Task) parentheses not working](rocket.chat)',
|
||||
[paragraph([link('rocket.chat', [plain('Jira (Task) parentheses not working')])])],
|
||||
],
|
||||
// Should not parse as link
|
||||
['77.77%', [paragraph([plain('77.77%')])]],
|
||||
['77.77', [paragraph([plain('77.77')])]],
|
||||
['https://77.77', [paragraph([plain('https://77.77')])]],
|
||||
['test.9gag', [paragraph([plain('test.9gag')])]],
|
||||
[
|
||||
'[here](https://github.com/RocketChat/Rocket.Chat/releases/tag/6.0.0-rc.3)',
|
||||
[paragraph([link('https://github.com/RocketChat/Rocket.Chat/releases/tag/6.0.0-rc.3', [plain('here')])])],
|
||||
],
|
||||
[
|
||||
'[ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [ ~ [test](https://rocket.chat)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://rocket.chat', [
|
||||
plain(' '),
|
||||
strike([plain(' [ ')]),
|
||||
plain(' [ '),
|
||||
strike([plain(' [ ')]),
|
||||
plain(' [ '),
|
||||
strike([plain(' [ ')]),
|
||||
plain(' [ '),
|
||||
strike([plain(' [ ')]),
|
||||
plain(' [ '),
|
||||
strike([plain(' [ ')]),
|
||||
plain(' [ '),
|
||||
strike([plain(' [ ')]),
|
||||
plain(' [ '),
|
||||
strike([plain(' [ ')]),
|
||||
plain(' [test'),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'[test **bold** and __italic__](https://rocket.chat)',
|
||||
[paragraph([link('https://rocket.chat', [plain('test '), bold([plain('bold')]), plain(' and '), italic([plain('italic')])])])],
|
||||
],
|
||||
[
|
||||
'[test **bold with __italic__**](https://rocket.chat)',
|
||||
[paragraph([link('https://rocket.chat', [plain('test '), bold([plain('bold with '), italic([plain('italic')])])])])],
|
||||
],
|
||||
// Test case for issue #31418 - text in brackets between two links should not break markdown
|
||||
[
|
||||
'[Rocket.Chat] [New release](https://www.rocket.chat/blog/new-starter-pro-plans)',
|
||||
[paragraph([plain('[Rocket.Chat] '), link('https://www.rocket.chat/blog/new-starter-pro-plans', [plain('New release')])])],
|
||||
],
|
||||
// Test case for issue #31766 - multiple links with bracketed text between them
|
||||
[
|
||||
'[BUG #11111](https://github.com/) - [BACK] changelog description',
|
||||
[paragraph([link('https://github.com/', [plain('BUG #11111')]), plain(' - [BACK] changelog description')])],
|
||||
],
|
||||
[
|
||||
'[BUG #11111](https://github.com/) - [BACK] [Another link](https://github.com/)',
|
||||
[
|
||||
paragraph([
|
||||
link('https://github.com/', [plain('BUG #11111')]),
|
||||
plain(' - [BACK] '),
|
||||
link('https://github.com/', [plain('Another link')]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
// Test case for brackets/parentheses in URL parameters
|
||||
[
|
||||
'[link](https://example.com/query?this=(is)&a=problem)',
|
||||
[paragraph([link('https://example.com/query?this=(is)&a=problem', [plain('link')])])],
|
||||
],
|
||||
['[link](https://example.com/path/to/func(param))', [paragraph([link('https://example.com/path/to/func(param)', [plain('link')])])]],
|
||||
['[link](https://example.com/path/(section)/page)', [paragraph([link('https://example.com/path/(section)/page', [plain('link')])])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
22
packages/message-parser/tests/mention.test.ts
Normal file
22
packages/message-parser/tests/mention.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { parse } from '../src';
|
||||
import { paragraph, plain, mentionUser, mentionChannel } from './helpers';
|
||||
|
||||
test.each([
|
||||
['@guilherme.gazzo', [paragraph([mentionUser('guilherme.gazzo')])]],
|
||||
['@guilherme.gazzo. ', [paragraph([mentionUser('guilherme.gazzo.'), plain(' ')])]],
|
||||
['#GENERAL', [paragraph([mentionChannel('GENERAL')])]],
|
||||
['@user:server.com', [paragraph([mentionUser('user:server.com')])]],
|
||||
['@marcos.defendi:matrix.org', [paragraph([mentionUser('marcos.defendi:matrix.org')])]],
|
||||
['@username@example.com', [paragraph([mentionUser('username@example.com')])]],
|
||||
['@099fnd2ee@example.com', [paragraph([mentionUser('099fnd2ee@example.com')])]],
|
||||
['@téstãçâò', [paragraph([mentionUser('téstãçâò')])]],
|
||||
['@สมชาย', [paragraph([mentionUser('สมชาย')])]],
|
||||
['@李祖阳', [paragraph([mentionUser('李祖阳')])]],
|
||||
['@あおい', [paragraph([mentionUser('あおい')])]],
|
||||
['@アオイ', [paragraph([mentionUser('アオイ')])]],
|
||||
['@Владимир', [paragraph([mentionUser('Владимир')])]],
|
||||
['@Кириллица', [paragraph([mentionUser('Кириллица')])]],
|
||||
['test @Кириллица test', [paragraph([plain('test '), mentionUser('Кириллица'), plain(' test')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
29
packages/message-parser/tests/orderedList.test.ts
Normal file
29
packages/message-parser/tests/orderedList.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { parse } from '../src';
|
||||
import { bold, plain, orderedList, listItem, emoji } from './helpers';
|
||||
|
||||
test.each([
|
||||
[
|
||||
`
|
||||
0. Zeroth item
|
||||
7. First item
|
||||
2. Second item
|
||||
8. Third item
|
||||
4. *Fourth item*
|
||||
15. *Fifteenth item*
|
||||
20. :smile:
|
||||
`.trim(),
|
||||
[
|
||||
orderedList([
|
||||
listItem([plain('Zeroth item')], 0),
|
||||
listItem([plain('First item')], 7),
|
||||
listItem([plain('Second item')], 2),
|
||||
listItem([plain('Third item')], 8),
|
||||
listItem([bold([plain('Fourth item')])], 4),
|
||||
listItem([bold([plain('Fifteenth item')])], 15),
|
||||
listItem([emoji('smile')], 20),
|
||||
]),
|
||||
],
|
||||
],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
26
packages/message-parser/tests/phoneNumber.test.ts
Normal file
26
packages/message-parser/tests/phoneNumber.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { parse } from '../src';
|
||||
import { link, paragraph, plain, bold } from './helpers';
|
||||
|
||||
test.each([
|
||||
['+07563546725', [paragraph([link('tel:07563546725', [plain('+07563546725')])])]],
|
||||
['+075-63546725', [paragraph([link('tel:07563546725', [plain('+075-63546725')])])]],
|
||||
['+(075)-63546725', [paragraph([link('tel:07563546725', [plain('+(075)-63546725')])])]],
|
||||
['+(075)63546725', [paragraph([link('tel:07563546725', [plain('+(075)63546725')])])]],
|
||||
['[here](+(075)63546725)', [paragraph([link('tel:07563546725', [plain('here')])])]],
|
||||
['[**here**](+(075)63546725)', [paragraph([link('tel:07563546725', [bold([plain('here')])])])]],
|
||||
['+(11)99999-9999', [paragraph([link('tel:11999999999', [plain('+(11)99999-9999')])])]],
|
||||
['5 +51231', [paragraph([plain('5 '), link('tel:51231', [plain('+51231')])])]],
|
||||
['5 +51231 5', [paragraph([plain('5 '), link('tel:51231', [plain('+51231')]), plain(' 5')])]],
|
||||
['+(12)3-45', [paragraph([link('tel:12345', [plain('+(12)3-45')])])]],
|
||||
['+1.599123', [paragraph([plain('+1.599123')])]],
|
||||
['1+1=2', [paragraph([plain('1+1=2')])]],
|
||||
['1+1=2 text', [paragraph([plain('1+1=2 text')])]],
|
||||
['+1000,00', [paragraph([plain('+1000,00')])]],
|
||||
['+ 1199999999', [paragraph([plain('+ 1199999999')])]],
|
||||
['+1234', [paragraph([plain('+1234')])]],
|
||||
['+(12)3-4', [paragraph([plain('+(12)3-4')])]],
|
||||
['+123-4', [paragraph([plain('+123-4')])]],
|
||||
['5+51231', [paragraph([plain('5+51231')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
15
packages/message-parser/tests/skip-flags-regression.spec.ts
Normal file
15
packages/message-parser/tests/skip-flags-regression.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { parse } from '../src';
|
||||
|
||||
describe('Skip Flags Regression (Complexity Audit)', () => {
|
||||
it('should parse nested formatting across multiple depths without throwing', () => {
|
||||
for (let d = 1; d <= 50; d++) {
|
||||
const input = `${'*'.repeat(d)}text${'*'.repeat(d)}`;
|
||||
expect(() => parse(input)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle pathological unmatched markers without crashing', () => {
|
||||
const pathological = '*_~*_~*_~*_~*_~ hello'.repeat(5);
|
||||
expect(() => parse(pathological)).not.toThrow();
|
||||
});
|
||||
});
|
||||
50
packages/message-parser/tests/spoiler.test.ts
Normal file
50
packages/message-parser/tests/spoiler.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { parse } from '../src';
|
||||
import { bold, emoji, italic, link, mentionChannel, mentionUser, paragraph, plain, spoiler, strike } from './helpers';
|
||||
|
||||
describe('spoiler parsing', () => {
|
||||
test.each([
|
||||
['||spoiler||', [paragraph([spoiler([plain('spoiler')])])]],
|
||||
['||spoiler **bold**||', [paragraph([spoiler([plain('spoiler '), bold([plain('bold')])])])]],
|
||||
['||__i__ ~~s~~||', [paragraph([spoiler([italic([plain('i')]), plain(' '), strike([plain('s')])])])]],
|
||||
['||unclosed', [paragraph([plain('||unclosed')])]],
|
||||
])('parses basic spoilers: %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
|
||||
test.each([
|
||||
// Nested markup
|
||||
['||**bold __italic__**||', [paragraph([spoiler([bold([plain('bold '), italic([plain('italic')])])])])]],
|
||||
['||~~**strike bold**~~||', [paragraph([spoiler([strike([bold([plain('strike bold')])])])])]],
|
||||
|
||||
// With mentions
|
||||
['||@user mention||', [paragraph([spoiler([mentionUser('user'), plain(' mention')])])]],
|
||||
['||#channel mention||', [paragraph([spoiler([mentionChannel('channel'), plain(' mention')])])]],
|
||||
|
||||
// With links
|
||||
['||[link text](https://example.com)||', [paragraph([spoiler([link('https://example.com', [plain('link text')])])])]],
|
||||
|
||||
// With emoji
|
||||
['||text :emoji: text||', [paragraph([spoiler([plain('text '), emoji('emoji'), plain(' text')])])]],
|
||||
|
||||
// Empty spoiler
|
||||
['||||', [paragraph([plain('||||')])]],
|
||||
|
||||
// Multiple spoilers in text
|
||||
['||first|| and ||second||', [paragraph([spoiler([plain('first')]), plain(' and '), spoiler([plain('second')])])]],
|
||||
|
||||
// Spoiler with special characters
|
||||
['||special: !@#$%^&*()||', [paragraph([spoiler([plain('special: !@#$%^&*()')])])]],
|
||||
|
||||
// Unclosed spoilers should be treated as plain text
|
||||
['||unclosed spoiler', [paragraph([plain('||unclosed spoiler')])]],
|
||||
['text ||unclosed', [paragraph([plain('text ||unclosed')])]],
|
||||
|
||||
// Single pipe should not be treated as spoiler
|
||||
['|single pipe|', [paragraph([plain('|single pipe|')])]],
|
||||
|
||||
// Spoiler at start and end
|
||||
['||start|| middle ||end||', [paragraph([spoiler([plain('start')]), plain(' middle '), spoiler([plain('end')])])]],
|
||||
])('parses edge cases: %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
30
packages/message-parser/tests/spoilerBlock.test.ts
Normal file
30
packages/message-parser/tests/spoilerBlock.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { parse } from '../src';
|
||||
import { lineBreak, paragraph, plain, spoilerBlock } from './helpers';
|
||||
|
||||
describe('block spoiler parsing', () => {
|
||||
test.each([
|
||||
[
|
||||
`||
|
||||
line one
|
||||
||`,
|
||||
[spoilerBlock([paragraph([plain('line one')])])],
|
||||
],
|
||||
[
|
||||
`||
|
||||
line one
|
||||
line two
|
||||
||`,
|
||||
[spoilerBlock([paragraph([plain('line one')]), paragraph([plain('line two')])])],
|
||||
],
|
||||
[
|
||||
`before
|
||||
||
|
||||
hidden
|
||||
||
|
||||
after`,
|
||||
[paragraph([plain('before')]), spoilerBlock([paragraph([plain('hidden')])]), lineBreak(), paragraph([plain('after')])],
|
||||
],
|
||||
])('parses block spoilers: %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
48
packages/message-parser/tests/strikethrough.test.ts
Normal file
48
packages/message-parser/tests/strikethrough.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { parse } from '../src';
|
||||
import { emoji, emojiUnicode, link, mentionChannel, mentionUser, paragraph, plain, strike } from './helpers';
|
||||
|
||||
test.each([
|
||||
['~:smile:~', [paragraph([strike([emoji('smile')])])]],
|
||||
['~test :smile: test~', [paragraph([strike([plain('test '), emoji('smile'), plain(' test')])])]],
|
||||
['~😀~', [paragraph([strike([emojiUnicode('😀')])])]],
|
||||
['~test 😀~', [paragraph([strike([plain('test '), emojiUnicode('😀')])])]],
|
||||
['~@guilherme.gazzo~', [paragraph([strike([mentionUser('guilherme.gazzo')])])]],
|
||||
['~#GENERAL~', [paragraph([strike([mentionChannel('GENERAL')])])]],
|
||||
['~test @guilherme.gazzo~', [paragraph([strike([plain('test '), mentionUser('guilherme.gazzo')])])]],
|
||||
['~test #GENERAL~', [paragraph([strike([plain('test '), mentionChannel('GENERAL')])])]],
|
||||
[
|
||||
'~~[A brand new Gist](https://gist.github.com/24dddfa97bef58f46ac2ce0f80c58ba4)~~',
|
||||
[paragraph([strike([link('https://gist.github.com/24dddfa97bef58f46ac2ce0f80c58ba4', [plain('A brand new Gist')])])])],
|
||||
],
|
||||
['~~strike~~', [paragraph([strike([plain('strike')])])]],
|
||||
['pre~~strike~~post', [paragraph([plain('pre'), strike([plain('strike')]), plain('post')])]],
|
||||
[' pre~~strike~~post ', [paragraph([plain(' pre'), strike([plain('strike')]), plain('post ')])]],
|
||||
['~~', [paragraph([plain('~~')])]],
|
||||
['~ ~', [paragraph([plain('~ ~')])]],
|
||||
['~~ ~', [paragraph([plain('~~ ~')])]],
|
||||
['~~ ~~', [paragraph([plain('~~ ~~')])]],
|
||||
['~ Hello~', [paragraph([strike([plain(' Hello')])])]],
|
||||
['~Hello ~', [paragraph([strike([plain('Hello ')])])]],
|
||||
[':custom~emoji~case:', [paragraph([plain(`:custom`), strike([plain('emoji')]), plain(`case:`)])]],
|
||||
['text~hello~text', [paragraph([plain(`text`), strike([plain('hello')]), plain(`text`)])]],
|
||||
['~hello~text', [paragraph([strike([plain('hello')]), plain(`text`)])]],
|
||||
['text~hello~', [paragraph([plain(`text`), strike([plain('hello')])])]],
|
||||
['~Hel lo~', [paragraph([strike([plain('Hel lo')])])]],
|
||||
['~Hello~', [paragraph([strike([plain('Hello')])])]],
|
||||
['~~Hello~~', [paragraph([strike([plain('Hello')])])]],
|
||||
['~~Hello~', [paragraph([plain(`~`), strike([plain('Hello')])])]],
|
||||
['~Hello~~', [paragraph([strike([plain('Hello')]), plain(`~`)])]],
|
||||
['~Hello', [paragraph([plain('~Hello')])]],
|
||||
['Hello~', [paragraph([plain('Hello~')])]],
|
||||
['He~llo', [paragraph([plain('He~llo')])]],
|
||||
['~~~Hello~~~', [paragraph([plain(`~`), strike([plain('Hello')]), plain(`~`)])]],
|
||||
['~~~Hello~~', [paragraph([plain(`~`), strike([plain('Hello')])])]],
|
||||
['~Hello~ this is dog', [paragraph([strike([plain('Hello')]), plain(` this is dog`)])]],
|
||||
['Rocket cat says ~Hello~', [paragraph([plain(`Rocket cat says `), strike([plain('Hello')])])]],
|
||||
['He said ~Hello~ to her', [paragraph([plain(`He said `), strike([plain('Hello')]), plain(` to her`)])]],
|
||||
['~~Hello~~ this is dog', [paragraph([strike([plain('Hello')]), plain(` this is dog`)])]],
|
||||
['Rocket cat says ~~Hello~~', [paragraph([plain(`Rocket cat says `), strike([plain('Hello')])])]],
|
||||
['He said ~~Hello~~ to her', [paragraph([plain(`He said `), strike([plain('Hello')]), plain(` to her`)])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
68
packages/message-parser/tests/strongEmphasis.test.ts
Normal file
68
packages/message-parser/tests/strongEmphasis.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { parse } from '../src';
|
||||
import { bold, link, paragraph, plain, italic, strike, emoji, emojiUnicode, mentionChannel, mentionUser } from './helpers';
|
||||
|
||||
test.each([
|
||||
['*:smile:*', [paragraph([bold([emoji('smile')])])]],
|
||||
['*test :smile: test*', [paragraph([bold([plain('test '), emoji('smile'), plain(' test')])])]],
|
||||
['*😀*', [paragraph([bold([emojiUnicode('😀')])])]],
|
||||
['*test 😀*', [paragraph([bold([plain('test '), emojiUnicode('😀')])])]],
|
||||
['*@guilherme.gazzo*', [paragraph([bold([mentionUser('guilherme.gazzo')])])]],
|
||||
['*#GENERAL*', [paragraph([bold([mentionChannel('GENERAL')])])]],
|
||||
['*test @guilherme.gazzo*', [paragraph([bold([plain('test '), mentionUser('guilherme.gazzo')])])]],
|
||||
['*test #GENERAL*', [paragraph([bold([plain('test '), mentionChannel('GENERAL')])])]],
|
||||
[
|
||||
'*[A brand new Gist](https://gist.github.com/24dddfa97bef58f46ac2ce0f80c58ba4)*',
|
||||
[paragraph([bold([link('https://gist.github.com/24dddfa97bef58f46ac2ce0f80c58ba4', [plain('A brand new Gist')])])])],
|
||||
],
|
||||
['**bold**', [paragraph([bold([plain('bold')])])]],
|
||||
['** bold**', [paragraph([bold([plain(' bold')])])]],
|
||||
['** bold **', [paragraph([bold([plain(' bold ')])])]],
|
||||
['** bo ld **', [paragraph([bold([plain(' bo ld ')])])]],
|
||||
['pre**bold**', [paragraph([plain('pre'), bold([plain('bold')])])]],
|
||||
['**bold**pos', [paragraph([bold([plain('bold')]), plain('pos')])]],
|
||||
['**bold****bold**', [paragraph([bold([plain('bold')]), bold([plain('bold')])])]],
|
||||
['**bold** **bold**', [paragraph([bold([plain('bold')]), plain(' '), bold([plain('bold')])])]],
|
||||
['**bold** __italic__', [paragraph([bold([plain('bold')]), plain(' '), italic([plain('italic')])])]],
|
||||
['**__italicbold__**', [paragraph([bold([italic([plain('italicbold')])])])]],
|
||||
['plain **__italicbold__**', [paragraph([plain('plain '), bold([italic([plain('italicbold')])])])]],
|
||||
['plain **__~~strikeitalicbold~~__**', [paragraph([plain('plain '), bold([italic([strike([plain('strikeitalicbold')])])])])]],
|
||||
['**', [paragraph([plain('**')])]],
|
||||
['* *', [paragraph([plain('* *')])]],
|
||||
['** *', [paragraph([plain('** *')])]],
|
||||
['** **', [paragraph([plain('** **')])]],
|
||||
['** **', [paragraph([plain('** **')])]],
|
||||
['* Hello*', [paragraph([bold([plain(' Hello')])])]],
|
||||
['*Hello *', [paragraph([bold([plain('Hello ')])])]],
|
||||
[':custom*emoji*case:', [paragraph([plain(':custom'), bold([plain('emoji')]), plain('case:')])]],
|
||||
['text*hello*text', [paragraph([plain('text'), bold([plain('hello')]), plain('text')])]],
|
||||
['*hello*text', [paragraph([bold([plain('hello')]), plain('text')])]],
|
||||
['text*hello*', [paragraph([plain('text'), bold([plain('hello')])])]],
|
||||
['*Hel lo*', [paragraph([bold([plain('Hel lo')])])]],
|
||||
['*Hello*', [paragraph([bold([plain('Hello')])])]],
|
||||
['**Hello*', [paragraph([plain('*'), bold([plain('Hello')])])]],
|
||||
['*Hello**', [paragraph([bold([plain('Hello')]), plain('*')])]],
|
||||
['*Hello', [paragraph([plain('*Hello')])]],
|
||||
['Hello*', [paragraph([plain('Hello*')])]],
|
||||
['He*llo', [paragraph([plain('He*llo')])]],
|
||||
['***Hello***', [paragraph([plain('*'), bold([plain('Hello')]), plain('*')])]],
|
||||
['***Hello**', [paragraph([plain('*'), bold([plain('Hello')])])]],
|
||||
['*Hello* this is dog', [paragraph([bold([plain('Hello')]), plain(' this is dog')])]],
|
||||
['Rocket cat says *Hello*', [paragraph([plain('Rocket cat says '), bold([plain('Hello')])])]],
|
||||
['He said *Hello* to her', [paragraph([plain('He said '), bold([plain('Hello')]), plain(' to her')])]],
|
||||
['**Hello** this is dog', [paragraph([bold([plain('Hello')]), plain(' this is dog')])]],
|
||||
['Rocket cat says **Hello**', [paragraph([plain('Rocket cat says '), bold([plain('Hello')])])]],
|
||||
['He said **Hello** to her', [paragraph([plain('He said '), bold([plain('Hello')]), plain(' to her')])]],
|
||||
['He was a**nn**oyed', [paragraph([plain('He was a'), bold([plain('nn')]), plain('oyed')])]],
|
||||
['There are two o in f*oo*tball', [paragraph([plain('There are two o in f'), bold([plain('oo')]), plain('tball')])]],
|
||||
['*(teste*', [paragraph([bold([plain('(teste')])])]],
|
||||
['*(teste)*', [paragraph([bold([plain('(teste)')])])]],
|
||||
['*__~bolditalicstrike~_*', [paragraph([bold([plain('_'), italic([strike([plain('bolditalicstrike')])])])])]],
|
||||
['*test:*', [paragraph([bold([plain('test:')])])]],
|
||||
['*bold ending with colon:*', [paragraph([bold([plain('bold ending with colon:')])])]],
|
||||
['*bold ending with colon:* and some more text', [paragraph([bold([plain('bold ending with colon:')]), plain(' and some more text')])]],
|
||||
['*bold ending with colon :*and some more text', [paragraph([bold([plain('bold ending with colon :')]), plain('and some more text')])]],
|
||||
['*bold with a kissing emoji :* *', [paragraph([bold([plain('bold with a kissing emoji :')]), plain(' *')])]],
|
||||
['*bold with a kissing emoji :* ', [paragraph([bold([plain('bold with a kissing emoji :')]), plain(' ')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
106
packages/message-parser/tests/table.test.ts
Normal file
106
packages/message-parser/tests/table.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Table } from '../src';
|
||||
import { parse } from '../src';
|
||||
import { bold, link, plain, table, tableCell, tableRow } from './helpers';
|
||||
|
||||
// `fallback` is the [start, end] offset span of the table in the source; since
|
||||
// each input is exactly the table, that span is [0, input.length].
|
||||
test.each([
|
||||
// basic table, no alignment
|
||||
[
|
||||
`
|
||||
| Header 1 | Header 2 |
|
||||
| -------- | -------- |
|
||||
| Cell 1 | Cell 2 |
|
||||
`.trim(),
|
||||
[tableCell([plain('Header 1')]), tableCell([plain('Header 2')])],
|
||||
[tableRow([tableCell([plain('Cell 1')]), tableCell([plain('Cell 2')])])],
|
||||
],
|
||||
// alignment markers
|
||||
[
|
||||
`
|
||||
| Left | Center | Right |
|
||||
| :--- | :----: | ----: |
|
||||
| a | b | c |
|
||||
`.trim(),
|
||||
[tableCell([plain('Left')], 'left'), tableCell([plain('Center')], 'center'), tableCell([plain('Right')], 'right')],
|
||||
[tableRow([tableCell([plain('a')], 'left'), tableCell([plain('b')], 'center'), tableCell([plain('c')], 'right')])],
|
||||
],
|
||||
// multiple body rows
|
||||
[
|
||||
`
|
||||
| h |
|
||||
| - |
|
||||
| 1 |
|
||||
| 2 |
|
||||
`.trim(),
|
||||
[tableCell([plain('h')])],
|
||||
[tableRow([tableCell([plain('1')])]), tableRow([tableCell([plain('2')])])],
|
||||
],
|
||||
// header only (no body rows)
|
||||
[
|
||||
`
|
||||
| a | b |
|
||||
| - | - |
|
||||
`.trim(),
|
||||
[tableCell([plain('a')]), tableCell([plain('b')])],
|
||||
[],
|
||||
],
|
||||
// inline markup inside cells
|
||||
[
|
||||
`
|
||||
| Name | Link |
|
||||
| ---- | ---- |
|
||||
| **bold** | [rc](https://rocket.chat) |
|
||||
`.trim(),
|
||||
[tableCell([plain('Name')]), tableCell([plain('Link')])],
|
||||
[tableRow([tableCell([bold([plain('bold')])]), tableCell([link('https://rocket.chat', [plain('rc')])])])],
|
||||
],
|
||||
// escaped pipe inside a cell stays literal
|
||||
[
|
||||
`
|
||||
| a | b |
|
||||
| - | - |
|
||||
| x \\| y | z |
|
||||
`.trim(),
|
||||
[tableCell([plain('a')]), tableCell([plain('b')])],
|
||||
[tableRow([tableCell([plain('x | y')]), tableCell([plain('z')])])],
|
||||
],
|
||||
])('parses %p', (input, header, rows) => {
|
||||
expect(parse(input)).toEqual([table(header, rows, [0, input.length])]);
|
||||
});
|
||||
|
||||
test('normalizes ragged body rows to the header column count', () => {
|
||||
const input = ['| a | b |', '| - | - |', '| 1 |', '| 1 | 2 | 3 |'].join('\n');
|
||||
const [node] = parse(input) as [Table];
|
||||
|
||||
// every row has exactly 2 cells (header count): short row padded, long row truncated
|
||||
expect(node.value.rows.map((row) => row.value.length)).toEqual([2, 2]);
|
||||
expect(node.value.rows[0].value[1].value).toEqual([]); // padded empty cell
|
||||
expect(node.value.rows[1].value.map((cell) => cell.value)).toEqual([[plain('1')], [plain('2')]]); // extra 3rd cell dropped
|
||||
});
|
||||
|
||||
test('exposes the source offsets as fallback for renderers without table support', () => {
|
||||
const input = '| a | b |\n| - | - |\n| 1 | 2 |';
|
||||
const [node] = parse(input) as [ReturnType<typeof table>];
|
||||
expect(node.fallback).toEqual([0, input.length]);
|
||||
// the offsets slice back to the original markup
|
||||
expect(input.slice(node.fallback![0], node.fallback![1])).toBe(input);
|
||||
});
|
||||
|
||||
test.each([
|
||||
// no delimiter row -> not a table, plain paragraph
|
||||
['| just | text |', [{ type: 'PARAGRAPH', value: [plain('| just | text |')] }]],
|
||||
// missing trailing pipe -> not a table (v1 requires surrounding pipes)
|
||||
[
|
||||
`
|
||||
| a | b
|
||||
| - | -
|
||||
`.trim(),
|
||||
[
|
||||
{ type: 'PARAGRAPH', value: [plain('| a | b')] },
|
||||
{ type: 'PARAGRAPH', value: [plain('| - | -')] },
|
||||
],
|
||||
],
|
||||
])('does not parse %p as table', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
36
packages/message-parser/tests/tasks.test.ts
Normal file
36
packages/message-parser/tests/tasks.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { parse } from '../src';
|
||||
import { plain, tasks, task, mentionUser, mentionChannel, link, bold, emoji } from './helpers';
|
||||
|
||||
test.each([
|
||||
[
|
||||
`
|
||||
- [ ] this is an incomplete item
|
||||
- [x] this is a complete item
|
||||
- [x] @mentions, #refs, [links](http://localhost), **formatting**
|
||||
- [x] list syntax required (any unordered or ordered list supported)
|
||||
- [ ] :smile:
|
||||
`.trim(),
|
||||
[
|
||||
tasks([
|
||||
task([plain('this is an incomplete item')], false),
|
||||
task([plain('this is a complete item')], true),
|
||||
task(
|
||||
[
|
||||
mentionUser('mentions'),
|
||||
plain(', '),
|
||||
mentionChannel('refs'),
|
||||
plain(', '),
|
||||
link('http://localhost', [plain('links')]),
|
||||
plain(', '),
|
||||
bold([plain('formatting')]),
|
||||
],
|
||||
true,
|
||||
),
|
||||
task([plain('list syntax required (any unordered or ordered list supported)')], true),
|
||||
task([emoji('smile')], false),
|
||||
]),
|
||||
],
|
||||
],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
80
packages/message-parser/tests/timestamp.test.ts
Normal file
80
packages/message-parser/tests/timestamp.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { parse } from '../src';
|
||||
|
||||
const plain = (value: string) => ({ type: 'PLAIN_TEXT' as const, value });
|
||||
|
||||
const paragraph = (value: Array<Record<string, unknown>>) => ({ type: 'PARAGRAPH' as const, value });
|
||||
|
||||
const bold = (value: Array<Record<string, unknown>>) => ({ type: 'BOLD' as const, value });
|
||||
|
||||
const strike = (value: Array<Record<string, unknown>>) => ({ type: 'STRIKE' as const, value });
|
||||
|
||||
// `fallback` is the [start, end] offset span of the raw `<t:...>` in the source.
|
||||
const timestampNode = (value: string, format: 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R' = 't', fallback?: [number, number]) => ({
|
||||
type: 'TIMESTAMP' as const,
|
||||
value: {
|
||||
timestamp: value,
|
||||
format,
|
||||
},
|
||||
...(fallback !== undefined ? { fallback } : {}),
|
||||
});
|
||||
|
||||
const spanOf = (input: string, raw: string): [number, number] => {
|
||||
const start = input.indexOf(raw);
|
||||
return [start, start + raw.length];
|
||||
};
|
||||
|
||||
test.each([
|
||||
['<t:1708551317>', '<t:1708551317>', '1708551317', 't' as const],
|
||||
['<t:1708551317:R>', '<t:1708551317:R>', '1708551317', 'R' as const],
|
||||
['hello <t:1708551317>', '<t:1708551317>', '1708551317', 't' as const],
|
||||
])('parses %p', (input, raw, value, format) => {
|
||||
const node = timestampNode(value, format, spanOf(input, raw));
|
||||
const prefix = input.slice(0, input.indexOf(raw));
|
||||
expect(parse(input)).toEqual([paragraph(prefix ? [plain(prefix), node] : [node])]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['<t:1708551317:I>', [paragraph([plain('<t:1708551317:I>')])]],
|
||||
['<t:17>', [paragraph([plain('<t:17>')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['~<t:1708551317>~', '<t:1708551317>', '1708551317', 't' as const, strike],
|
||||
['~<t:1708551317:R>~', '<t:1708551317:R>', '1708551317', 'R' as const, strike],
|
||||
['*<t:1708551317>*', '<t:1708551317>', '1708551317', 't' as const, bold],
|
||||
])('parses %p', (input, raw, value, format, wrapper) => {
|
||||
const node = timestampNode(value, format, spanOf(input, raw));
|
||||
expect(parse(input)).toEqual([paragraph([wrapper([node])])]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['<t:2025-07-22T10:00:00.000+00:00:R>', '1753178400', 'R' as const],
|
||||
['<t:2025-07-22T10:00:00+00:00:R>', '1753178400', 'R' as const],
|
||||
['<t:2025-07-24T20:19:58.154+00:00:R>', '1753388398', 'R' as const],
|
||||
])('parses %p', (input, value, format) => {
|
||||
const node = timestampNode(value, format, [0, input.length]);
|
||||
expect(parse(input)).toEqual([paragraph([node])]);
|
||||
});
|
||||
|
||||
describe('relative hour timestamp parsing', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2025-07-22T00:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test.each([
|
||||
['<t:10:00:00+00:00:R>', '1753178400', 'R' as const],
|
||||
['<t:10:00+00:00:R>', '1753178400', 'R' as const],
|
||||
['<t:10:00:05+00:00>', '1753178405', 't' as const],
|
||||
['<t:10:00+00:00>', '1753178400', 't' as const],
|
||||
])('parses %p', (input, value, format) => {
|
||||
const node = timestampNode(value, format, [0, input.length]);
|
||||
expect(parse(input)).toEqual([paragraph([node])]);
|
||||
});
|
||||
});
|
||||
76
packages/message-parser/tests/unorderedList.test.ts
Normal file
76
packages/message-parser/tests/unorderedList.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { parse } from '../src';
|
||||
import { unorderedList, plain, listItem, bold, emoji } from './helpers';
|
||||
|
||||
test.each([
|
||||
[
|
||||
`
|
||||
- First item
|
||||
- Second item
|
||||
- Third item
|
||||
- *Fourth item*
|
||||
- :smile:
|
||||
`.trim(),
|
||||
[
|
||||
unorderedList([
|
||||
listItem([plain('First item')]),
|
||||
listItem([plain('Second item')]),
|
||||
listItem([plain('Third item')]),
|
||||
listItem([bold([plain('Fourth item')])]),
|
||||
listItem([emoji('smile')]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
`
|
||||
* First item
|
||||
* Second item
|
||||
* Third item
|
||||
* *Fourth item*
|
||||
`.trim(),
|
||||
[
|
||||
unorderedList([
|
||||
listItem([plain('First item')]),
|
||||
listItem([plain('Second item')]),
|
||||
listItem([plain('Third item')]),
|
||||
listItem([bold([plain('Fourth item')])]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
|
||||
[
|
||||
`
|
||||
- First item
|
||||
* Second item
|
||||
* Third item
|
||||
* *Fourth item*
|
||||
`.trim(),
|
||||
[
|
||||
unorderedList([listItem([plain('First item')])]),
|
||||
unorderedList([listItem([plain('Second item')]), listItem([plain('Third item')]), listItem([bold([plain('Fourth item')])])]),
|
||||
],
|
||||
],
|
||||
// [
|
||||
// `
|
||||
// * First item
|
||||
// * Second item
|
||||
// * Third item
|
||||
// * Indented item
|
||||
// * Indented item
|
||||
// * Fourth item
|
||||
// `.trim(),
|
||||
// [paragraph([])],
|
||||
// ],
|
||||
// [
|
||||
// `
|
||||
// - First item
|
||||
// - Second item
|
||||
// - Third item
|
||||
// - Indented item
|
||||
// - Indented item
|
||||
// - Fourth item
|
||||
// `.trim(),
|
||||
// [paragraph([])],
|
||||
// ],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
188
packages/message-parser/tests/url.test.ts
Normal file
188
packages/message-parser/tests/url.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { parse } from '../src';
|
||||
import { lineBreak, paragraph, plain, link } from './helpers';
|
||||
import { autoLink } from '../src/utils';
|
||||
|
||||
test.each([
|
||||
[
|
||||
'https://pt.wikipedia.org/wiki/Condi%C3%A7%C3%A3o_de_corrida#:~:text=Uma%20condi%C3%A7%C3%A3o%20de%20corrida%20%C3%A9,sequ%C3%AAncia%20ou%20sincronia%20doutros%20eventos',
|
||||
[
|
||||
paragraph([
|
||||
link(
|
||||
'https://pt.wikipedia.org/wiki/Condi%C3%A7%C3%A3o_de_corrida#:~:text=Uma%20condi%C3%A7%C3%A3o%20de%20corrida%20%C3%A9,sequ%C3%AAncia%20ou%20sincronia%20doutros%20eventos',
|
||||
),
|
||||
]),
|
||||
],
|
||||
],
|
||||
['https://pt.wikipedia.org/', [paragraph([link('https://pt.wikipedia.org/')])]],
|
||||
['https://pt.wikipedia.org/with-hyphen', [paragraph([link('https://pt.wikipedia.org/with-hyphen')])]],
|
||||
['https://pt.wikipedia.org/with_underscore', [paragraph([link('https://pt.wikipedia.org/with_underscore')])]],
|
||||
[
|
||||
'https://www.npmjs.com/package/@rocket.chat/message-parser',
|
||||
[paragraph([link('https://www.npmjs.com/package/@rocket.chat/message-parser')])],
|
||||
],
|
||||
['http:/rocket.chat/teste', [paragraph([plain('http:/rocket.chat/teste')])]],
|
||||
['https:/rocket.chat/', [paragraph([plain('https:/rocket.chat/')])]],
|
||||
['https://test', [paragraph([plain('https://test')])]],
|
||||
['httpsss://rocket.chat/test', [paragraph([link('httpsss://rocket.chat/test')])]],
|
||||
['https://rocket.chat:3000/test', [paragraph([link('https://rocket.chat:3000/test')])]],
|
||||
['https://rocket.chat:99999', [paragraph([link('https://rocket.chat:99999')])]],
|
||||
['https://rocket.chat:99999/test', [paragraph([link('https://rocket.chat:99999/test')])]],
|
||||
['https://rocket.chat/test?search', [paragraph([link('https://rocket.chat/test?search')])]],
|
||||
['https://rocket.chat/test?search=test', [paragraph([link('https://rocket.chat/test?search=test')])]],
|
||||
['https://rocket.chat', [paragraph([link('https://rocket.chat')])]],
|
||||
['http://127.0.0.1:3000/images/logo/logo.png', [paragraph([link('http://127.0.0.1:3000/images/logo/logo.png')])]],
|
||||
['https://localhost', [paragraph([link('https://localhost')])]],
|
||||
['https://localhost:3000', [paragraph([link('https://localhost:3000')])]],
|
||||
['https://localhost:3000#fragment', [paragraph([link('https://localhost:3000#fragment')])]],
|
||||
['https://localhost:3000#', [paragraph([link('https://localhost:3000#')])]],
|
||||
['https://localhost:3000?', [paragraph([link('https://localhost:3000?')])]],
|
||||
['https://localhost:3000/', [paragraph([link('https://localhost:3000/')])]],
|
||||
['ftp://user:pass@localhost:21/etc/hosts', [paragraph([link('ftp://user:pass@localhost:21/etc/hosts')])]],
|
||||
['ssh://test@example.com', [paragraph([link('ssh://test@example.com')])]],
|
||||
['custom://test@example.com', [paragraph([link('custom://test@example.com')])]],
|
||||
['ftp://example.com', [paragraph([link('ftp://example.com')])]],
|
||||
['https://www.thingiverse.com/thing:5451684', [paragraph([link('https://www.thingiverse.com/thing:5451684')])]],
|
||||
['http://📙.la/❤️', [paragraph([link('http://📙.la/❤️')])]],
|
||||
[
|
||||
'https://developer.rocket.chat/reference/api/rest-api#production-security-concerns look at this',
|
||||
[paragraph([link('https://developer.rocket.chat/reference/api/rest-api#production-security-concerns'), plain(' look at this')])],
|
||||
],
|
||||
[
|
||||
'https://developer.rocket.chat/reference/api/rest-api look at this',
|
||||
[paragraph([link('https://developer.rocket.chat/reference/api/rest-api'), plain(' look at this')])],
|
||||
],
|
||||
|
||||
[
|
||||
'https://developer.rocket.chat/reference/api/rest-api#fragment?query=query look at this',
|
||||
[paragraph([link('https://developer.rocket.chat/reference/api/rest-api#fragment?query=query'), plain(' look at this')])],
|
||||
],
|
||||
['https://developer.rocket.chat look at this', [paragraph([link('https://developer.rocket.chat'), plain(' look at this')])]],
|
||||
[
|
||||
'https://developer.rocket.chat?query=query look at this',
|
||||
[paragraph([link('https://developer.rocket.chat?query=query'), plain(' look at this')])],
|
||||
],
|
||||
[
|
||||
'https://developer.rocket.chat?query=query\nline break',
|
||||
[paragraph([link('https://developer.rocket.chat?query=query')]), paragraph([plain('line break')])],
|
||||
],
|
||||
[
|
||||
'https://developer.rocket.chat?query=query\n\nline break',
|
||||
[paragraph([link('https://developer.rocket.chat?query=query')]), lineBreak(), paragraph([plain('line break')])],
|
||||
],
|
||||
[
|
||||
'https://developer.rocket.chat?query=query_with_underscore look at this',
|
||||
[paragraph([link('https://developer.rocket.chat?query=query_with_underscore'), plain(' look at this')])],
|
||||
],
|
||||
[
|
||||
'https://developer.rocket.chat/path_with_underscore look at this',
|
||||
[paragraph([link('https://developer.rocket.chat/path_with_underscore'), plain(' look at this')])],
|
||||
],
|
||||
[
|
||||
'https://developer.rocket.chat#fragment_with_underscore look at this',
|
||||
[paragraph([link('https://developer.rocket.chat#fragment_with_underscore'), plain(' look at this')])],
|
||||
],
|
||||
['https://developer.rocket.chat followed by text', [paragraph([link('https://developer.rocket.chat'), plain(' followed by text')])]],
|
||||
[
|
||||
'two urls https://developer.rocket.chat , https://rocket.chat',
|
||||
[paragraph([plain('two urls '), link('https://developer.rocket.chat'), plain(' , '), link('https://rocket.chat')])],
|
||||
],
|
||||
['https://1developer.rocket.chat', [paragraph([link('https://1developer.rocket.chat')])]],
|
||||
['https://en.m.wikipedia.org/wiki/Main_Page', [paragraph([link('https://en.m.wikipedia.org/wiki/Main_Page')])]],
|
||||
['test.1test.com', [paragraph([link('//test.1test.com', [plain('test.1test.com')])])]],
|
||||
['http://test.e-xample.com', [paragraph([link('http://test.e-xample.com')])]],
|
||||
['www.n-tv.de', [paragraph([link('//www.n-tv.de', [plain('www.n-tv.de')])])]],
|
||||
['www.n-tv.de/test, test', [paragraph([link('//www.n-tv.de/test', [plain('www.n-tv.de/test')]), plain(', test')])]],
|
||||
['www.n-tv.de/, test', [paragraph([link('//www.n-tv.de/', [plain('www.n-tv.de/')]), plain(', test')])]],
|
||||
['www.n-tv.de, test', [paragraph([link('//www.n-tv.de', [plain('www.n-tv.de')]), plain(', test')])]],
|
||||
['https://www.n-tv.de, test', [paragraph([link('https://www.n-tv.de', [plain('https://www.n-tv.de')]), plain(', test')])]],
|
||||
['http://te_st.com', [paragraph([link('http://te_st.com', [plain('http://te_st.com')])])]],
|
||||
['www.te_st.com', [paragraph([link('//www.te_st.com', [plain('www.te_st.com')])])]],
|
||||
['[google_search](http://google.com)', [paragraph([link('http://google.com', [plain('google_search')])])]],
|
||||
['app...https://rocket.chat https://rocket.chat', [paragraph([plain('app...https://rocket.chat '), link('https://rocket.chat')])]],
|
||||
[
|
||||
'Hey check it out the best communication platform https://rocket.chat! There is not discussion about it.',
|
||||
[
|
||||
paragraph([
|
||||
plain('Hey check it out the best communication platform '),
|
||||
link('https://rocket.chat'),
|
||||
plain('! There is not discussion about it.'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
['This is a normal phrase.This in another phrase.', [paragraph([plain('This is a normal phrase.This in another phrase.')])]],
|
||||
[
|
||||
'https://github.com/RocketChat/Rocket.Chat/releases/tag/6.0.0-rc.3',
|
||||
[paragraph([link('https://github.com/RocketChat/Rocket.Chat/releases/tag/6.0.0-rc.3')])],
|
||||
],
|
||||
[
|
||||
'https://www.rocket.chat/(W(601))/Main?ScreenId=GI000027',
|
||||
[paragraph([link('https://www.rocket.chat/(W(601))/Main?ScreenId=GI000027')])],
|
||||
],
|
||||
[
|
||||
'https://rocketchat.atlassian.net/browse/OC-718?filter=10078&jql=%22Defect%20from%5BVersion%20Picker%20(multiple%20versions)%5D%22%20%3D%206.0.0%20AND%20%22Defect%20from%5BVersion%20Picker%20(multiple%20versions)%5D%22%20%3D%206.0.0%20AND%20created%20%3E%3D%20-48h%20ORDER%20BY%20cf%5B10070%5D%20ASC%2C%20status%20ASC%2C%20created%20DESC',
|
||||
[
|
||||
paragraph([
|
||||
link(
|
||||
'https://rocketchat.atlassian.net/browse/OC-718?filter=10078&jql=%22Defect%20from%5BVersion%20Picker%20(multiple%20versions)%5D%22%20%3D%206.0.0%20AND%20%22Defect%20from%5BVersion%20Picker%20(multiple%20versions)%5D%22%20%3D%206.0.0%20AND%20created%20%3E%3D%20-48h%20ORDER%20BY%20cf%5B10070%5D%20ASC%2C%20status%20ASC%2C%20created%20DESC',
|
||||
),
|
||||
]),
|
||||
],
|
||||
],
|
||||
['go to https://www.google.com.', [paragraph([plain('go to '), link('https://www.google.com'), plain('.')])]],
|
||||
['https://www.google.com.', [paragraph([link('https://www.google.com'), plain('.')])]],
|
||||
['https://www.google.com!', [paragraph([link('https://www.google.com'), plain('!')])]],
|
||||
['visit www.google.com.', [paragraph([plain('visit '), link('//www.google.com', [plain('www.google.com')]), plain('.')])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input)).toEqual(output);
|
||||
});
|
||||
|
||||
describe('autoLink with custom hosts settings comming from Rocket.Chat', () => {
|
||||
test.each([
|
||||
['http://gitlab.local', [paragraph([link('http://gitlab.local', [plain('http://gitlab.local')])])]],
|
||||
['gitlab.local', [paragraph([link('//gitlab.local', [plain('gitlab.local')])])]],
|
||||
['internaltool.intranet', [paragraph([link('//internaltool.intranet', [plain('internaltool.intranet')])])]],
|
||||
])('parses %p', (input, output) => {
|
||||
expect(parse(input, { customDomains: ['local', 'intranet'] })).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('autoLink WITHOUT custom hosts settings comming from Rocket.Chat', () => {
|
||||
test.each([['https://internaltool.testt', [paragraph([plain('https://internaltool.testt')])]]])('parses %p', (input, output) => {
|
||||
expect(parse(input, { customDomains: ['local'] })).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('autoLink helper function', () => {
|
||||
it('should preserve the original protocol if the protocol is http or https', () => {
|
||||
expect(autoLink('https://rocket.chat/test')).toEqual(link('https://rocket.chat/test'));
|
||||
|
||||
expect(autoLink('http://rocket.chat/test')).toEqual(link('http://rocket.chat/test'));
|
||||
});
|
||||
|
||||
it('should preserve the original protocol for invalid absolute URLs', () => {
|
||||
expect(autoLink('https://rocket.chat:99999')).toMatchObject(link('https://rocket.chat:99999'));
|
||||
expect(autoLink('https://rocket.chat:65536')).toMatchObject(link('https://rocket.chat:65536'));
|
||||
});
|
||||
|
||||
it('should preserve the original protocol even if for custom protocols', () => {
|
||||
expect(autoLink('custom://rocket.chat/test')).toEqual(link('custom://rocket.chat/test'));
|
||||
});
|
||||
|
||||
it('should return // as the protocol if // is the protocol specified', () => {
|
||||
expect(autoLink('//rocket.chat/test')).toEqual(link('//rocket.chat/test'));
|
||||
});
|
||||
|
||||
it("should return an url concatenated '//' if the url has no protocol", () => {
|
||||
expect(autoLink('rocket.chat/test')).toEqual(link('//rocket.chat/test', [plain('rocket.chat/test')]));
|
||||
});
|
||||
|
||||
it("should return an url concatenated '//' if the url has no protocol and has sub-domain", () => {
|
||||
expect(autoLink('spark-public.s3.amazonaws.com')).toEqual(
|
||||
link('//spark-public.s3.amazonaws.com', [plain('spark-public.s3.amazonaws.com')]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return an plain text url due to invalid TLD that's validate with the external library TLDTS", () => {
|
||||
expect(autoLink('rocket.chattt/url_path')).toEqual(plain('rocket.chattt/url_path'));
|
||||
});
|
||||
});
|
||||
10
packages/message-parser/tsconfig.build.json
Normal file
10
packages/message-parser/tsconfig.build.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
8
packages/message-parser/tsconfig.json
Normal file
8
packages/message-parser/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@rocket.chat/tsconfig/server.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"target": "es2020",
|
||||
"module": "esnext"
|
||||
}
|
||||
}
|
||||
40
packages/message-parser/webpack.config.ts
Normal file
40
packages/message-parser/webpack.config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default [
|
||||
{
|
||||
entry: './src/index.ts',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: {
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: resolve('./tsconfig.build.json'),
|
||||
},
|
||||
},
|
||||
include: [resolve('./src')],
|
||||
exclude: [resolve('./tests')],
|
||||
},
|
||||
{
|
||||
test: /\.pegjs$/,
|
||||
use: ['@rocket.chat/peggy-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.pegjs'],
|
||||
},
|
||||
mode: 'production',
|
||||
experiments: {
|
||||
outputModule: true,
|
||||
},
|
||||
output: {
|
||||
path: resolve('./dist'),
|
||||
filename: 'messageParser.js',
|
||||
library: {
|
||||
type: 'module',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user