fix(mattermost): preserve markdown formatting and native tables (#18655)

Merged via squash.

Prepared head SHA: d30fff1776
Co-authored-by: echo931 <259437483+echo931@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Reviewed-by: @mukhtharcm
This commit is contained in:
Echo
2026-03-10 08:10:01 -04:00
committed by GitHub
parent aca216bfcf
commit bda63c3c7f
6 changed files with 137 additions and 10 deletions

View File

@@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import { normalizeMention } from "./monitor-helpers.js";
describe("normalizeMention", () => {
it("returns trimmed text when no mention provided", () => {
expect(normalizeMention(" hello world ", undefined)).toBe("hello world");
});
it("strips bot mention from text", () => {
expect(normalizeMention("@echobot hello", "echobot")).toBe("hello");
});
it("strips mention case-insensitively", () => {
expect(normalizeMention("@EchoBot hello", "echobot")).toBe("hello");
});
it("preserves newlines in multi-line messages", () => {
const input = "@echobot\nline1\nline2\nline3";
const result = normalizeMention(input, "echobot");
expect(result).toBe("line1\nline2\nline3");
});
it("preserves Markdown headings", () => {
const input = "@echobot\n# Heading\n\nSome text";
const result = normalizeMention(input, "echobot");
expect(result).toContain("# Heading");
expect(result).toContain("\n");
});
it("preserves Markdown blockquotes", () => {
const input = "@echobot\n> quoted line\n> second line";
const result = normalizeMention(input, "echobot");
expect(result).toContain("> quoted line");
expect(result).toContain("> second line");
});
it("preserves Markdown lists", () => {
const input = "@echobot\n- item A\n- item B\n - sub B1";
const result = normalizeMention(input, "echobot");
expect(result).toContain("- item A");
expect(result).toContain("- item B");
});
it("preserves task lists", () => {
const input = "@echobot\n- [ ] todo\n- [x] done";
const result = normalizeMention(input, "echobot");
expect(result).toContain("- [ ] todo");
expect(result).toContain("- [x] done");
});
it("handles mention in middle of text", () => {
const input = "hey @echobot check this\nout";
const result = normalizeMention(input, "echobot");
expect(result).toBe("hey check this\nout");
});
it("preserves leading indentation for nested lists", () => {
const input = "@echobot\n- item\n - nested\n - deep";
const result = normalizeMention(input, "echobot");
expect(result).toContain(" - nested");
expect(result).toContain(" - deep");
});
it("preserves first-line indentation for nested list items", () => {
const input = "@echobot\n - nested\n - deep";
const result = normalizeMention(input, "echobot");
expect(result).toBe(" - nested\n - deep");
});
it("preserves indented code blocks", () => {
const input = "@echobot\ntext\n code line 1\n code line 2";
const result = normalizeMention(input, "echobot");
expect(result).toContain(" code line 1");
expect(result).toContain(" code line 2");
});
it("preserves first-line indentation for indented code blocks", () => {
const input = "@echobot\n code line 1\n code line 2";
const result = normalizeMention(input, "echobot");
expect(result).toBe(" code line 1\n code line 2");
});
});

View File

@@ -70,3 +70,38 @@ export function resolveThreadSessionKeys(params: {
normalizeThreadId: (threadId) => threadId,
});
}
/**
* Strip bot mention from message text while preserving newlines and
* block-level Markdown formatting (headings, lists, blockquotes).
*/
export function normalizeMention(text: string, mention: string | undefined): string {
if (!mention) {
return text.trim();
}
const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const hasMentionRe = new RegExp(`@${escaped}\\b`, "i");
const leadingMentionRe = new RegExp(`^([\\t ]*)@${escaped}\\b[\\t ]*`, "i");
const trailingMentionRe = new RegExp(`[\\t ]*@${escaped}\\b[\\t ]*$`, "i");
const normalizedLines = text.split("\n").map((line) => {
const hadMention = hasMentionRe.test(line);
const normalizedLine = line
.replace(leadingMentionRe, "$1")
.replace(trailingMentionRe, "")
.replace(new RegExp(`@${escaped}\\b`, "gi"), "")
.replace(/(\S)[ \t]{2,}/g, "$1 ");
return {
text: normalizedLine,
mentionOnlyBlank: hadMention && normalizedLine.trim() === "",
};
});
while (normalizedLines[0]?.mentionOnlyBlank) {
normalizedLines.shift();
}
while (normalizedLines.at(-1)?.text.trim() === "") {
normalizedLines.pop();
}
return normalizedLines.map((line) => line.text).join("\n");
}

View File

@@ -70,6 +70,7 @@ import {
import {
createDedupeCache,
formatInboundFromLabel,
normalizeMention,
resolveThreadSessionKeys,
} from "./monitor-helpers.js";
import { resolveOncharPrefixes, stripOncharPrefix } from "./monitor-onchar.js";
@@ -143,15 +144,6 @@ function resolveRuntime(opts: MonitorMattermostOpts): RuntimeEnv {
);
}
function normalizeMention(text: string, mention: string | undefined): string {
if (!mention) {
return text.trim();
}
const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(`@${escaped}\\b`, "gi");
return text.replace(re, " ").replace(/\s+/g, " ").trim();
}
function isSystemPost(post: MattermostPost): boolean {
const type = post.type?.trim();
return Boolean(type);