Skip to content

Commit 1f19bcc

Browse files
authored
Add html generator (#94)
* Add html generator * use node16 CI * Update README.md * Update index.js * fix test
1 parent 8dba368 commit 1f19bcc

File tree

5 files changed

+82
-5
lines changed

5 files changed

+82
-5
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
node-version: [14.x]
16+
node-version: [16.x]
1717

1818
steps:
1919
- uses: actions/checkout@v2

README.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,32 @@ console.log(msg.toString()) // Example chat message
2828
#### chat.toString([lang])
2929

3030
Flattens the message in to plain-text
31-
* `lang` - (optional) - Set a custom lang (defaults to mcData.language)
31+
* `lang` - (optional) - Set a custom lang (defaults to registry.language)
3232

3333
#### chat.toMotd([lang], parent)
3434

3535
Converts to motd format
36-
* `lang` - (optional) - Set a custom lang (defaults to mcData.language)
36+
* `lang` - (optional) - Set a custom lang (defaults to registry.language)
3737
* `parent` - Set a custom lang (defaults to mcData.language)
3838

3939
#### chat.getText(idx, [lang])
4040

4141
Returns a text part from the message
4242
* `idx` - Index of the part
43-
* `lang` - (optional) - Set a custom lang (defaults to mcData.language)
43+
* `lang` - (optional) - Set a custom lang (defaults to registry.language)
4444

4545
#### chat.toAnsi([lang], [codes])
4646

4747
Converts to ansi format
48-
* `lang` - (optional) - Set a custom lang (defaults to mcData.language)
48+
* `lang` - (optional) - Set a custom lang (defaults to registry.language)
4949
* `codes` - (optional) - Specify which ANSI formatting codes should be used for each Minecraft color code
5050

51+
#### chat.toHTML([lang], [codes], [allowedFormats])
52+
Converts to escaped HTML
53+
* `lang` - (optional) - Set a custom lang (defaults to registry.language)
54+
* `codes` - (optional) - Specify which CSS style props should be used for each Minecraft color code
55+
* `allowedFormats` - The set of allowed formats. Default is ['color', 'bold', 'strikethrough', 'underlined', 'italic']
56+
5157
#### chat.length()
5258

5359
Returns the count of text extras and child ChatMessages

index.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ declare class ChatMessage {
4848
*/
4949
toAnsi(language?: Language): string
5050

51+
/**
52+
* Returns a CSS styled and escaped HTML string
53+
*/
54+
toHTML(language?: Language, styles?: Record<Color, string>, allowedFormats?: DefaultFormats): string
55+
5156
/**
5257
* Returns the count of text extras and child ChatMessages.
5358
* Does not count recursively in to the children.
@@ -226,3 +231,4 @@ type Color =
226231
| "underlined"
227232
| "italic"
228233
| "reset"
234+
type DefaultFormats = 'color' | 'bold' | 'strikethrough' | 'underlined' | 'italic'

index.js

+59
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,29 @@ function loader (registryOrVersion) {
3030
'§k': '\u001b[6m',
3131
'§r': '\u001b[0m'
3232
}
33+
const cssDefaultStyles = {
34+
black: 'color:#000000',
35+
dark_blue: 'color:#0000AA',
36+
dark_green: 'color:#00AA00',
37+
dark_aqua: 'color:#00AAAA',
38+
dark_red: 'color:#AA0000',
39+
dark_purple: 'color:#AA00AA',
40+
gold: 'color:#FFAA00',
41+
gray: 'color:#AAAAAA',
42+
dark_gray: 'color:#555555',
43+
blue: 'color:#5555FF',
44+
green: 'color:#55FF55',
45+
aqua: 'color:#55FFFF',
46+
red: 'color:#FF5555',
47+
light_purple: 'color:#FF55FF',
48+
yellow: 'color:#FFFF55',
49+
white: 'color:#FFFFFF',
50+
bold: 'font-weight:900',
51+
strikethrough: 'text-decoration:line-through',
52+
underlined: 'text-decoration:underline',
53+
italic: 'font-style:italic'
54+
}
55+
const formatMembers = ['color', 'bold', 'strikethrough', 'underlined', 'italic']
3356
const { MessageBuilder } = require('./MessageBuilder')(registry)
3457

3558
/**
@@ -369,6 +392,39 @@ function loader (registryOrVersion) {
369392
return codes['§r'] + message + codes['§r']
370393
}
371394

395+
// NOTE : Have to be be mindful here as bad HTML gen may lead to arbitrary code execution from server
396+
toHTML (lang = registry.language, styles = cssDefaultStyles, allowedFormats = formatMembers) {
397+
let str = ''
398+
if (allowedFormats.some(member => this[member])) {
399+
const cssProps = allowedFormats.reduce((acc, cur) => this[cur]
400+
? acc.push(cur === 'color'
401+
? (this.color.startsWith('#') ? escapeRGB(this.color.slice(1)) : styles[this.color])
402+
: styles[cur]) &&
403+
acc
404+
: acc, [])
405+
str += `<span style="${cssProps.join(';')}">`
406+
} else {
407+
str += '<span>'
408+
}
409+
410+
if (this.text) {
411+
str += escapeHtml(this.text)
412+
} else if (this.translate) {
413+
const params = []
414+
for (const param of this.with) {
415+
params.push(param.toHTML(lang, styles, allowedFormats))
416+
}
417+
const format = lang[this.translate] ?? this.translate
418+
str += vsprintf(escapeHtml(format), params)
419+
}
420+
421+
if (this.extra) {
422+
str += this.extra.map(entry => entry.toHTML(lang, styles, allowedFormats)).join('')
423+
}
424+
str += '</span>'
425+
return str
426+
}
427+
372428
static fromNotch (msg) {
373429
let toRet
374430
try {
@@ -392,3 +448,6 @@ function loader (registryOrVersion) {
392448
ChatMessage.MessageBuilder = MessageBuilder
393449
return ChatMessage
394450
}
451+
452+
const escapeHtml = (unsafe) => unsafe.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;')
453+
const escapeRGB = (unsafe) => `color:rgb(${unsafe.match(/.{2}/g).map(e => parseInt(e, 16)).join(',')})`

test/basic.test.js

+6
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ describe('Parsing chat on 1.16', function () {
9595
const msg = new ChatMessage({ translate: '%2$s %1$s' })
9696
expect(msg.toString()).toBe(' ')
9797
})
98+
99+
it('can format to HTML', () => {
100+
const msg = new ChatMessage({ color: 'blue', translate: 'chat.type.text', with: [{ text: 'IM_U9G', color: 'aqua' }, { text: 'yo sup', color: 'green' }], extra: [{ text: 'test', color: '#ff0000', strikethrough: true }] })
101+
assert.strictEqual(msg.toHTML(), '<span style="color:#5555FF">&lt;<span style="color:#55FFFF">IM_U9G</span>&gt; <span style="color:#55FF55">yo sup</span><span style="color:rgb(255,0,0);text-decoration:line-through">test</span></span>')
102+
assert.strictEqual(msg.toHTML(undefined, undefined, ['color']), '<span style="color:#5555FF">&lt;<span style="color:#55FFFF">IM_U9G</span>&gt; <span style="color:#55FF55">yo sup</span><span style="color:rgb(255,0,0)">test</span></span>')
103+
})
98104
})
99105

100106
describe('Client-side chat formatting', function () {

0 commit comments

Comments
 (0)