| 147 |
147 |
|
/// make the server fetch an attacker-chosen address. That is the security
|
| 148 |
148 |
|
/// boundary the preset exists to hold, and `chat_render_path_cannot_fetch`
|
| 149 |
149 |
|
/// in this module pins it.
|
|
150 |
+ |
///
|
|
151 |
+ |
/// One thing the preset deliberately does not do: resolve `@mentions`. In a
|
|
152 |
+ |
/// room a mention is a client-side highlight for whoever is present, not a
|
|
153 |
+ |
/// link, so `@name` comes out of here as the plain text it went in as.
|
|
154 |
+ |
/// `resolve_mentions` (the `mentions` feature) stays a step a consumer
|
|
155 |
+ |
/// applies to markdown by choice, never a stage of this renderer, and chat
|
|
156 |
+ |
/// consumers do not apply it. `chat_leaves_mentions_as_plain_text` pins that.
|
| 150 |
157 |
|
pub fn chat() -> Self {
|
| 151 |
158 |
|
Self {
|
| 152 |
159 |
|
tables: false,
|
| 693 |
700 |
|
}
|
| 694 |
701 |
|
}
|
| 695 |
702 |
|
|
|
703 |
+ |
#[test]
|
|
704 |
+ |
fn chat_leaves_mentions_as_plain_text() {
|
|
705 |
+ |
// Mention highlighting in a room is client-side and in-room only, so the
|
|
706 |
+ |
// chat render path must never turn `@name` into a profile link. A link
|
|
707 |
+ |
// is the wrong artifact twice over: it outlives the room, surviving into
|
|
708 |
+ |
// every archive and quote that reuses the HTML, and it implies a
|
|
709 |
+ |
// "which usernames exist" lookup at render time, on a hot path, for a
|
|
710 |
+ |
// cosmetic effect the client already has the roster to produce.
|
|
711 |
+ |
//
|
|
712 |
+ |
// The invariant is structural today. `resolve_mentions` is a free
|
|
713 |
+ |
// function a consumer runs over markdown before rendering, not a stage
|
|
714 |
+ |
// of `Renderer`, so `chat()` has no path to it. This is the tripwire for
|
|
715 |
+ |
// the day someone wires one into the other.
|
|
716 |
+ |
let r = Renderer::chat();
|
|
717 |
+ |
|
|
718 |
+ |
let html = r.render("hi @alice, see [docs](https://example.com)");
|
|
719 |
+ |
assert!(
|
|
720 |
+ |
html.contains("@alice"),
|
|
721 |
+ |
"the mention text survives verbatim: {html}"
|
|
722 |
+ |
);
|
|
723 |
+ |
assert!(
|
|
724 |
+ |
!html.contains(">@alice</a>"),
|
|
725 |
+ |
"the mention must not become an anchor: {html}"
|
|
726 |
+ |
);
|
|
727 |
+ |
assert_eq!(
|
|
728 |
+ |
html.matches("<a ").count(),
|
|
729 |
+ |
1,
|
|
730 |
+ |
"exactly one anchor, the one the author wrote: {html}"
|
|
731 |
+ |
);
|
|
732 |
+ |
|
|
733 |
+ |
// A message that is nothing but a mention renders to no markup at all
|
|
734 |
+ |
// beyond the text, which is the case a resolution step would break most
|
|
735 |
+ |
// visibly.
|
|
736 |
+ |
let html = r.render("@bob");
|
|
737 |
+ |
assert!(!html.contains("<a"), "no anchor: {html}");
|
|
738 |
+ |
assert!(html.contains("@bob"), "text survives: {html}");
|
|
739 |
+ |
}
|
|
740 |
+ |
|
| 696 |
741 |
|
#[test]
|
| 697 |
742 |
|
fn chat_flattens_block_structure() {
|
| 698 |
743 |
|
let r = Renderer::chat();
|