Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
9 KB
Referenced Files
None
Subscribers
None
diff --git a/src/services/mini_html_converter/mini_html_converter.service.js b/src/services/mini_html_converter/mini_html_converter.service.js
index 879ff544c9..01f8adf8ed 100644
--- a/src/services/mini_html_converter/mini_html_converter.service.js
+++ b/src/services/mini_html_converter/mini_html_converter.service.js
@@ -1,138 +1,138 @@
/**
* This is a not-so-tiny purpose-built HTML parser/processor. It was made for use
* with StatusText component for purpose of replacing tags with vue components
*
* known issue: doesn't handle CDATA so nested CDATA might not work well
*
* @param {Object} input - input data
* @param {(string) => string} lineProcessor - function that will be called on every line
* @param {{ key[string]: (string) => string}} tagProcessor - map of processors for tags
* @return {string} processed html
*/
export const convertHtml = (html) => {
// Elements that are implicitly self-closing
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
const emptyElements = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
])
// TODO For future - also parse HTML5 multi-source components?
const buffer = [] // Current output buffer
const levels = [['', buffer]] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
const getCurrentBuffer = () => {
return levels[levels.length - 1][1]
}
const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer === '') return
getCurrentBuffer().push(textBuffer)
textBuffer = ''
}
const handleSelfClosing = (tag) => {
getCurrentBuffer().push([tag])
}
const handleOpen = (tag) => {
const curBuf = getCurrentBuffer()
const newLevel = [tag, []]
levels.push(newLevel)
curBuf.push(newLevel)
}
const handleClose = (tag) => {
const currentTag = levels[levels.length - 1]
if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) {
currentTag.push(tag)
levels.pop()
} else {
getCurrentBuffer().push(tag)
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
flushText()
tagBuffer = char
} else if (char !== '>' && tagBuffer !== null) {
tagBuffer += char
} else if (char === '>' && tagBuffer !== null) {
tagBuffer += char
const tagFull = tagBuffer
tagBuffer = null
const tagName = getTagName(tagFull)
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') {
// self-closing
handleSelfClosing(tagFull)
} else {
handleOpen(tagFull)
}
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flushText()
return buffer
}
// Extracts tag name from tag, i.e. <span a="b"> => span
export const getTagName = (tag) => {
const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag)
return result && (result[1] || result[2])
}
export const processTextForEmoji = (text, emojis, processor) => {
const buffer = []
let textBuffer = ''
for (let i = 0; i < text.length; i++) {
const char = text[i]
if (char === ':') {
const next = text.slice(i + 1)
let found = false
for (let emoji of emojis) {
if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
found = emoji
break
}
}
if (found) {
buffer.push(textBuffer)
textBuffer = ''
buffer.push(processor(found))
i += found.shortcode.length + 1
} else {
textBuffer += char
}
} else {
textBuffer += char
}
}
if (textBuffer) buffer.push(textBuffer)
return buffer
}
export const getAttrs = tag => {
const innertag = tag
.substring(1, tag.length - 1)
.replace(new RegExp('^' + getTagName(tag)), '')
.replace(/\/?$/, '')
.trim()
- const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=(?:"([^"]+?)"|'([^']+?)'))?/gi))
+ const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
.map(([trash, key, value]) => [key, value])
.map(([k, v]) => {
if (!v) return [k, true]
- return [k, v]
+ return [k, v.substring(1, v.length - 1)]
})
return Object.fromEntries(attrs)
}
diff --git a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js
index b42f5f35fe..8df2fbc3b9 100644
--- a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js
+++ b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js
@@ -1,166 +1,166 @@
import { convertHtml, processTextForEmoji, getAttrs } from 'src/services/mini_html_converter/mini_html_converter.service.js'
describe('MiniHtmlConverter', () => {
describe('convertHtml', () => {
it('converts html into a tree structure', () => {
const input = '1 <p>2</p> <b>3<img src="a">4</b>5'
expect(convertHtml(input)).to.eql([
'1 ',
[
'<p>',
['2'],
'</p>'
],
' ',
[
'<b>',
[
'3',
['<img src="a">'],
'4'
],
'</b>'
],
'5'
])
})
it('converts html to tree while preserving tag formatting', () => {
const input = '1 <p >2</p><b >3<img src="a">4</b>5'
expect(convertHtml(input)).to.eql([
'1 ',
[
'<p >',
['2'],
'</p>'
],
[
'<b >',
[
'3',
['<img src="a">'],
'4'
],
'</b>'
],
'5'
])
})
it('converts semi-broken html', () => {
const input = '1 <br> 2 <p> 42'
expect(convertHtml(input)).to.eql([
'1 ',
['<br>'],
' 2 ',
[
'<p>',
[' 42']
]
])
})
it('realistic case 1', () => {
const input = '<p><span class="h-card"><a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">@<span>benis</span></a></span> <span class="h-card"><a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">@<span>hj</span></a></span> nice</p>'
expect(convertHtml(input)).to.eql([
[
'<p>',
[
[
'<span class="h-card">',
[
[
'<a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">',
[
'@',
[
'<span>',
[
'benis'
],
'</span>'
]
],
'</a>'
]
],
'</span>'
],
' ',
[
'<span class="h-card">',
[
[
'<a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">',
[
'@',
[
'<span>',
[
'hj'
],
'</span>'
]
],
'</a>'
]
],
'</span>'
],
' nice'
],
'</p>'
]
])
})
it('realistic case 2', () => {
const inputOutput = 'Country improv: give me a city<br/>Audience: Memphis<br/>Improv troupe: come on, a better one<br/>Audience: el paso'
expect(convertHtml(inputOutput)).to.eql([
'Country improv: give me a city',
[
'<br/>'
],
'Audience: Memphis',
[
'<br/>'
],
'Improv troupe: come on, a better one',
[
'<br/>'
],
'Audience: el paso'
])
})
})
describe('processTextForEmoji', () => {
it('processes all emoji in text', () => {
const input = 'Hello from finland! :lol: We have best water! :lmao:'
const emojis = [
{ shortcode: 'lol', src: 'LOL' },
{ shortcode: 'lmao', src: 'LMAO' }
]
const processor = ({ shortcode, src }) => ({ shortcode, src })
expect(processTextForEmoji(input, emojis, processor)).to.eql([
'Hello from finland! ',
{ shortcode: 'lol', src: 'LOL' },
' We have best water! ',
{ shortcode: 'lmao', src: 'LMAO' }
])
})
it('leaves text as is', () => {
const input = 'Number one: that\'s terror'
const emojis = []
const processor = ({ shortcode, src }) => ({ shortcode, src })
expect(processTextForEmoji(input, emojis, processor)).to.eql([
'Number one: that\'s terror'
])
})
})
describe('getAttrs', () => {
it('extracts arguments from tag', () => {
- const input = '<img src="boop" cool ebin="true">'
+ const input = '<img src="boop" cool ebin=\'true\'>'
const output = { src: 'boop', cool: true, ebin: 'true' }
expect(getAttrs(input)).to.eql(output)
})
})
})

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 30, 5:33 AM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799560
Default Alt Text
(9 KB)

Event Timeline