Skip to content

Commit fcf6ea5

Browse files
authored
Merge branch 'add-discogs-support' into copilot/sub-pr-180-another-one
2 parents a6973f0 + 22ff3cc commit fcf6ea5

File tree

3 files changed

+87
-48
lines changed

3 files changed

+87
-48
lines changed

src/components/artist/ArtistInfo.vue

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
5656
import ArtistSidebar from "@/components/artist/ArtistSidebar.vue";
5757
import CustomSelect, { type SelectOption } from "@/components/ui/CustomSelect.vue";
5858
import LanguageSelect, { type LanguageOption } from "@/components/ui/LanguageSelect.vue";
59+
import { parseDiscogsMarkup } from "@/helpers/discogs";
5960
import { useArtist } from "@/views/artist/ArtistStore";
6061
6162
interface WikipediaSection {
@@ -137,54 +138,6 @@ onBeforeUnmount(() => {
137138
}
138139
});
139140
140-
const DISCOGS_BASE_URL = "https://www.discogs.com";
141-
const LINK_ATTRS = 'target="_blank" rel="noopener noreferrer" class="discogs-link"';
142-
143-
const DISCOGS_ENTITIES: Record<string, { path: string; searchType: string; text: string }> = {
144-
a: { path: "artist", searchType: "artist", text: "artist" },
145-
l: { path: "label", searchType: "label", text: "label" },
146-
m: { path: "master", searchType: "master", text: "release" },
147-
r: { path: "release", searchType: "release", text: "release" },
148-
};
149-
150-
const TEXT_FORMATS: Array<{ pattern: RegExp; replacement: string }> = [
151-
{ pattern: /\[b\](.*?)\[\/b\]/gi, replacement: "<strong>$1</strong>" },
152-
{ pattern: /\[i\](.*?)\[\/i\]/gi, replacement: "<em>$1</em>" },
153-
{ pattern: /\[u\](.*?)\[\/u\]/gi, replacement: "<u>$1</u>" },
154-
];
155-
156-
function createDiscogsLinkById(type: string, id: string): string {
157-
const entity = DISCOGS_ENTITIES[type.toLowerCase()];
158-
if (!entity) return `[${type}${id}]`;
159-
return `<a href="${DISCOGS_BASE_URL}/${entity.path}/${id}" ${LINK_ATTRS}>${entity.text}</a>`;
160-
}
161-
162-
function createDiscogsSearchLink(type: string, name: string): string {
163-
const entity = DISCOGS_ENTITIES[type.toLowerCase()];
164-
if (!entity) return `[${type}=${name}]`;
165-
return `<a href="${DISCOGS_BASE_URL}/search/?q=${encodeURIComponent(name)}&type=${entity.searchType}" ${LINK_ATTRS}>${name}</a>`;
166-
}
167-
168-
function parseDiscogsMarkup(text: string): string {
169-
let result = text;
170-
result = result.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
171-
172-
for (const { pattern, replacement } of TEXT_FORMATS) {
173-
result = result.replace(pattern, replacement);
174-
}
175-
176-
result = result.replace(/\[([almr])=?(\d+)\]/gi, (_, type, id) => createDiscogsLinkById(type, id));
177-
result = result.replace(/\[([al])=([^\]]+)\]/gi, (_, type, name) => createDiscogsSearchLink(type, name));
178-
result = result.replace(
179-
/\[url=(.*?)\](.*?)\[\/url\]/gi,
180-
'<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>',
181-
);
182-
result = result.replace(/\[url\](.*?)\[\/url\]/gi, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
183-
result = result.replace(/\n/g, "<br>");
184-
185-
return result;
186-
}
187-
188141
const formattedDiscogsProfile = computed(() => {
189142
if (!artistStore.discogsArtist?.profile) return "";
190143
return parseDiscogsMarkup(artistStore.discogsArtist.profile);

src/components/artist/ArtistProfile.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import DOMPurify from "dompurify";
1515
import { computed } from "vue";
1616
17+
import { parseDiscogsMarkup } from "@/helpers/discogs";
1718
import { useArtist } from "@/views/artist/ArtistStore";
1819
1920
const artistStore = useArtist();

src/helpers/discogs.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@ const DISCOGS_API_URL = "https://api.discogs.com/";
99
const DISCOGS_TOKEN = import.meta.env.VITE_DISCOGS_TOKEN || "";
1010
const USER_AGENT = "Beardify/1.0.0 (https://github.com/BeardedBear/beardify)";
1111

12+
/**
13+
* Discogs markup parsing configuration
14+
*/
15+
const DISCOGS_BASE_URL = "https://www.discogs.com";
16+
const LINK_ATTRS = 'target="_blank" rel="noopener noreferrer" class="discogs-link"';
17+
18+
/**
19+
* Discogs entity types configuration
20+
* Maps entity type letter to URL path and display text
21+
*/
22+
const DISCOGS_ENTITIES: Record<string, { path: string; searchType: string; text: string }> = {
23+
a: { path: "artist", searchType: "artist", text: "artist" },
24+
l: { path: "label", searchType: "label", text: "label" },
25+
m: { path: "master", searchType: "master", text: "release" },
26+
r: { path: "release", searchType: "release", text: "release" },
27+
};
28+
29+
/**
30+
* Text formatting tags configuration
31+
*/
32+
const TEXT_FORMATS: Array<{ pattern: RegExp; replacement: string }> = [
33+
{ pattern: /\[b\](.*?)\[\/b\]/gi, replacement: "<strong>$1</strong>" },
34+
{ pattern: /\[i\](.*?)\[\/i\]/gi, replacement: "<em>$1</em>" },
35+
{ pattern: /\[u\](.*?)\[\/u\]/gi, replacement: "<u>$1</u>" },
36+
];
37+
1238
/**
1339
* Creates a Discogs API client instance
1440
*/
@@ -46,3 +72,62 @@ export async function getDiscogsArtist(discogsId: string): Promise<DiscogsArtist
4672
return null;
4773
}
4874
}
75+
76+
/**
77+
* Parse Discogs markup and convert to HTML
78+
* Supported tags:
79+
* - [i], [b], [u] -> text formatting
80+
* - [a], [l], [r], [m] -> Discogs entity links (by ID or name)
81+
* - [url] -> external links
82+
* @param text - The Discogs markup text to parse
83+
* @returns HTML string with converted markup
84+
*/
85+
export function parseDiscogsMarkup(text: string): string {
86+
let result = text;
87+
88+
// Escape HTML to prevent XSS
89+
result = result.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
90+
91+
// Apply text formatting
92+
for (const { pattern, replacement } of TEXT_FORMATS) {
93+
result = result.replace(pattern, replacement);
94+
}
95+
96+
// [x123456] or [x=123456] -> link to Discogs entity by ID
97+
result = result.replace(/\[([almr])=?(\d+)\]/gi, (_, type, id) => createDiscogsLinkById(type, id));
98+
99+
// [x=Name] -> link to Discogs search by name (non-numeric)
100+
result = result.replace(/\[([al])=([^\]]+)\]/gi, (_, type, name) => createDiscogsSearchLink(type, name));
101+
102+
// [url=http://...]text[/url] -> <a href="...">text</a>
103+
result = result.replace(
104+
/\[url=(.*?)\](.*?)\[\/url\]/gi,
105+
'<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>',
106+
);
107+
108+
// [url]http://...[/url] -> <a href="...">...</a>
109+
result = result.replace(/\[url\](.*?)\[\/url\]/gi, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
110+
111+
// Convert newlines to <br> tags
112+
result = result.replace(/\n/g, "<br>");
113+
114+
return result;
115+
}
116+
117+
/**
118+
* Create a Discogs link by ID
119+
*/
120+
function createDiscogsLinkById(type: string, id: string): string {
121+
const entity = DISCOGS_ENTITIES[type.toLowerCase()];
122+
if (!entity) return `[${type}${id}]`;
123+
return `<a href="${DISCOGS_BASE_URL}/${entity.path}/${id}" ${LINK_ATTRS}>${entity.text}</a>`;
124+
}
125+
126+
/**
127+
* Create a Discogs search link by name
128+
*/
129+
function createDiscogsSearchLink(type: string, name: string): string {
130+
const entity = DISCOGS_ENTITIES[type.toLowerCase()];
131+
if (!entity) return `[${type}=${name}]`;
132+
return `<a href="${DISCOGS_BASE_URL}/search/?q=${encodeURIComponent(name)}&type=${entity.searchType}" ${LINK_ATTRS}>${name}</a>`;
133+
}

0 commit comments

Comments
 (0)