diff --git a/src/app/utils/kumaStatus.test.ts b/src/app/utils/kumaStatus.test.ts
index ad08f850c..93088a411 100644
--- a/src/app/utils/kumaStatus.test.ts
+++ b/src/app/utils/kumaStatus.test.ts
@@ -1,6 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
+ plain,
DEFAULT_GROUPS,
kumaBeatTime,
kumaUrls,
@@ -260,3 +261,27 @@ test('a dismissed calls-down banner lets the next one through, never hides serve
'server-down',
);
});
+
+test('plain: strips Markdown syntax only, keeps ordinary #, _, > and * (#308)', () => {
+ assert.equal(
+ plain('join #lotus-general or #announcements:lotusguild.org'),
+ 'join #lotus-general or #announcements:lotusguild.org',
+ );
+ assert.equal(
+ plain('ping @some_user about snake_case and C#'),
+ 'ping @some_user about snake_case and C#',
+ );
+ assert.equal(
+ plain('calls -> fixed, > 5 people, 5 * 3 = 15'),
+ 'calls -> fixed, > 5 people, 5 * 3 = 15',
+ );
+ assert.equal(
+ plain('# Heading\n* item one\n- item two\n> quoted'),
+ 'Heading\nitem one\nitem two\nquoted',
+ );
+ assert.equal(plain('**bold**, *em*, _em_, `/sfu/get`'), 'bold, em, em, /sfu/get');
+ assert.equal(
+ plain('Read the guide
[docs](https://x.y)'),
+ 'Read the guide docs',
+ );
+});
diff --git a/src/app/utils/kumaStatus.ts b/src/app/utils/kumaStatus.ts
index 4b4b71837..7e23274c2 100644
--- a/src/app/utils/kumaStatus.ts
+++ b/src/app/utils/kumaStatus.ts
@@ -240,12 +240,25 @@ const incidentTone = (style: string): BannerTone => {
};
/** Markdown-ish incident text as plain text for the Details line. */
-const plain = (s: string): string =>
+/**
+ * [Gitea #308] Markdown → plain text for the Details line. Strips only the
+ * syntax: removing every `*_\`#>` also broke "#lotus-general", "snake_case",
+ * "->" and "5 * 3". HTML tags are dropped (React escapes the text anyway).
+ */
+export const plain = (s: string): string =>
s
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
- .replace(/[*_`#>]/g, '')
- .replace(/\s+\n/g, '\n')
+ .replace(/<\/?[a-z][^>]*>/gi, '')
+ .replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, '')
+ .replace(/^[ \t]*>[ \t]?/gm, '')
+ .replace(/^[ \t]*[*+-][ \t]+/gm, '')
+ .replace(/`([^`\n]+)`/g, '$1')
+ .replace(/\*\*(?=\S)([^*\n]*?\S)\*\*/g, '$1')
+ .replace(/__(?=\S)([^_\n]*?\S)__/g, '$1')
+ .replace(/(^|[^\w*])\*(?=\S)([^*\n]*?\S)\*(?![\w*])/g, '$1$2')
+ .replace(/(^|[^\w])_(?=\S)([^_\n]*?\S)_(?!\w)/g, '$1$2')
+ .replace(/[ \t]+\n/g, '\n')
.trim();
/**