diff --git a/.changeset/cleanbody-multiline-tags.md b/.changeset/cleanbody-multiline-tags.md
new file mode 100644
index 0000000..c6f11f1
--- /dev/null
+++ b/.changeset/cleanbody-multiline-tags.md
@@ -0,0 +1,7 @@
+---
+"@dualmark/core": patch
+---
+
+Fix `cleanBody` leaking raw tags when a tag spans multiple lines.
+
+The HTML-tag replacement (e.g. `…` to `**…**`, and any custom `htmlTagReplacements`) used a regex without the dotAll flag, so `.` never matched newlines. A tag whose content spanned more than one line was left untouched, leaking raw markup into the cleaned markdown that AI clients read. The regex now uses the `s` flag while keeping the lazy match, so multi-line tags are converted and adjacent tags are not merged.
diff --git a/packages/core/src/text.ts b/packages/core/src/text.ts
index 54f277d..3cfe86b 100644
--- a/packages/core/src/text.ts
+++ b/packages/core/src/text.ts
@@ -70,7 +70,11 @@ export function cleanBody(body: string, opts: CleanBodyOptions = {}): string {
let out = stripImg ? stripImages(body) : body;
for (const [tag, marker] of Object.entries(replacements)) {
- const re = new RegExp(`<${tag}>(.*?)<\\/${tag}>`, "g");
+ // `s` (dotAll) so a tag whose content spans multiple lines is still
+ // replaced; without it a multi-line `…` leaks its raw markup
+ // into the cleaned markdown. `.*?` stays lazy, so it still stops at the
+ // first closing tag.
+ const re = new RegExp(`<${tag}>(.*?)<\\/${tag}>`, "gs");
out = out.replace(re, `${marker}$1${marker}`);
}
diff --git a/packages/core/test/text.test.ts b/packages/core/test/text.test.ts
index e4bbbb0..f2b1574 100644
--- a/packages/core/test/text.test.ts
+++ b/packages/core/test/text.test.ts
@@ -86,6 +86,18 @@ describe("cleanBody", () => {
expect(cleanBody("see this")).toBe("see **this**");
});
+ it("converts a tag whose content spans multiple lines", () => {
+ expect(cleanBody("see multi\nline end")).toBe(
+ "see **multi\nline** end",
+ );
+ });
+
+ it("keeps tag replacement lazy across multiple tags", () => {
+ expect(cleanBody("a mid b", { htmlTagReplacements: { Em: "*" } })).toBe(
+ "*a* mid *b*",
+ );
+ });
+
it("converts
to newline", () => {
expect(cleanBody("line1
line2
line3")).toBe("line1\nline2\nline3");
});