Remove HTML version of parse()

This commit is contained in:
Pavel Djundik 2018-07-12 11:41:40 +03:00 committed by Pavel Djundik
parent d83dcc35e2
commit e3ff385ae0
18 changed files with 113 additions and 180 deletions

View File

@ -28,8 +28,9 @@
<span class="title">{{ channel.name }}</span> <span class="title">{{ channel.name }}</span>
<span <span
:title="channel.topic" :title="channel.topic"
class="topic" class="topic"><ParsedMessage
v-html="$options.filters.parse(channel.topic)"/> v-if="channel.topic"
:text="channel.topic"/></span>
<button <button
class="menu" class="menu"
aria-label="Open the context menu" aria-label="Open the context menu"
@ -86,6 +87,7 @@
<script> <script>
require("intersection-observer"); require("intersection-observer");
const socket = require("../js/socket"); const socket = require("../js/socket");
import ParsedMessage from "./ParsedMessage.vue";
import MessageList from "./MessageList.vue"; import MessageList from "./MessageList.vue";
import ChatInput from "./ChatInput.vue"; import ChatInput from "./ChatInput.vue";
import ChatUserList from "./ChatUserList.vue"; import ChatUserList from "./ChatUserList.vue";
@ -96,6 +98,7 @@ import ListIgnored from "./Special/ListIgnored.vue";
export default { export default {
name: "Chat", name: "Chat",
components: { components: {
ParsedMessage,
MessageList, MessageList,
ChatInput, ChatInput,
ChatUserList, ChatUserList,

View File

@ -28,9 +28,7 @@
</template> </template>
</span> </span>
<span class="content"> <span class="content">
<span <span class="text"><ParsedMessage :message="message"/></span>
ref="text"
class="text"><ParsedMessage :message="message"/></span>
<LinkPreview <LinkPreview
v-for="preview in message.previews" v-for="preview in message.previews"

View File

@ -1,24 +1,24 @@
<template> <template>
<span class="content"> <span class="content">
<template v-if="message.self"> <ParsedMessage
<i v-html="$options.filters.parse(message.text)"/> v-if="message.self"
</template> :message="message"/>
<template v-else> <template v-else>
<Username :user="message.from"/> <Username :user="message.from"/>
is away is away
<i <i class="away-message">(<ParsedMessage :message="message"/>)</i>
class="away-message"
v-html="'(' + $options.filters.parse(message.text) + ')'"/>
</template> </template>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeAway", name: "MessageTypeAway",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -1,8 +1,8 @@
<template> <template>
<span class="content"> <span class="content">
<template v-if="message.self"> <ParsedMessage
<i v-html="$options.filters.parse(message.text)"/> v-if="message.self"
</template> :message="message"/>
<template v-else> <template v-else>
<Username :user="message.from"/> <Username :user="message.from"/>
is back is back

View File

@ -1,18 +1,18 @@
<template> <template>
<span class="content"> <span class="content">
<Username :user="message.from"/> <Username :user="message.from"/>
<span <span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage"/></span>
class="ctcp-message"
v-html="$options.filters.parse(message.ctcpMessage)"/>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeCTCP", name: "MessageTypeCTCP",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -2,18 +2,18 @@
<span class="content"> <span class="content">
<Username :user="message.from"/> <Username :user="message.from"/>
sent a <abbr title="Client-to-client protocol">CTCP</abbr> request: sent a <abbr title="Client-to-client protocol">CTCP</abbr> request:
<span <span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage"/></span>
class="ctcp-message"
v-html="$options.filters.parse(message.ctcpMessage)"/>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeRequestCTCP", name: "MessageTypeRequestCTCP",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -6,16 +6,18 @@
<Username <Username
v-else v-else
:user="message.target"/> :user="message.target"/>
to <span v-html="$options.filters.parse(message.channel)"/> to <ParsedMessage :text="message.channel"/>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeInvite", name: "MessageTypeInvite",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -5,17 +5,18 @@
<Username :user="message.target"/> <Username :user="message.target"/>
<i <i
v-if="message.text" v-if="message.text"
class="part-reason" class="part-reason">(<ParsedMessage :message="message"/>)</i>
v-html="'(' + $options.filters.parse(message.text) + ')'"/>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeKick", name: "MessageTypeKick",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -2,16 +2,18 @@
<span class="content"> <span class="content">
<Username :user="message.from"/> <Username :user="message.from"/>
sets mode sets mode
<span v-html="$options.filters.parse(message.text)"/> <ParsedMessage :message="message"/>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeMode", name: "MessageTypeMode",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -5,17 +5,18 @@
has left the channel has left the channel
<i <i
v-if="message.text" v-if="message.text"
class="part-reason" class="part-reason">(<ParsedMessage :message="message"/>)</i>
v-html="'(' + $options.filters.parse(message.text) + ')'"/>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypePart", name: "MessageTypePart",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -5,17 +5,18 @@
has quit has quit
<i <i
v-if="message.text" v-if="message.text"
class="quit-reason" class="quit-reason">(<ParsedMessage :message="message"/>)</i>
v-html="'(' + $options.filters.parse(message.text) + ')'"/>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeQuit", name: "MessageTypeQuit",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -9,17 +9,18 @@
</template> </template>
<span <span
v-if="message.text" v-if="message.text"
class="new-topic" class="new-topic"><ParsedMessage :message="message"/></span>
v-html="$options.filters.parse(message.text)"/>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeTopic", name: "MessageTypeTopic",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -27,7 +27,7 @@
<template v-if="message.whois.real_name"> <template v-if="message.whois.real_name">
<dt>Real name:</dt> <dt>Real name:</dt>
<dd v-html="$options.filters.parse(message.whois.real_name)"/> <dd><ParsedMessage :text="message.whois.real_name"/></dd>
</template> </template>
<template v-if="message.whois.registered_nick"> <template v-if="message.whois.registered_nick">
@ -37,7 +37,7 @@
<template v-if="message.whois.channels"> <template v-if="message.whois.channels">
<dt>Channels:</dt> <dt>Channels:</dt>
<dd v-html="$options.filters.parse(message.whois.channels)"/> <dd><ParsedMessage :text="message.whois.channels"/></dd>
</template> </template>
<template v-if="message.whois.modes"> <template v-if="message.whois.modes">
@ -67,7 +67,7 @@
<template v-if="message.whois.away"> <template v-if="message.whois.away">
<dt>Away:</dt> <dt>Away:</dt>
<dd v-html="$options.filters.parse(message.whois.away)"/> <dd><ParsedMessage :text="message.whois.away"/></dd>
</template> </template>
<template v-if="message.whois.secure"> <template v-if="message.whois.secure">
@ -94,11 +94,13 @@
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeWhois", name: "MessageTypeWhois",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View File

@ -5,10 +5,15 @@ export default {
name: "ParsedMessage", name: "ParsedMessage",
functional: true, functional: true,
props: { props: {
text: String,
message: Object, message: Object,
}, },
render(createElement, context) { render(createElement, context) {
return parse(context.props.message.text, context.props.message, createElement); if (typeof context.props.text !== "undefined") {
return parse(createElement, context.props.text);
}
return parse(createElement, context.props.message.text, context.props.message);
}, },
}; };
</script> </script>

View File

@ -12,9 +12,7 @@
v-for="ban in channel.data" v-for="ban in channel.data"
:key="ban.hostmask"> :key="ban.hostmask">
<td class="hostmask">{{ ban.hostmask }}</td> <td class="hostmask">{{ ban.hostmask }}</td>
<td <td class="banned_by">{{ ban.banned_by }}</td>
class="banned_by"
v-html="$options.filters.parse(ban.banned_by)"/>
<td class="banned_at">{{ ban.banned_at | localetime }}</td> <td class="banned_at">{{ ban.banned_at | localetime }}</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -14,21 +14,22 @@
<tr <tr
v-for="chan in channel.data" v-for="chan in channel.data"
:key="chan.channel"> :key="chan.channel">
<td <td class="channel"><ParsedMessage :text="chan.channel"/></td>
class="channel"
v-html="$options.filters.parse(chan.channel)"/>
<td class="users">{{ chan.num_users }}</td> <td class="users">{{ chan.num_users }}</td>
<td <td class="topic"><ParsedMessage :text="chan.topic"/></td>
class="topic"
v-html="$options.filters.parse(chan.topic)"/>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
export default { export default {
name: "ListChannels", name: "ListChannels",
components: {
ParsedMessage,
},
props: { props: {
channel: Object, channel: Object,
}, },

View File

@ -1,6 +1,5 @@
"use strict"; "use strict";
const Handlebars = require("handlebars/runtime");
const parseStyle = require("./ircmessageparser/parseStyle"); const parseStyle = require("./ircmessageparser/parseStyle");
const findChannels = require("./ircmessageparser/findChannels"); const findChannels = require("./ircmessageparser/findChannels");
const findLinks = require("./ircmessageparser/findLinks"); const findLinks = require("./ircmessageparser/findLinks");
@ -12,58 +11,7 @@ const emojiMap = require("../fullnamemap.json");
const LinkPreviewToggle = require("../../../components/LinkPreviewToggle.vue").default; const LinkPreviewToggle = require("../../../components/LinkPreviewToggle.vue").default;
// Create an HTML `span` with styling information for a given fragment // Create an HTML `span` with styling information for a given fragment
function createFragment(fragment) { function createFragment(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");
}
let attributes = classes.length ? ` class="${classes.join(" ")}"` : "";
const escapedText = Handlebars.Utils.escapeExpression(fragment.text);
if (fragment.hexColor) {
attributes += ` style="color:#${fragment.hexColor}`;
if (fragment.hexBgColor) {
attributes += `;background-color:#${fragment.hexBgColor}`;
}
attributes += '"';
}
if (attributes.length) {
return `<span${attributes}>${escapedText}</span>`;
}
return escapedText;
}
function createVueFragment(fragment, createElement) {
const classes = []; const classes = [];
if (fragment.bold) { if (fragment.bold) {
@ -109,7 +57,7 @@ function createVueFragment(fragment, createElement) {
// Transform an IRC message potentially filled with styling control codes, URLs, // 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. // nicknames, and channels into a string of HTML elements to display on the client.
module.exports = function parse(text, message = null, createElement = null) { module.exports = function parse(createElement, text, message = null) {
// Extract the styling information and get the plain text version from it // Extract the styling information and get the plain text version from it
const styleFragments = parseStyle(text); const styleFragments = parseStyle(text);
const cleanText = styleFragments.map((fragment) => fragment.text).join(""); const cleanText = styleFragments.map((fragment) => fragment.text).join("");
@ -129,97 +77,69 @@ module.exports = function parse(text, message = null, createElement = null) {
.concat(emojiParts) .concat(emojiParts)
.concat(nameParts); .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) {
const preview = message && message.previews.find((p) => p.link === textPart.link);
const link = createElement("a", {
class: [
"inline-channel",
],
attrs: {
href: textPart.link,
target: "_blank",
rel: "noopener",
},
}, fragments);
if (!preview) {
return link;
}
return [link, createElement(LinkPreviewToggle, {
class: ["toggle-button", "toggle-preview"],
props: {
link: preview,
},
}, 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 // Merge the styling information with the channels / URLs / nicks / text objects and
// generate HTML strings with the resulting fragments // generate HTML strings with the resulting fragments
return merge(parts, styleFragments, cleanText).map((textPart) => { return merge(parts, styleFragments, cleanText).map((textPart) => {
// Create HTML strings with styling information const fragments = textPart.fragments.map((fragment) => createFragment(fragment, createElement));
const fragments = textPart.fragments.map(createFragment).join("");
// Wrap these potentially styled fragments with links and channel buttons // Wrap these potentially styled fragments with links and channel buttons
if (textPart.link) { if (textPart.link) {
const escapedLink = Handlebars.Utils.escapeExpression(textPart.link); const preview = message && message.previews.find((p) => p.link === textPart.link);
return `<a href="${escapedLink}" target="_blank" rel="noopener">${fragments}</a>` + const link = createElement("a", {
`<button class="toggle-button toggle-preview" data-url="${escapedLink}" hidden></button>`; class: [
} else if (textPart.channel) { "inline-channel",
const escapedChannel = Handlebars.Utils.escapeExpression(textPart.channel); ],
return `<span class="inline-channel" role="button" tabindex="0" data-chan="${escapedChannel}">${fragments}</span>`; attrs: {
} else if (textPart.emoji) { href: textPart.link,
if (!emojiMap[textPart.emoji]) { target: "_blank",
return `<span class="emoji" role="img">${fragments}</span>`; rel: "noopener",
},
}, fragments);
if (!preview) {
return link;
} }
return `<span class="emoji" role="img" aria-label="Emoji: ${emojiMap[textPart.emoji]}" title="${emojiMap[textPart.emoji]}">${fragments}</span>`; return [link, createElement(LinkPreviewToggle, {
class: ["toggle-button", "toggle-preview"],
props: {
link: preview,
},
}, 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) { } else if (textPart.nick) {
const nick = Handlebars.Utils.escapeExpression(textPart.nick); return createElement("span", {
return `<span role="button" class="user ${colorClass(textPart.nick)}" data-name="${nick}">${fragments}</span>`; class: [
"user",
colorClass(textPart.nick),
],
attrs: {
"role": "button",
"data-name": textPart.nick,
},
}, fragments);
} }
return fragments; return fragments;
}).join(""); });
}; };

View File

@ -3,7 +3,6 @@
const Vue = require("vue").default; const Vue = require("vue").default;
const App = require("../components/App.vue").default; const App = require("../components/App.vue").default;
const roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber"); const roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber");
const parse = require("./libs/handlebars/parse");
const tz = require("./libs/handlebars/tz"); const tz = require("./libs/handlebars/tz");
const localetime = require("./libs/handlebars/localetime"); const localetime = require("./libs/handlebars/localetime");
const localedate = require("./libs/handlebars/localedate"); const localedate = require("./libs/handlebars/localedate");
@ -11,7 +10,6 @@ const friendlydate = require("./libs/handlebars/friendlydate");
const friendlysize = require("./libs/handlebars/friendlysize"); const friendlysize = require("./libs/handlebars/friendlysize");
const colorClass = require("./libs/handlebars/colorClass"); const colorClass = require("./libs/handlebars/colorClass");
Vue.filter("parse", parse);
Vue.filter("tz", tz); Vue.filter("tz", tz);
Vue.filter("localetime", localetime); Vue.filter("localetime", localetime);
Vue.filter("localedate", localedate); Vue.filter("localedate", localedate);