1(function (root, factory) { 2 if (typeof module === 'object' && module.exports) { 3 module.exports = factory(); 4 } else { 5 root.sereSidebar = factory(); 6 } 7})(typeof window !== 'undefined' ? window : globalThis, function () { 8 const COLLECTION_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; 9 const ALLOWED_VIEWS = new Set(['home-view', 'station-list-view']); 10 const ALLOWED_TYPES = new Set(['view', 'category', 'collection', 'section', 'divider', 'action']); 11 const ALLOWED_ACTIONS = new Set(['about', 'settings', 'help']); 12 const DEFAULT_UTILITY_ITEMS = [ 13 { type: 'action', label: 'About SERE+', icon: 'info-circle', action: 'about' }, 14 { type: 'action', label: 'Settings', icon: 'cog', action: 'settings' }, 15 { type: 'action', label: 'Help & Support', icon: 'question-circle', action: 'help' } 16 ]; 17 18 function defaultSidebarItems(collections) { 19 const items = [ 20 { type: 'view', label: 'Home', icon: 'home', view: 'home-view' }, 21 { type: 'view', label: 'All Stations', icon: 'broadcast-tower', view: 'station-list-view' }, 22 { type: 'section', label: 'Categories', icon: '' }, 23 { type: 'category', label: 'Music', icon: 'music', filter: 'Music' }, 24 { type: 'category', label: 'Talk', icon: 'microphone', filter: 'Talk' }, 25 { type: 'category', label: 'News', icon: 'newspaper', filter: 'News' }, 26 { type: 'category', label: 'Sports', icon: 'futbol', filter: 'Sports' } 27 ]; 28 29 if (Array.isArray(collections)) { 30 collections.forEach(collection => { 31 if (!collection || !COLLECTION_ID_PATTERN.test(String(collection.id || ''))) { 32 return; 33 } 34 35 const title = String(collection.title || '').trim(); 36 if (title === '') { 37 return; 38 } 39 40 items.push({ 41 type: 'collection', 42 label: title, 43 icon: 'users', 44 collection_id: String(collection.id) 45 }); 46 }); 47 } 48 49 items.push({ type: 'divider', label: '', icon: '' }, ...DEFAULT_UTILITY_ITEMS); 50 51 return items; 52 } 53 54 function normalizeSidebarItems(items, collections) { 55 const availableCollections = new Set( 56 Array.isArray(collections) 57 ? collections 58 .filter(collection => collection && COLLECTION_ID_PATTERN.test(String(collection.id || ''))) 59 .map(collection => String(collection.id)) 60 : [] 61 ); 62 const source = Array.isArray(items) && items.length > 0 63 ? items 64 : defaultSidebarItems(collections); 65 66 const normalized = source 67 .filter(item => item && item.enabled !== false) 68 .map((item, index) => normalizeSidebarItem(item, index, availableCollections)) 69 .filter(Boolean) 70 .sort((left, right) => left.sort_order - right.sort_order); 71 72 if (normalized.length === 0) { 73 return defaultSidebarItems(collections); 74 } 75 76 return appendMissingUtilityItems(normalized, source); 77 } 78 79 function normalizeSidebarItem(item, index, availableCollections) { 80 const type = String(item.type || '').trim().toLowerCase(); 81 if (!ALLOWED_TYPES.has(type)) { 82 return null; 83 } 84 85 const sortOrder = Number.isFinite(Number(item.sort_order)) 86 ? Number(item.sort_order) 87 : index * 10; 88 89 if (type === 'divider') { 90 return { type, label: '', icon: '', sort_order: sortOrder }; 91 } 92 93 const label = String(item.label || '').trim(); 94 if (label === '') { 95 return null; 96 } 97 98 const base = { 99 type, 100 label, 101 icon: cleanIcon(item.icon), 102 sort_order: sortOrder 103 }; 104 105 if (type === 'section') { 106 return base; 107 } 108 109 if (type === 'view') { 110 const view = String(item.view || '').trim(); 111 return ALLOWED_VIEWS.has(view) ? { ...base, view } : null; 112 } 113 114 if (type === 'category') { 115 const filter = String(item.filter || label).trim(); 116 return filter !== '' ? { ...base, filter } : null; 117 } 118 119 if (type === 'action') { 120 const action = String(item.action || '').trim().toLowerCase(); 121 return ALLOWED_ACTIONS.has(action) ? { ...base, action } : null; 122 } 123 124 const collectionId = String(item.collection_id || '').trim(); 125 if (!availableCollections.has(collectionId)) { 126 return null; 127 } 128 129 return COLLECTION_ID_PATTERN.test(collectionId) 130 ? { ...base, collection_id: collectionId } 131 : null; 132 } 133 134 function cleanIcon(icon) {
135 const value = String(icon || '').trim().toLowerCase(); 136 return /^[a-z0-9-]{0,40}$/.test(value) ? value : ''; 137 } 138 139 function appendMissingUtilityItems(items, source) { 140 const presentActions = new Set( 141 items 142 .filter(item => item.type === 'action') 143 .map(item => item.action) 144 ); 145 const disabledActions = disabledUtilityActions(source); 146 const missingActions = DEFAULT_UTILITY_ITEMS 147 .filter(item => !presentActions.has(item.action) && !disabledActions.has(item.action)); 148 149 if (missingActions.length === 0) { 150 return items; 151 } 152 153 const restored = [...items]; 154 let sortOrder = restored.reduce((max, item) => Math.max(max, Number(item.sort_order) || 0), 0) + 10; 155 156 if (restored.length > 0 && restored[restored.length - 1].type !== 'divider') { 157 restored.push({ type: 'divider', label: '', icon: '', sort_order: sortOrder }); 158 sortOrder += 10; 159 } 160 161 missingActions.forEach(item => { 162 restored.push({ ...item, sort_order: sortOrder }); 163 sortOrder += 10; 164 }); 165 166 return restored; 167 } 168 169 function disabledUtilityActions(source) { 170 if (!Array.isArray(source)) { 171 return new Set(); 172 } 173 174 return new Set( 175 source 176 .filter(item => item && item.enabled === false && String(item.type || '').toLowerCase() === 'action') 177 .map(item => String(item.action || '').trim().toLowerCase()) 178 .filter(action => ALLOWED_ACTIONS.has(action)) 179 ); 180 } 181 182 function groupSidebarItems(items) { 183 const primary = ['home-view', 'station-list-view'].map(view => 184 items.find(item => item.type === 'view' && item.view === view) 185 || defaultSidebarItems([]).find(item => item.type === 'view' && item.view === view) 186 ); 187 const heading = items.find(item => item.type === 'section') 188 || { type: 'section', label: 'Categories', icon: '' }; 189 const categories = items.filter(item => item.type !== 'view' && item.type !== 'action' && item !== heading); 190 while (categories[0]?.type === 'divider') categories.shift(); 191 while (categories[categories.length - 1]?.type === 'divider') categories.pop(); 192 return { primary, heading, categories, utilities: items.filter(item => item.type === 'action') }; 193 } 194 195 return { 196 groupSidebarItems, 197 defaultSidebarItems, 198 normalizeSidebarItems 199 }; 200});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.