class ProjectCard extends HTMLElement { static get observedAttributes() { return ['image', 'image-alt', 'title', 'description']; } constructor() { super(); this.attachShadow({ mode: 'open' }); this._rendered = false; } connectedCallback() { this._render(); } attributeChangedCallback() { if (this._rendered) { this._render(); } } _render() { this._rendered = true; const alt = this.getAttribute('image-alt') || ''; const title = this.getAttribute('title') || ''; const description = this.getAttribute('description') || ''; this.shadowRoot.innerHTML = `
${this._escapeAttribute(alt)}

`; this.shadowRoot.querySelector('.intro-text').textContent = title; this._renderDescription(description); } _renderDescription(description) { const slot = this.shadowRoot.querySelector('slot'); slot.textContent = ''; const linkPattern = /\[\[([^|\]]+)\|([^\]]+)\]\]/g; let lastIndex = 0; let match; while ((match = linkPattern.exec(description)) !== null) { if (match.index > lastIndex) { slot.appendChild(document.createTextNode(description.slice(lastIndex, match.index))); } const link = document.createElement('a'); link.href = match[2]; link.textContent = match[1]; if (match[2].startsWith('http')) { link.target = '_blank'; link.rel = 'noopener'; } slot.appendChild(link); lastIndex = match.index + match[0].length; if (linkPattern.lastIndex === match.index) { linkPattern.lastIndex++; } } if (lastIndex < description.length) { slot.appendChild(document.createTextNode(description.slice(lastIndex))); } } _escapeAttribute(value) { return value .replace(/&/g, '&') .replace(/"/g, '"') .replace(/