diff --git a/client/components/Message.vue b/client/components/Message.vue
index 6c7dc380..acddec41 100644
--- a/client/components/Message.vue
+++ b/client/components/Message.vue
@@ -30,8 +30,7 @@
+ class="text">
import Username from "./Username.vue";
import LinkPreview from "./LinkPreview.vue";
+import ParsedMessage from "./ParsedMessage.vue";
import MessageTypes from "./MessageTypes";
+MessageTypes.ParsedMessage = ParsedMessage;
MessageTypes.LinkPreview = LinkPreview;
MessageTypes.Username = Username;
diff --git a/client/components/ParsedMessage.vue b/client/components/ParsedMessage.vue
new file mode 100644
index 00000000..4be022da
--- /dev/null
+++ b/client/components/ParsedMessage.vue
@@ -0,0 +1,14 @@
+
diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js
index 7c120a5b..3d14947b 100644
--- a/client/js/libs/handlebars/parse.js
+++ b/client/js/libs/handlebars/parse.js
@@ -62,9 +62,53 @@ function createFragment(fragment) {
return escapedText;
}
+function createVueFragment(fragment, createElement) {
+ const classes = [];
+
+ if (fragment.bold) {
+ classes.push("irc-bold");
+ }
+
+ if (fragment.textColor !== undefined) {
+ classes.push("irc-fg" + fragment.textColor);
+ }
+
+ if (fragment.bgColor !== undefined) {
+ classes.push("irc-bg" + fragment.bgColor);
+ }
+
+ if (fragment.italic) {
+ classes.push("irc-italic");
+ }
+
+ if (fragment.underline) {
+ classes.push("irc-underline");
+ }
+
+ if (fragment.strikethrough) {
+ classes.push("irc-strikethrough");
+ }
+
+ if (fragment.monospace) {
+ classes.push("irc-monospace");
+ }
+
+ if (classes.length === 0 && !fragment.hexColor) {
+ return fragment.text;
+ }
+
+ return createElement("span", {
+ class: classes,
+ style: {
+ color: `#${fragment.hexColor}`,
+ "background-color": fragment.hexBgColor ? `#${fragment.hexBgColor}` : null,
+ },
+ }, fragment.text);
+}
+
// Transform an IRC message potentially filled with styling control codes, URLs,
// nicknames, and channels into a string of HTML elements to display on the client.
-module.exports = function parse(text, users = []) {
+module.exports = function parse(text, users = [], createElement = null) {
// Extract the styling information and get the plain text version from it
const styleFragments = parseStyle(text);
const cleanText = styleFragments.map((fragment) => fragment.text).join("");
@@ -84,6 +128,60 @@ module.exports = function parse(text, users = []) {
.concat(emojiParts)
.concat(nameParts);
+ if (createElement) {
+ return merge(parts, styleFragments, cleanText).map((textPart) => {
+ const fragments = textPart.fragments.map((fragment) => createVueFragment(fragment, createElement));
+
+ // Wrap these potentially styled fragments with links and channel buttons
+ if (textPart.link) {
+ return createElement("a", {
+ class: [
+ "inline-channel",
+ ],
+ attrs: {
+ href: textPart.link,
+ target: "_blank",
+ rel: "noopener",
+ },
+ }, fragments);
+ } else if (textPart.channel) {
+ return createElement("span", {
+ class: [
+ "inline-channel",
+ ],
+ attrs: {
+ role: "button",
+ tabindex: 0,
+ "data-chan": textPart.channel,
+ },
+ }, fragments);
+ } else if (textPart.emoji) {
+ return createElement("span", {
+ class: [
+ "emoji",
+ ],
+ attrs: {
+ role: "img",
+ "aria-label": emojiMap[textPart.emoji] ? `Emoji: ${emojiMap[textPart.emoji]}` : null,
+ },
+ }, fragments);
+ } else if (textPart.nick) {
+ return createElement("span", {
+ class: [
+ "user",
+ colorClass(textPart.nick),
+ ],
+ attrs: {
+ role: "button",
+ "data-name": textPart.nick,
+ },
+ }, fragments);
+ }
+
+ return fragments;
+ });
+ }
+
// Merge the styling information with the channels / URLs / nicks / text objects and
// generate HTML strings with the resulting fragments
return merge(parts, styleFragments, cleanText).map((textPart) => {