feat(parse-mode): support spoiler entity

This commit is contained in:
teidesu 2022-05-06 00:40:47 +03:00
parent 05be58f903
commit 1ef1c0669d
7 changed files with 76 additions and 50 deletions

View file

@ -17,6 +17,7 @@ const entityToType: Partial<
messageEntityPhone: 'phone_number', messageEntityPhone: 'phone_number',
messageEntityPre: 'pre', messageEntityPre: 'pre',
messageEntityStrike: 'strikethrough', messageEntityStrike: 'strikethrough',
messageEntitySpoiler: 'spoiler',
messageEntityTextUrl: 'text_link', messageEntityTextUrl: 'text_link',
messageEntityUnderline: 'underline', messageEntityUnderline: 'underline',
messageEntityUrl: 'url', messageEntityUrl: 'url',
@ -54,6 +55,7 @@ export namespace MessageEntity {
| 'italic' | 'italic'
| 'underline' | 'underline'
| 'strikethrough' | 'strikethrough'
| 'spoiler'
| 'code' | 'code'
| 'pre' | 'pre'
| 'text_link' | 'text_link'

View file

@ -47,6 +47,7 @@ Inline entities are entities that are in-line with other text. We support these
| Italic | `<b>text</b>` | _text_ | | Italic | `<b>text</b>` | _text_ |
| Underline | `<u>text</u>` | <u>text</u> | | Underline | `<u>text</u>` | <u>text</u> |
| Strikethrough | `<s>text</s>` | ~~text~~ | | Strikethrough | `<s>text</s>` | ~~text~~ |
| Spoiler | `<spoiler>text</spoiler>` | N/A |
| Monospace (code) | `<code>text</code>` | `text` | | Monospace (code) | `<code>text</code>` | `text` |
| Text link | `<a href="https://google.com">Google</a>` | [Google](https://google.com) | | Text link | `<a href="https://google.com">Google</a>` | [Google](https://google.com) |
| Text mention | `<a href="tg://user?id=1234567">Name</a>` | N/A | | Text mention | `<a href="tg://user?id=1234567">Name</a>` | N/A |

View file

@ -178,6 +178,13 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
language: attribs.language ?? '', language: attribs.language ?? '',
} }
break break
case 'spoiler':
entity = {
_: 'messageEntitySpoiler',
offset: plainText.length,
length: 0,
}
break
case 'a': { case 'a': {
let url = attribs.href let url = attribs.href
if (!url) return if (!url) return
@ -343,22 +350,25 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
break break
case 'code': case 'code':
case 'pre': case 'pre':
case 'blockquote':
html.push( html.push(
`<${type}${ `<${type}${
type === 'pre' && entity.language entity.language
? ` language="${entity.language}"` ? ` language="${entity.language}"`
: '' : ''
}>${ }>${
this._syntaxHighlighter && entity.language this._syntaxHighlighter && entity.language
? this._syntaxHighlighter( ? this._syntaxHighlighter(
entityText, entityText,
entity.language! entity.language
) )
: entityText : entityText
}</${type}>` }</${type}>`
) )
break break
case 'blockquote':
case 'spoiler':
html.push(`<${type}>${entityText}</${type}>`)
break
case 'email': case 'email':
html.push( html.push(
`<a href="mailto:${entityText}">${entityText}</a>` `<a href="mailto:${entityText}">${entityText}</a>`

View file

@ -59,15 +59,16 @@ describe('HtmlMessageEntityParser', () => {
) )
}) })
it('should handle <code>, <pre>, <blockquote> tags', () => { it('should handle <code>, <pre>, <blockquote>, <spoiler> tags', () => {
test( test(
'plain code pre blockquote plain', 'plain code pre blockquote spoiler plain',
[ [
createEntity('messageEntityCode', 6, 4), createEntity('messageEntityCode', 6, 4),
createEntity('messageEntityPre', 11, 3), createEntity('messageEntityPre', 11, 3),
createEntity('messageEntityBlockquote', 15, 10), createEntity('messageEntityBlockquote', 15, 10),
createEntity('messageEntitySpoiler', 26, 7),
], ],
'plain <code>code</code> <pre>pre</pre> <blockquote>blockquote</blockquote> plain' 'plain <code>code</code> <pre>pre</pre> <blockquote>blockquote</blockquote> <spoiler>spoiler</spoiler> plain'
) )
}) })
@ -309,15 +310,16 @@ describe('HtmlMessageEntityParser', () => {
) )
}) })
it('should handle <code>, <pre>, <blockquote> tags', () => { it('should handle <code>, <pre>, <blockquote>, <spoiler> tags', () => {
test( test(
'plain <code>code</code> <pre>pre</pre> <blockquote>blockquote</blockquote> plain', 'plain <code>code</code> <pre>pre</pre> <blockquote>blockquote</blockquote> <spoiler>spoiler</spoiler> plain',
[ [
createEntity('messageEntityCode', 6, 4), createEntity('messageEntityCode', 6, 4),
createEntity('messageEntityPre', 11, 3, { language: '' }), createEntity('messageEntityPre', 11, 3, { language: '' }),
createEntity('messageEntityBlockquote', 15, 10), createEntity('messageEntityBlockquote', 15, 10),
createEntity('messageEntitySpoiler', 26, 7),
], ],
'plain code pre blockquote plain' 'plain code pre blockquote spoiler plain'
) )
}) })

