feat: rich command templates with public links + media text-first flow
- Command templates now match notification template style: type icons,
linked filenames via album shared links, location, favorite status
- Media mode sends text message first, then media as reply (was media-only)
- Search/find/person/place resolve asset public URLs from tracked albums'
shared links (share/{key}/photos/{id})
- Albums/summary commands include album public_url in context
- Enriched command template preview sample context with public_url, city,
country, is_favorite
- Extract sanitizePreview to shared lib/sanitize.ts
- Command template preview now renders HTML links (was raw text)
- Global provider filter moved above search in sidebar
- CLAUDE.md: template consistency + context variable sync rules
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Sanitize HTML preview output — allows only safe Telegram-compatible tags.
|
||||
* Used by notification and command template preview renderers.
|
||||
*/
|
||||
|
||||
const ALLOWED_TAGS = new Set(['B', 'I', 'CODE', 'PRE', 'A', 'BR']);
|
||||
|
||||
function walkNodes(parent: Node, target: Node): void {
|
||||
for (const node of Array.from(parent.childNodes)) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
target.appendChild(document.createTextNode(node.textContent || ''));
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as Element;
|
||||
if (ALLOWED_TAGS.has(el.tagName)) {
|
||||
const safe = document.createElement(el.tagName);
|
||||
if (el.tagName === 'A') {
|
||||
const href = el.getAttribute('href') || '';
|
||||
if (/^https?:\/\//i.test(href)) {
|
||||
safe.setAttribute('href', href);
|
||||
safe.setAttribute('target', '_blank');
|
||||
safe.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
}
|
||||
walkNodes(el, safe);
|
||||
target.appendChild(safe);
|
||||
} else {
|
||||
walkNodes(el, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizePreview(html: string): string {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const fragment = document.createDocumentFragment();
|
||||
walkNodes(doc.body, fragment);
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.appendChild(fragment);
|
||||
return wrapper.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user