11import { i18n } from '../i18n.js'
22import { search as performFullTextSearch , isIndexReady } from '../utils/search-index.js'
33
4+ function escapeHtml ( value ) {
5+ return String ( value ?? '' )
6+ . replaceAll ( '&' , '&' )
7+ . replaceAll ( '<' , '<' )
8+ . replaceAll ( '>' , '>' )
9+ . replaceAll ( '"' , '"' )
10+ . replaceAll ( "'" , ''' )
11+ }
12+
413/**
514 * Category color palette (matching previous categories)
615 */
@@ -65,10 +74,10 @@ function renderCategorySection(category, allAnchors) {
6574 const categoryName = i18n . t ( `categories.${ category . id } ` ) || category . name
6675
6776 return `
68- <section class="category-section" data-category="${ category . id } ">
77+ <section class="category-section" data-category="${ escapeHtml ( category . id ) } ">
6978 <h2 class="category-heading">
7079 <span class="category-icon" style="background-color: ${ color } ">${ icon } </span>
71- <span data-i18n="categories.${ category . id } ">${ categoryName } </span>
80+ <span data-i18n="categories.${ escapeHtml ( category . id ) } ">${ escapeHtml ( categoryName ) } </span>
7281 </h2>
7382
7483 <div class="anchor-cards-grid">
@@ -83,44 +92,43 @@ function renderCategorySection(category, allAnchors) {
8392 */
8493function renderAnchorCard ( anchor , categoryColor ) {
8594 const rolesCount = anchor . roles ? anchor . roles . length : 0
86- const tagsPreview = anchor . tags ? anchor . tags . slice ( 0 , 3 ) . join ( ', ' ) : ''
8795 const githubEditUrl = `https://github.com/LLM-Coding/Semantic-Anchors/edit/main/docs/anchors/${ anchor . id } .adoc`
8896 const roleText = rolesCount === 1 ? i18n . t ( 'card.roles' ) : i18n . t ( 'card.rolesPlural' )
8997 const tagsText = i18n . t ( 'card.tags' )
9098 const editTitle = i18n . t ( 'card.edit' )
9199 const copyLinkTitle = i18n . t ( 'card.copyLink' )
100+ const safeId = escapeHtml ( anchor . id )
92101
93102 return `
94103 <article
95104 class="anchor-card"
96- data-anchor="${ anchor . id } "
97- data-roles="${ anchor . roles ? anchor . roles . join ( ',' ) : '' } "
98- data-tags="${ anchor . tags ? anchor . tags . join ( ',' ) : '' } "
105+ data-anchor="${ safeId } "
106+ data-roles="${ escapeHtml ( anchor . roles ? anchor . roles . join ( ',' ) : '' ) } "
107+ data-tags="${ escapeHtml ( anchor . tags ? anchor . tags . join ( ',' ) : '' ) } "
99108 tabindex="0"
100109 role="button"
101- aria-label="${ i18n . t ( 'card.openDetails' ) . replace ( '{title}' , anchor . title ) } "
110+ aria-label="${ escapeHtml ( i18n . t ( 'card.openDetails' ) . replace ( '{title}' , anchor . title ) ) } "
102111 >
103112 <div class="anchor-card-header">
104- <h3 class="anchor-card-title">${ anchor . title } </h3>
113+ <h3 class="anchor-card-title">${ escapeHtml ( anchor . title ) } </h3>
105114 <div class="flex gap-1">
106115 <button
107116 class="anchor-copy-link-btn"
108- title="${ copyLinkTitle } "
109- onclick="event.stopPropagation(); window.copyAnchorLink(' ${ anchor . id } ') "
117+ title="${ escapeHtml ( copyLinkTitle ) } "
118+ data-copy-link=" ${ safeId } "
110119 data-i18n-title="card.copyLink"
111- aria-label="${ copyLinkTitle } "
120+ aria-label="${ escapeHtml ( copyLinkTitle ) } "
112121 >
113122 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114123 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
115124 </svg>
116125 </button>
117126 <a
118- href="${ githubEditUrl } "
127+ href="${ escapeHtml ( githubEditUrl ) } "
119128 target="_blank"
120129 rel="noopener noreferrer"
121130 class="anchor-edit-btn"
122- title="${ editTitle } "
123- onclick="event.stopPropagation()"
131+ title="${ escapeHtml ( editTitle ) } "
124132 data-i18n-title="card.edit"
125133 >
126134 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -130,8 +138,8 @@ function renderAnchorCard(anchor, categoryColor) {
130138 </div>
131139 </div>
132140
133- ${ anchor . proponents ? `
134- <p class="anchor-card-proponents">${ anchor . proponents . slice ( 0 , 2 ) . join ( ', ' ) } </p>
141+ ${ anchor . proponents && anchor . proponents . length > 0 ? `
142+ <p class="anchor-card-proponents">${ escapeHtml ( anchor . proponents . slice ( 0 , 2 ) . join ( ', ' ) ) } </p>
135143 ` : '' }
136144
137145 <div class="anchor-card-meta">
@@ -180,6 +188,21 @@ export function initCardGrid() {
180188
181189 // Click handler using event delegation
182190 clickHandler = ( e ) => {
191+ if ( e . target . closest ( '.anchor-copy-link-btn' ) ) {
192+ const button = e . target . closest ( '.anchor-copy-link-btn' )
193+ const anchorId = button ?. dataset . copyLink
194+ if ( anchorId ) {
195+ e . stopPropagation ( )
196+ window . copyAnchorLink ( anchorId )
197+ }
198+ return
199+ }
200+
201+ if ( e . target . closest ( '.anchor-edit-btn' ) ) {
202+ e . stopPropagation ( )
203+ return
204+ }
205+
183206 const card = e . target . closest ( '.anchor-card' )
184207 if ( card ) {
185208 const anchorId = card . dataset . anchor
0 commit comments