diff --git a/app/soapbox/components/status_content.js b/app/soapbox/components/status_content.js
index d539a31f7..5ea78b02e 100644
--- a/app/soapbox/components/status_content.js
+++ b/app/soapbox/components/status_content.js
@@ -6,6 +6,7 @@ import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
import Icon from 'soapbox/components/icon';
+import { processHtml } from 'soapbox/utils/tiny_post_html_processor';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
@@ -157,25 +158,27 @@ export default class StatusContent extends React.PureComponent {
return this.greentext(properContent);
}
- greentext = string => {
- if (!this.props.greentext) return string;
+ greentext = html => {
+ if (!this.props.greentext) return html;
// Copied from Pleroma FE
// https://git.pleroma.social/pleroma/pleroma-fe/-/blob/19475ba356c3fd6c54ca0306d3ae392358c212d1/src/components/status_content/status_content.js#L132
- try {
- if (string.includes('>') &&
- string
- .replace(/<[^>]+?>/gi, '') // remove all tags
- .replace(/@\w+/gi, '') // remove mentions (even failed ones)
- .trim()
- .startsWith('>')) {
- return `${string}`;
- } else {
+ return processHtml(html, (string) => {
+ try {
+ if (string.includes('>') &&
+ string
+ .replace(/<[^>]+?>/gi, '') // remove all tags
+ .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+ .trim()
+ .startsWith('>')) {
+ return `${string}`;
+ } else {
+ return string;
+ }
+ } catch(e) {
return string;
}
- } catch(e) {
- return string;
- }
+ });
}
render() {
diff --git a/app/soapbox/utils/tiny_post_html_processor.js b/app/soapbox/utils/tiny_post_html_processor.js
new file mode 100644
index 000000000..288d53c07
--- /dev/null
+++ b/app/soapbox/utils/tiny_post_html_processor.js
@@ -0,0 +1,97 @@
+// Copied from Pleroma FE
+// https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
+
+/**
+ * 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
+ 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;
+};