diff --git a/web/index.html b/web/index.html index afde7ef..4dc9900 100644 --- a/web/index.html +++ b/web/index.html @@ -8,6 +8,7 @@ + diff --git a/web/js/ui/fmt.js b/web/js/ui/fmt.js index 9a2debc..732e2ff 100644 --- a/web/js/ui/fmt.js +++ b/web/js/ui/fmt.js @@ -7,21 +7,21 @@ function linkify(text="", show_media=false) { } catch (err) { return match; } - let html; + let markup; if (show_media && is_img_url(parsed.pathname)) { - html = ` + markup = html` `; } else if (show_media && is_video_url(parsed.pathname)) { - html = ` + markup = html` `; } else { - html = `${url}`; + markup = html`${url}`; } - return p1+html; + return p1+markup; }) } diff --git a/web/js/ui/render.js b/web/js/ui/render.js index afb0851..80f3f9a 100644 --- a/web/js/ui/render.js +++ b/web/js/ui/render.js @@ -67,13 +67,13 @@ function render_replying_to(model, ev) { if (pubkeys.length === 0 && ev.refs.reply) { const replying_to = model.all_events[ev.refs.reply] if (!replying_to) - return `
reply to ${ev.refs.reply}
`; + return html`
reply to ${ev.refs.reply}
`; pubkeys = [replying_to.pubkey] } const names = pubkeys.map((pk) => { return render_mentioned_name(pk, model.profiles[pk]); }).join(", ") - return ` + return html` replying to ${names} @@ -100,23 +100,23 @@ function render_comment_body(model, ev, opts) { // Only show media for content that is by friends. const show_media = !opts.is_composing && model.contacts.friends.has(ev.pubkey); - return ` + return html`
- ${render_replying_to(model, ev)} - ${render_shared_by(ev, opts)} + $${render_replying_to(model, ev)} + $${render_shared_by(ev, opts)}

- ${format_content(ev, show_media)} + $${format_content(ev, show_media)}

- ${render_reactions(model, ev)} - ${bar}` + $${render_reactions(model, ev)} + $${bar}` } function render_shared_by(ev, opts) { if (!opts.shared) return ""; const { profile, pubkey } = opts.shared - return `
Shared by ${render_name(pubkey, profile)} + return html`
Shared by $${render_name(pubkey, profile)}
` } @@ -137,22 +137,22 @@ function render_event(model, ev, opts={}) { const border_bottom = opts.is_composing || has_bot_line ? "" : "bottom-border"; let thread_btn = ""; if (!reply_line_bot) reply_line_bot = ''; - return `
+ return html`
- ${render_reply_line_top(has_top_line)} - ${render_pfp(ev.pubkey, profile)} - ${reply_line_bot} + $${render_reply_line_top(has_top_line)} + $${render_pfp(ev.pubkey, profile)} + $${reply_line_bot}
- ${render_name(ev.pubkey, profile)} + $${render_name(ev.pubkey, profile)} ${delta}
- ${render_comment_body(model, ev, opts)} + $${render_comment_body(model, ev, opts)}
` @@ -161,17 +161,17 @@ function render_event(model, ev, opts={}) { function render_event_nointeract(model, ev, opts={}) { const profile = model.profiles[ev.pubkey]; const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000) - return `
+ return html`
- ${render_pfp(ev.pubkey, profile)} + $${render_pfp(ev.pubkey, profile)}
- ${render_name(ev.pubkey, profile)} + $${render_name(ev.pubkey, profile)} ${delta}
- ${render_comment_body(model, ev, opts)} + $${render_comment_body(model, ev, opts)}
` @@ -180,9 +180,9 @@ function render_event_nointeract(model, ev, opts={}) { function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) { const reaction = reactions[our_pubkey] if (!reaction) { - return `onclick="send_reply('${emoji}', '${reacting_to}')"` + return html`onclick="send_reply('${emoji}', '${reacting_to}')"` } else { - return `onclick="delete_post('${reaction.id}')"` + return html`onclick="delete_post('${reaction.id}')"` } } @@ -200,12 +200,12 @@ function render_reaction_group(model, emoji, reactions, reacting_to) { } let onclick = render_react_onclick(model.pubkey, reacting_to.id, emoji, reactions); - return ` - + return html` + ${emoji} - ${str} + $${str} `; } @@ -214,7 +214,7 @@ function render_action_bar(model, ev, opts={}) { let { can_delete } = opts; let delete_html = "" if (can_delete) { - delete_html = ` + delete_html = html` ` @@ -224,7 +224,7 @@ function render_action_bar(model, ev, opts={}) { const reaction = model_get_reacts_to(model, pubkey, ev.id, R_HEART); const liked = !!reaction; const reaction_id = reaction ? reaction.id : ""; - return ` + return html`
- ${delete_html} + $${delete_html}
` @@ -254,7 +254,7 @@ function render_reactions_inner(model, ev) { } function render_reactions(model, ev) { - return `
${render_reactions_inner(model, ev)}
` + return html`
${render_reactions_inner(model, ev)}
` } // Utility Methods @@ -274,7 +274,7 @@ function render_mentioned_name(pk, profile) { function render_name(pk, profile, prefix="") { // Beware of whitespace. - return `${prefix}${prefix}${fmt_profile_name(profile, fmt_pubkey(pk))}` } @@ -284,7 +284,7 @@ function render_pfp(pk, profile, opts={}) { let str = `class="pfp clickable" onclick="open_profile('${pk}')"`; if (opts.noclick) str = "class='pfp'"; - return `
diff --git a/web/js/ui/safe-html.js b/web/js/ui/safe-html.js new file mode 100644 index 0000000..0a8a797 --- /dev/null +++ b/web/js/ui/safe-html.js @@ -0,0 +1,38 @@ +// https://github.com/AntonioVdlC/html-template-tag + +const chars = { + "&": "&", + ">": ">", + "<": "<", + '"': """, + "'": "'", + "`": "`", +}; + +// Dynamically create a RegExp from the `chars` object +const re = new RegExp(Object.keys(chars).join("|"), "g"); + +// Return the escaped string +function escape(str) { + return String(str).replace(re, (match) => chars[match]); +} + +function html( + literals, + ...substs +) { + return literals.raw.reduce((acc, lit, i) => { + let subst = substs[i - 1]; + if (Array.isArray(subst)) { + subst = subst.join(""); + } else if (literals.raw[i - 1] && literals.raw[i - 1].endsWith("$")) { + // If the interpolation is preceded by a dollar sign, + // substitution is considered safe and will not be escaped + acc = acc.slice(0, -1); + } else { + subst = escape(subst); + } + + return acc + subst + lit; + }); +}