View file

@ -35,6 +35,7 @@ Supported entities:
- Italic, tag is `__` - Italic, tag is `__`
- Underline, tag is `--` (_NON-STANDARD_) - Underline, tag is `--` (_NON-STANDARD_)
- Strikethrough, tag is `~~` - Strikethrough, tag is `~~`
- Spoiler, tag is `||` (_NON-STANDARD_)
- Code (monospaced font), tag is <code>`</code> - Code (monospaced font), tag is <code>`</code>
- Note that escaping text works differently inside code, see below. - Note that escaping text works differently inside code, see below.
@ -44,16 +45,17 @@ Supported entities:
> >
> This eliminates a lot of confusion, like: `_bold_`_bold_, `**italic**` → **italic** > This eliminates a lot of confusion, like: `_bold_`_bold_, `**italic**` → **italic**
| Code | Result (visual) | Result (as HTML) | Code | Result (visual) | Result (as HTML) |
|---|---|---| |----------------------------------------------|-------------------|------------------------------|
| `**bold**` | **bold** | `<b>bold</b>` | `**bold**` | **bold** | `<b>bold</b>` |
| `__italic__` | __italic__ | `<i>italic</i>` | `__italic__` | __italic__ | `<i>italic</i>` |
| `--underline` | <u>underline</u> | `<u>underline</u>` | `--underline--` | <u>underline</u> | `<u>underline</u>` |
| `~~strikethrough~~` | ~~strikethrough~~ | `<s>strikethrough</s>` | `~~strikethrough~~` | ~~strikethrough~~ | `<s>strikethrough</s>` |
| `*whatever*` | \*whatever\* | `*whatever*` | <code>&#124;&#124;spoiler&#124;&#124;</code> | N/A | `<spoiler>spoiler</spoiler>` |
| `_whatever_` | \_whatever\_ | `_whatever_` | `*whatever*` | \*whatever\* | `*whatever*` |
| <code>\`hello world\`</code> | `hello world` | `<code>hello world</code>` | `_whatever_` | \_whatever\_ | `_whatever_` |
| <code>\`__text__\`</code> | `__text__` | `<code>__text__</code>` | <code>\`hello world\`</code> | `hello world` | `<code>hello world</code>` |
| <code>\`__text__\`</code> | `__text__` | `<code>__text__</code>` |
### Pre ### Pre
@ -62,11 +64,11 @@ Pre represents a single block of code, optionally with a language.
This entity starts with <code>\`\`\`</code> (triple backtick), optionally followed with language name and a must be This entity starts with <code>\`\`\`</code> (triple backtick), optionally followed with language name and a must be
followed with a line break, and ends with <code>\`\`\`</code> (triple backtick), optionally preceded with a line break. followed with a line break, and ends with <code>\`\`\`</code> (triple backtick), optionally preceded with a line break.
| Code | Result (visual) | Result (as HTML) | Code | Result (visual) | Result (as HTML) |
|---|---|---| |--------------------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------|
| <pre><code>\`\`\`<br>hello<br>\`\`\`</code></pre> | `hello` | `<pre>hello</pre>` | <pre><code>\`\`\`<br>hello<br>\`\`\`</code></pre> | `hello` | `<pre>hello</pre>` |
| <pre><code>\`\`\`<br>hello\`\`\`</code></pre> | `hello` | `<pre>hello</pre>` | <pre><code>\`\`\`<br>hello\`\`\`</code></pre> | `hello` | `<pre>hello</pre>` |
| <pre><code>\`\`\`javascript<br>const a = ``<br>\`\`\`</code></pre> | <code>const a = ``</code> | <pre><code>&lt;pre language="javascript"&gt;<br> const a = ``<br>&lt;/pre&gt;</code></pre> | <pre><code>\`\`\`javascript<br>const a = ``<br>\`\`\`</code></pre> | <code>const a = ``</code> | <pre><code>&lt;pre language="javascript"&gt;<br> const a = ``<br>&lt;/pre&gt;</code></pre> |
### Links ### Links
@ -87,13 +89,13 @@ where `1234567` is the ID of the user you want to mention
> where `abc` is user's access hash written as a base-16 *unsigned* integer. > where `abc` is user's access hash written as a base-16 *unsigned* integer.
> Order of the parameters does matter, i.e. `tg://user?hash=abc&id=1234567` will not be processed as expected. > Order of the parameters does matter, i.e. `tg://user?hash=abc&id=1234567` will not be processed as expected.
| Code | Result (visual) | Result (as HTML) | Code | Result (visual) | Result (as HTML) |
|---|---|---| |------------------------------------|--------------------------------|--------------------------------------------------|
| `[Google](https://google.com)` | [Google](https://google.com) | `<a href="https://google.com">Google</a>` | `[Google](https://google.com)` | [Google](https://google.com) | `<a href="https://google.com">Google</a>` |
| `[__Google__](https://google.com)` | [_Google_](https://google.com) | `<a href="https://google.com"><i>Google</i></a>` | `[__Google__](https://google.com)` | [_Google_](https://google.com) | `<a href="https://google.com"><i>Google</i></a>` |
| `[empty link]()` | empty link | `empty link` | `[empty link]()` | empty link | `empty link` |
| `[empty link]` | [empty link] | `[empty link]` | `[empty link]` | [empty link] | `[empty link]` |
| `[User](tg://user?id=1234567)` | N/A | N/A | `[User](tg://user?id=1234567)` | N/A | N/A |
### Nested and overlapping entities ### Nested and overlapping entities
@ -103,10 +105,10 @@ code) can be overlapped.
Since inline entities are only defined by their tag, and nesting same entities doesn't make sense, you can think of the Since inline entities are only defined by their tag, and nesting same entities doesn't make sense, you can think of the
tags just as start/end markers, and not in terms of nesting. tags just as start/end markers, and not in terms of nesting.
| Code | Result (visual) | Result (as HTML) | Code | Result (visual) | Result (as HTML) |
|---|---|---| |-------------------------------|---------------------------|----------------------------------------|
| `**Welcome back, __User__!**` | **Welcome back, _User_!** | `<b>Welcome back, <i>User</i>!</b>` | `**Welcome back, __User__!**` | **Welcome back, _User_!** | `<b>Welcome back, <i>User</i>!</b>` |
| `**bold __and** italic__` | **bold _and_** _italic_ | `<b>bold <i>and</i></b><i> italic</i>` | `**bold __and** italic__` | **bold _and_** _italic_ | `<b>bold <i>and</i></b><i> italic</i>` |
## Escaping ## Escaping
@ -128,10 +130,10 @@ like `"\\_\\_not italic\\_\\_`.
> In theory, you could escape every single non-markup character, but why would you want to do that 😜 > In theory, you could escape every single non-markup character, but why would you want to do that 😜
| Code | Result (visual) | Result (as HTML) | Code | Result (visual) | Result (as HTML) |
|---|---|---| |----------------------------------------|--------------------------------|---------------------------------------------------------|
| `\_\_not italic\_\_` | \_\_not italic\_\_ | `__not italic__` | `\_\_not italic\_\_` | \_\_not italic\_\_ | `__not italic__` |
| `__italic \_ text__` | _italic \_ text_ | `<i>italic _ text </i>` | `__italic \_ text__` | _italic \_ text_ | `<i>italic _ text </i>` |
| <code>\`__not italic__\`</code> | `__not italic__` | `<code>__not italic__</code>` | <code>\`__not italic__\`</code> | `__not italic__` | `<code>__not italic__</code>` |
| <code>C:\\\\Users\\\\Guest</code> | C:\Users\Guest | `C:\Users\Guest` | <code>C:\\\\Users\\\\Guest</code> | C:\Users\Guest | `C:\Users\Guest` |
| <code>\`var a = \\\`hello\\\`\`</code> | <code>var a = \`hello\`</code> | <code>&lt;code&gt;var a = \`hello\`&lt;/code&gt;</code> | <code>\`var a = \\\`hello\\\`\`</code> | <code>var a = \`hello\`</code> | <code>&lt;code&gt;var a = \`hello\`&lt;/code&gt;</code> |

View file

@ -10,10 +10,11 @@ const TAG_BOLD = '**'
const TAG_ITALIC = '__' const TAG_ITALIC = '__'
const TAG_UNDERLINE = '--' const TAG_UNDERLINE = '--'
const TAG_STRIKE = '~~' const TAG_STRIKE = '~~'
const TAG_SPOILER = '||'
const TAG_CODE = '`' const TAG_CODE = '`'
const TAG_PRE = '```' const TAG_PRE = '```'
const TO_BE_ESCAPED = /[*_\-~`[\\\]]/g const TO_BE_ESCAPED = /[*_\-~`[\\\]|]/g
/** /**
* Tagged template based helper for escaping entities in Markdown * Tagged template based helper for escaping entities in Markdown
@ -248,7 +249,7 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
if (c === text[pos + 1]) { if (c === text[pos + 1]) {
// maybe (?) start or end of an entity // maybe (?) start or end of an entity
let type: 'Italic' | 'Bold' | 'Underline' | 'Strike' | null = let type: 'Italic' | 'Bold' | 'Underline' | 'Strike' | 'Spoiler' | null =
null null
switch (c) { switch (c) {
case '_': case '_':
@ -263,6 +264,9 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
case '~': case '~':
type = 'Strike' type = 'Strike'
break break
case '|':
type = 'Spoiler'
break
} }
if (type) { if (type) {
@ -352,6 +356,9 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
case 'strikethrough': case 'strikethrough':
startTag = endTag = TAG_STRIKE startTag = endTag = TAG_STRIKE
break break
case 'spoiler':
startTag = endTag = TAG_SPOILER
break
case 'code': case 'code':
startTag = endTag = TAG_CODE startTag = endTag = TAG_CODE
break break

View file

@ -51,16 +51,17 @@ describe('MarkdownMessageEntityParser', () => {
test('some text', [], 'some text') test('some text', [], 'some text')
}) })
it('should handle bold, italic, underline and strikethrough', () => { it('should handle bold, italic, underline, strikethrough and spoiler', () => {
test( test(
'plain bold italic underline strikethrough plain', 'plain bold italic underline strikethrough spoiler plain',
[ [
createEntity('messageEntityBold', 6, 4), createEntity('messageEntityBold', 6, 4),
createEntity('messageEntityItalic', 11, 6), createEntity('messageEntityItalic', 11, 6),
createEntity('messageEntityUnderline', 18, 9), createEntity('messageEntityUnderline', 18, 9),
createEntity('messageEntityStrike', 28, 13), createEntity('messageEntityStrike', 28, 13),
createEntity('messageEntitySpoiler', 42, 7),
], ],
'plain **bold** __italic__ --underline-- ~~strikethrough~~ plain' 'plain **bold** __italic__ --underline-- ~~strikethrough~~ ||spoiler|| plain'
) )
}) })
@ -294,16 +295,17 @@ describe('MarkdownMessageEntityParser', () => {
} }
} }
it('should handle bold, italic, underline and strikethrough', () => { it('should handle bold, italic, underline, spoiler and strikethrough', () => {
test( test(
'plain **bold** __italic__ --underline-- ~~strikethrough~~ plain', 'plain **bold** __italic__ --underline-- ~~strikethrough~~ ||spoiler|| plain',
[ [
createEntity('messageEntityBold', 6, 4), createEntity('messageEntityBold', 6, 4),
createEntity('messageEntityItalic', 11, 6), createEntity('messageEntityItalic', 11, 6),
createEntity('messageEntityUnderline', 18, 9), createEntity('messageEntityUnderline', 18, 9),
createEntity('messageEntityStrike', 28, 13), createEntity('messageEntityStrike', 28, 13),
createEntity('messageEntitySpoiler', 42, 7),
], ],
'plain bold italic underline strikethrough plain' 'plain bold italic underline strikethrough spoiler plain'
) )
}) })