fix: link only the repo head in message titles, keep the subject plain

This commit is contained in:
RhenCloud 2026-08-13 19:18:55 +08:00
parent 84d8a5384d
commit dfb65ef70d
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
7 changed files with 75 additions and 15 deletions

View file

@ -1,4 +1,5 @@
import type { FormattedMessage, NeutralActionStyle, NeutralMessage } from "../../types";
import { repoUrlFromMessage, splitMessageTitle } from "../../formatters/helpers";
function toStyle(style: NeutralActionStyle): number {
switch (style) {
@ -15,14 +16,26 @@ export function renderNeutralMessage(message: NeutralMessage): FormattedMessage
const content = message.mentionRoleIds?.length
? message.mentionRoleIds.map((id) => `<@&${id}>`).join(" ")
: undefined;
// Discord embed titles can only be linked as a whole, so only the repo head
// goes into the title (linked to the repository); the `: subject` text is
// rendered as the first line of the description, unlinked.
const { head, subject } = splitMessageTitle(message.title);
const repoUrl = repoUrlFromMessage(message.url);
const description = subject
? message.description
? `${subject}\n${message.description}`
: subject
: message.description;
return {
content,
embeds: [
{
title: message.title,
url: message.url,
title: head,
url: subject ? repoUrl : message.url,
color: message.color,
description: message.description,
description,
author: message.author
? {
name: message.author.name,

View file

@ -1,4 +1,5 @@
import type { NeutralMessage } from "../../types";
import { repoUrlFromMessage, splitMessageTitle } from "../../formatters/helpers";
function esc(s: string): string {
return s
@ -32,10 +33,16 @@ function formatTimestamp(ts?: string): string {
export function renderNeutralMessage(message: NeutralMessage): string {
const parts: string[] = [];
const title = message.url
? `<a href="${esc(message.url)}">${mdToHtml(message.title)}</a>`
: mdToHtml(message.title);
parts.push(`<b>${title}</b>`);
// HTML allows partial links, so keep the `{repo}{#number}: {subject}` line
// intact with only the repo head linked (the subject stays plain text).
const { head, subject } = splitMessageTitle(message.title);
const repoUrl = repoUrlFromMessage(message.url);
const title = subject
? `<b>${repoUrl ? `<a href="${esc(repoUrl)}">${mdToHtml(head)}</a>` : mdToHtml(head)}: ${mdToHtml(subject)}</b>`
: message.url
? `<b><a href="${esc(message.url)}">${mdToHtml(message.title)}</a></b>`
: `<b>${mdToHtml(message.title)}</b>`;
parts.push(title);
if (message.author) {
const name = mdToHtml(message.author.name);

View file

@ -12,6 +12,42 @@ export function makeT(tr?: Translations): T {
return (key, params) => translate(key, params, undefined, tr);
}
export interface TitleParts {
/** `{repo}` or `{repo}#{number}` — what embed titles link (the repo). */
head: string;
/** Text after `": "`, undefined when the title has no `: ` separator. */
subject?: string;
}
/**
* Split a `{repo}{#number}: {subject}` title into the repo head and the
* subject. Discord embed titles can only link as a whole, so drivers render
* the head as the linked title and the subject as plain text (description /
* unlinked remainder) to avoid hyperlinking the whole title.
*/
export function splitMessageTitle(title: string): TitleParts {
const idx = title.indexOf(": ");
if (idx <= 0) return { head: title };
return { head: title.slice(0, idx), subject: title.slice(idx + 2) };
}
/**
* Repository URL derived from an event URL (origin + owner + repo). Returns
* undefined when there is no URL to derive from callers then render the
* title without a link.
*/
export function repoUrlFromMessage(url: string | undefined): string | undefined {
if (!url) return undefined;
try {
const parsed = new URL(url);
const segments = parsed.pathname.split("/").filter(Boolean);
if (segments.length >= 2) return `${parsed.origin}/${segments[0]}/${segments[1]}`;
} catch {
// not a parseable URL — no link
}
return undefined;
}
export function buildMessage(
partial: Omit<Partial<NeutralMessage>, "title"> & { title: string },
t: T,