aboutsummaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/services')
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js42
-rw-r--r--src/services/favicon_service/favicon_service.js70
-rw-r--r--src/services/file_type/file_type.service.js4
-rw-r--r--src/services/gesture_service/gesture_service.js137
-rw-r--r--src/services/html_converter/html_line_converter.service.js136
-rw-r--r--src/services/html_converter/html_tree_converter.service.js98
-rw-r--r--src/services/html_converter/utility.service.js73
-rw-r--r--src/services/ruffle_service/ruffle_service.js40
-rw-r--r--src/services/theme_data/pleromafe.js6
-rw-r--r--src/services/tiny_post_html_processor/tiny_post_html_processor.service.js94
-rw-r--r--src/services/user_highlighter/user_highlighter.js14
11 files changed, 558 insertions, 156 deletions
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 9b2b30e6..f219c161 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -44,6 +44,7 @@ export const parseUser = (data) => {
const mastoShort = masto && !data.hasOwnProperty('avatar')
output.id = String(data.id)
+ output._original = data // used for server-side settings
if (masto) {
output.screen_name = data.acct
@@ -54,17 +55,20 @@ export const parseUser = (data) => {
return output
}
- output.name = data.display_name
- output.name_html = addEmojis(escape(data.display_name), data.emojis)
+ output.emoji = data.emojis
+ output.name = escape(data.display_name)
+ output.name_html = output.name
+ output.name_unescaped = data.display_name
output.description = data.note
- output.description_html = addEmojis(data.note, data.emojis)
+ // TODO cleanup this shit, output.description is overriden with source data
+ output.description_html = data.note
output.fields = data.fields
output.fields_html = data.fields.map(field => {
return {
- name: addEmojis(escape(field.name), data.emojis),
- value: addEmojis(field.value, data.emojis)
+ name: escape(field.name),
+ value: field.value
}
})
output.fields_text = data.fields.map(field => {
@@ -205,7 +209,7 @@ export const parseUser = (data) => {
// Convert punycode to unicode for UI
output.screen_name_ui = output.screen_name
- if (output.screen_name.includes('@')) {
+ if (output.screen_name && output.screen_name.includes('@')) {
const parts = output.screen_name.split('@')
let unicodeDomain = punycode.toUnicode(parts[1])
if (unicodeDomain !== parts[1]) {
@@ -239,16 +243,6 @@ export const parseAttachment = (data) => {
return output
}
-export const addEmojis = (string, emojis) => {
- const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g
- return emojis.reduce((acc, emoji) => {
- const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
- return acc.replace(
- new RegExp(`:${regexSafeShortCode}:`, 'g'),
- `<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />`
- )
- }, string)
-}
export const parseStatus = (data) => {
const output = {}
@@ -266,7 +260,8 @@ export const parseStatus = (data) => {
output.type = data.reblog ? 'retweet' : 'status'
output.nsfw = data.sensitive
- output.statusnet_html = addEmojis(data.content, data.emojis)
+ output.raw_html = data.content
+ output.emojis = data.emojis
output.tags = data.tags
@@ -293,13 +288,13 @@ export const parseStatus = (data) => {
output.retweeted_status = parseStatus(data.reblog)
}
- output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
+ output.summary_raw_html = escape(data.spoiler_text)
output.external_url = data.url
output.poll = data.poll
if (output.poll) {
output.poll.options = (output.poll.options || []).map(field => ({
...field,
- title_html: addEmojis(escape(field.title), data.emojis)
+ title_html: escape(field.title)
}))
}
output.pinned = data.pinned
@@ -325,7 +320,7 @@ export const parseStatus = (data) => {
output.nsfw = data.nsfw
}
- output.statusnet_html = data.statusnet_html
+ output.raw_html = data.statusnet_html
output.text = data.text
output.in_reply_to_status_id = data.in_reply_to_status_id
@@ -444,11 +439,8 @@ export const parseChatMessage = (message) => {
output.id = message.id
output.created_at = new Date(message.created_at)
output.chat_id = message.chat_id
- if (message.content) {
- output.content = addEmojis(message.content, message.emojis)
- } else {
- output.content = ''
- }
+ output.emojis = message.emojis
+ output.content = message.content
if (message.attachment) {
output.attachments = [parseAttachment(message.attachment)]
} else {
diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js
index d1ddee41..7e19629d 100644
--- a/src/services/favicon_service/favicon_service.js
+++ b/src/services/favicon_service/favicon_service.js
@@ -1,52 +1,58 @@
-import { find } from 'lodash'
-
const createFaviconService = () => {
- let favimg, favcanvas, favcontext, favicon
+ const favicons = []
const faviconWidth = 128
const faviconHeight = 128
const badgeRadius = 32
const initFaviconService = () => {
- const nodes = document.getElementsByTagName('link')
- favicon = find(nodes, node => node.rel === 'icon')
- if (favicon) {
- favcanvas = document.createElement('canvas')
- favcanvas.width = faviconWidth
- favcanvas.height = faviconHeight
- favimg = new Image()
- favimg.src = favicon.href
- favcontext = favcanvas.getContext('2d')
- }
+ const nodes = document.querySelectorAll('link[rel="icon"]')
+ nodes.forEach(favicon => {
+ if (favicon) {
+ const favcanvas = document.createElement('canvas')
+ favcanvas.width = faviconWidth
+ favcanvas.height = faviconHeight
+ const favimg = new Image()
+ favimg.crossOrigin = 'anonymous'
+ favimg.src = favicon.href
+ const favcontext = favcanvas.getContext('2d')
+ favicons.push({ favcanvas, favimg, favcontext, favicon })
+ }
+ })
}
const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0
const clearFaviconBadge = () => {
- if (!favimg || !favcontext || !favicon) return
+ if (favicons.length === 0) return
+ favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
+ if (!favimg || !favcontext || !favicon) return
- favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
- if (isImageLoaded(favimg)) {
- favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
- }
- favicon.href = favcanvas.toDataURL('image/png')
+ favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
+ if (isImageLoaded(favimg)) {
+ favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
+ }
+ favicon.href = favcanvas.toDataURL('image/png')
+ })
}
const drawFaviconBadge = () => {
- if (!favimg || !favcontext || !favcontext) return
-
+ if (favicons.length === 0) return
clearFaviconBadge()
+ favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
+ if (!favimg || !favcontext || !favcontext) return
+
+ const style = getComputedStyle(document.body)
+ const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
- const style = getComputedStyle(document.body)
- const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
-
- if (isImageLoaded(favimg)) {
- favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
- }
- favcontext.fillStyle = badgeColor
- favcontext.beginPath()
- favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
- favcontext.fill()
- favicon.href = favcanvas.toDataURL('image/png')
+ if (isImageLoaded(favimg)) {
+ favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
+ }
+ favcontext.fillStyle = badgeColor
+ favcontext.beginPath()
+ favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
+ favcontext.fill()
+ favicon.href = favcanvas.toDataURL('image/png')
+ })
}
return {
diff --git a/src/services/file_type/file_type.service.js b/src/services/file_type/file_type.service.js
index 2a046bec..5182ecd1 100644
--- a/src/services/file_type/file_type.service.js
+++ b/src/services/file_type/file_type.service.js
@@ -2,6 +2,10 @@
// or the entire service could be just mimetype service that only operates
// on mimetypes and not files. Currently the naming is confusing.
const fileType = mimetype => {
+ if (mimetype.match(/flash/)) {
+ return 'flash'
+ }
+
if (mimetype.match(/text\/html/)) {
return 'html'
}
diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js
index 88a328f3..265a7f25 100644
--- a/src/services/gesture_service/gesture_service.js
+++ b/src/services/gesture_service/gesture_service.js
@@ -4,9 +4,15 @@ const DIRECTION_RIGHT = [1, 0]
const DIRECTION_UP = [0, -1]
const DIRECTION_DOWN = [0, 1]
+const BUTTON_LEFT = 0
+
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
-const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
+const touchCoord = touch => [touch.screenX, touch.screenY]
+
+const touchEventCoord = e => touchCoord(e.touches[0])
+
+const pointerEventCoord = e => [e.clientX, e.clientY]
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
@@ -61,6 +67,132 @@ const updateSwipe = (event, gesture) => {
gesture._swiping = false
}
+class SwipeAndClickGesture {
+ // swipePreviewCallback(offsets: Array[Number])
+ // offsets: the offset vector which the underlying component should move, from the starting position
+ // swipeEndCallback(sign: 0|-1|1)
+ // sign: if the swipe does not meet the threshold, 0
+ // if the swipe meets the threshold in the positive direction, 1
+ // if the swipe meets the threshold in the negative direction, -1
+ constructor ({
+ direction,
+ // swipeStartCallback
+ swipePreviewCallback,
+ swipeEndCallback,
+ swipeCancelCallback,
+ swipelessClickCallback,
+ threshold = 30,
+ perpendicularTolerance = 1.0,
+ disableClickThreshold = 1
+ }) {
+ const nop = () => {}
+ this.direction = direction
+ this.swipePreviewCallback = swipePreviewCallback || nop
+ this.swipeEndCallback = swipeEndCallback || nop
+ this.swipeCancelCallback = swipeCancelCallback || nop
+ this.swipelessClickCallback = swipelessClickCallback || nop
+ this.threshold = typeof threshold === 'function' ? threshold : () => threshold
+ this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold
+ this.perpendicularTolerance = perpendicularTolerance
+ this._reset()
+ }
+
+ _reset () {
+ this._startPos = [0, 0]
+ this._pointerId = -1
+ this._swiping = false
+ this._swiped = false
+ this._preventNextClick = false
+ }
+
+ start (event) {
+ // Only handle left click
+ if (event.button !== BUTTON_LEFT) {
+ return
+ }
+
+ this._startPos = pointerEventCoord(event)
+ this._pointerId = event.pointerId
+ this._swiping = true
+ this._swiped = false
+ }
+
+ move (event) {
+ if (this._swiping && this._pointerId === event.pointerId) {
+ this._swiped = true
+
+ const coord = pointerEventCoord(event)
+ const delta = deltaCoord(this._startPos, coord)
+
+ this.swipePreviewCallback(delta)
+ }
+ }
+
+ cancel (event) {
+ if (!this._swiping || this._pointerId !== event.pointerId) {
+ return
+ }
+
+ this.swipeCancelCallback()
+ }
+
+ end (event) {
+ if (!this._swiping) {
+ return
+ }
+
+ if (this._pointerId !== event.pointerId) {
+ return
+ }
+
+ this._swiping = false
+
+ // movement too small
+ const coord = pointerEventCoord(event)
+ const delta = deltaCoord(this._startPos, coord)
+
+ const sign = (() => {
+ if (vectorLength(delta) < this.threshold()) {
+ return 0
+ }
+ // movement is opposite from direction
+ const isPositive = dotProduct(delta, this.direction) > 0
+
+ // movement perpendicular to direction is too much
+ const towardsDir = project(delta, this.direction)
+ const perpendicularDir = perpendicular(this.direction)
+ const towardsPerpendicular = project(delta, perpendicularDir)
+ if (
+ vectorLength(towardsDir) * this.perpendicularTolerance <
+ vectorLength(towardsPerpendicular)
+ ) {
+ return 0
+ }
+
+ return isPositive ? 1 : -1
+ })()
+
+ if (this._swiped) {
+ this.swipeEndCallback(sign)
+ }
+ this._reset()
+ // Only a mouse will fire click event when
+ // the end point is far from the starting point
+ // so for other kinds of pointers do not check
+ // whether we have swiped
+ if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') {
+ this._preventNextClick = true
+ }
+ }
+
+ click (event) {
+ if (!this._preventNextClick) {
+ this.swipelessClickCallback()
+ }
+ this._reset()
+ }
+}
+
const GestureService = {
DIRECTION_LEFT,
DIRECTION_RIGHT,
@@ -68,7 +200,8 @@ const GestureService = {
DIRECTION_DOWN,
swipeGesture,
beginSwipe,
- updateSwipe
+ updateSwipe,
+ SwipeAndClickGesture
}
export default GestureService
diff --git a/src/services/html_converter/html_line_converter.service.js b/src/services/html_converter/html_line_converter.service.js
new file mode 100644
index 00000000..5eeaa7cb
--- /dev/null
+++ b/src/services/html_converter/html_line_converter.service.js
@@ -0,0 +1,136 @@
+import { getTagName } from './utility.service.js'
+
+/**
+ * This is a tiny purpose-built HTML parser/processor. This basically detects
+ * any type of visual newline and converts entire HTML into a array structure.
+ *
+ * Text nodes are represented as object with single property - text - containing
+ * the visual line. Intended usage is to process the array with .map() in which
+ * map function returns a string and resulting array can be converted back to html
+ * with a .join('').
+ *
+ * Generally this isn't very useful except for when you really need to either
+ * modify visual lines (greentext i.e. simple quoting) or do something with
+ * first/last line.
+ *
+ * known issue: doesn't handle CDATA so nested CDATA might not work well
+ *
+ * @param {Object} input - input data
+ * @return {(string|{ text: string })[]} processed html in form of a list.
+ */
+export const convertHtmlToLines = (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'
+ ])
+ // Block-level element (they make a visual line)
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
+ const blockElements = new Set([
+ 'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd',
+ 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main',
+ 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
+ ])
+ // br is very weird in a way that it's technically not block-level, it's
+ // essentially converted to a \n (or \r\n). There's also wbr but it doesn't
+ // guarantee linebreak, only suggest it.
+ const linebreakElements = new Set(['br'])
+
+ const visualLineElements = new Set([
+ ...blockElements.values(),
+ ...linebreakElements.values()
+ ])
+
+ // All block-level elements that aren't empty elements, i.e. not <hr>
+ const nonEmptyElements = new Set(visualLineElements)
+ // Difference
+ for (let elem of emptyElements) {
+ nonEmptyElements.delete(elem)
+ }
+
+ // All elements that we are recognizing
+ const allElements = new Set([
+ ...nonEmptyElements.values(),
+ ...emptyElements.values()
+ ])
+
+ let buffer = [] // Current output buffer
+ const level = [] // 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 flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
+ if (textBuffer.trim().length > 0) {
+ buffer.push({ level: [...level], text: textBuffer })
+ } else {
+ buffer.push(textBuffer)
+ }
+ textBuffer = ''
+ }
+
+ const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
+ flush()
+ buffer.push(tag)
+ }
+
+ const handleOpen = (tag) => { // handles opening tags
+ flush()
+ buffer.push(tag)
+ level.unshift(getTagName(tag))
+ }
+
+ const handleClose = (tag) => { // handles closing tags
+ if (level[0] === getTagName(tag)) {
+ flush()
+ buffer.push(tag)
+ level.shift()
+ } else { // Broken case
+ textBuffer += tag
+ }
+ }
+
+ for (let i = 0; i < html.length; i++) {
+ const char = html[i]
+ if (char === '<' && tagBuffer === null) {
+ 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 (allElements.has(tagName)) {
+ if (linebreakElements.has(tagName)) {
+ handleBr(tagFull)
+ } else if (nonEmptyElements.has(tagName)) {
+ if (tagFull[1] === '/') {
+ handleClose(tagFull)
+ } else if (tagFull[tagFull.length - 2] === '/') {
+ // self-closing
+ handleBr(tagFull)
+ } else {
+ handleOpen(tagFull)
+ }
+ } else {
+ textBuffer += tagFull
+ }
+ } else {
+ textBuffer += tagFull
+ }
+ } else if (char === '\n') {
+ handleBr(char)
+ } else {
+ textBuffer += char
+ }
+ }
+ if (tagBuffer) {
+ textBuffer += tagBuffer
+ }
+
+ flush()
+
+ return buffer
+}
diff --git a/src/services/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js
new file mode 100644
index 00000000..247a8173
--- /dev/null
+++ b/src/services/html_converter/html_tree_converter.service.js
@@ -0,0 +1,98 @@
+import { getTagName } from './utility.service.js'
+import { unescape } from 'lodash'
+
+/**
+ * This is a not-so-tiny purpose-built HTML parser/processor. This parses html
+ * and converts it into a tree structure representing tag openers/closers and
+ * children.
+ *
+ * Structure follows this pattern: [opener, [...children], closer] except root
+ * node which is just [...children]. Text nodes can only be within children and
+ * are represented as strings.
+ *
+ * Intended use is to convert HTML structure and then recursively iterate over it
+ * most likely using a map. Very useful for dynamically rendering html replacing
+ * tags with JSX elements in a render function.
+ *
+ * known issue: doesn't handle CDATA so CDATA might not work well
+ * known issue: doesn't handle HTML comments
+ *
+ * @param {Object} input - input data
+ * @return {string} processed html
+ */
+export const convertHtmlToTree = (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 = [unescape(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
+}
diff --git a/src/services/html_converter/utility.service.js b/src/services/html_converter/utility.service.js
new file mode 100644
index 00000000..4d0c36c2
--- /dev/null
+++ b/src/services/html_converter/utility.service.js
@@ -0,0 +1,73 @@
+/**
+ * Extract tag name from tag opener/closer.
+ *
+ * @param {String} tag - tag string, i.e. '<a href="...">'
+ * @return {String} - tagname, i.e. "div"
+ */
+export const getTagName = (tag) => {
+ const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag)
+ return result && (result[1] || result[2])
+}
+
+/**
+ * Extract attributes from tag opener.
+ *
+ * @param {String} tag - tag string, i.e. '<a href="...">'
+ * @return {Object} - map of attributes key = attribute name, value = attribute value
+ * attributes without values represented as boolean true
+ */
+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))
+ .map(([trash, key, value]) => [key, value])
+ .map(([k, v]) => {
+ if (!v) return [k, true]
+ return [k, v.substring(1, v.length - 1)]
+ })
+ return Object.fromEntries(attrs)
+}
+
+/**
+ * Finds shortcodes in text
+ *
+ * @param {String} text - original text to find emojis in
+ * @param {{ url: String, shortcode: Sring }[]} emoji - list of shortcodes to find
+ * @param {Function} processor - function to call on each encountered emoji,
+ * function is passed single object containing matching emoji ({ url, shortcode })
+ * return value will be inserted into resulting array instead of :shortcode:
+ * @return {Array} resulting array with non-emoji parts of text and whatever {processor}
+ * returned for emoji
+ */
+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
+}
diff --git a/src/services/ruffle_service/ruffle_service.js b/src/services/ruffle_service/ruffle_service.js
new file mode 100644
index 00000000..7411dd96
--- /dev/null
+++ b/src/services/ruffle_service/ruffle_service.js
@@ -0,0 +1,40 @@
+const createRuffleService = () => {
+ let ruffleInstance = null
+
+ const getRuffle = () => new Promise((resolve, reject) => {
+ if (ruffleInstance) {
+ resolve(ruffleInstance)
+ return
+ }
+ // Ruffle needs these to be set before it's loaded
+ // https://github.com/ruffle-rs/ruffle/issues/3952
+ window.RufflePlayer = {}
+ window.RufflePlayer.config = {
+ polyfills: false,
+ publicPath: '/static/ruffle'
+ }
+
+ // Currently it's seems like a better way of loading ruffle
+ // because it needs the wasm publically accessible, but it needs path to it
+ // and filename of wasm seems to be pseudo-randomly generated (is it a hash?)
+ const script = document.createElement('script')
+ // see webpack config, using CopyPlugin to copy it from node_modules
+ // provided via ruffle-mirror
+ script.src = '/static/ruffle/ruffle.js'
+ script.type = 'text/javascript'
+ script.onerror = (e) => { reject(e) }
+ script.onabort = (e) => { reject(e) }
+ script.oncancel = (e) => { reject(e) }
+ script.onload = () => {
+ ruffleInstance = window.RufflePlayer
+ resolve(ruffleInstance)
+ }
+ document.body.appendChild(script)
+ })
+
+ return { getRuffle }
+}
+
+const RuffleService = createRuffleService()
+
+export default RuffleService
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 14aac975..c2983be7 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -369,6 +369,12 @@ export const SLOT_INHERITANCE = {
textColor: 'preserve'
},
+ postCyantext: {
+ depends: ['cBlue'],
+ layer: 'bg',
+ textColor: 'preserve'
+ },
+
border: {
depends: ['fg'],
opacity: 'border',
diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
deleted file mode 100644
index de6f20ef..00000000
--- a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
- * allows it to be processed, useful for greentexting, mostly
- *
- * known issue: doesn't handle CDATA so nested CDATA might not work well
- *
- * @param {Object} input - input data
- * @param {(string) => string} processor - function that will be called on every line
- * @return {string} processed html
- */
-export const processHtml = (html, processor) => {
- const handledTags = new Set(['p', 'br', 'div'])
- const openCloseTags = new Set(['p', 'div'])
-
- let buffer = '' // Current output buffer
- const level = [] // 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
-
- // Extracts tag name from tag, i.e. <span a="b"> => span
- const getTagName = (tag) => {
- const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
- return result && (result[1] || result[2])
- }
-
- const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
- if (textBuffer.trim().length > 0) {
- buffer += processor(textBuffer)
- } else {
- buffer += textBuffer
- }
- textBuffer = ''
- }
-
- const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
- flush()
- buffer += tag
- }
-
- const handleOpen = (tag) => { // handles opening tags
- flush()
- buffer += tag
- level.push(tag)
- }
-
- const handleClose = (tag) => { // handles closing tags
- flush()
- buffer += tag
- if (level[level.length - 1] === tag) {
- level.pop()
- }
- }
-
- for (let i = 0; i < html.length; i++) {
- const char = html[i]
- if (char === '<' && tagBuffer === null) {
- 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 (handledTags.has(tagName)) {
- if (tagName === 'br') {
- handleBr(tagFull)
- } else if (openCloseTags.has(tagName)) {
- if (tagFull[1] === '/') {
- handleClose(tagFull)
- } else if (tagFull[tagFull.length - 2] === '/') {
- // self-closing
- handleBr(tagFull)
- } else {
- handleOpen(tagFull)
- }
- }
- } else {
- textBuffer += tagFull
- }
- } else if (char === '\n') {
- handleBr(char)
- } else {
- textBuffer += char
- }
- }
- if (tagBuffer) {
- textBuffer += tagBuffer
- }
-
- flush()
-
- return buffer
-}
diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js
index b91c0f78..3b07592e 100644
--- a/src/services/user_highlighter/user_highlighter.js
+++ b/src/services/user_highlighter/user_highlighter.js
@@ -8,6 +8,11 @@ const highlightStyle = (prefs) => {
const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`
const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`
+ const customProps = {
+ '--____highlight-solidColor': solidColor,
+ '--____highlight-tintColor': tintColor,
+ '--____highlight-tintColor2': tintColor2
+ }
if (type === 'striped') {
return {
backgroundImage: [
@@ -17,11 +22,13 @@ const highlightStyle = (prefs) => {
`${tintColor2} 20px,`,
`${tintColor2} 40px`
].join(' '),
- backgroundPosition: '0 0'
+ backgroundPosition: '0 0',
+ ...customProps
}
} else if (type === 'solid') {
return {
- backgroundColor: tintColor2
+ backgroundColor: tintColor2,
+ ...customProps
}
} else if (type === 'side') {
return {
@@ -31,7 +38,8 @@ const highlightStyle = (prefs) => {
`${solidColor} 2px,`,
`transparent 6px`
].join(' '),
- backgroundPosition: '0 0'
+ backgroundPosition: '0 0',
+ ...customProps
}
}
}