PageSourceSearch

https://beerbottle.ru/wp-content/plugins/brewery-map-plugin/assets/js/bmj-frontend-v360.js?x13750

js beerbottle.ru collected 2026-10-02 10:23:23 UTC 23,216 bytes, 572 lines download raw bytes

1
2(() => {
3  const state = {
4    items: [],
5    filtered: [],
6    stats: null,
7    activeMapItem: null,
8    filters: { search: '', country: '', city: '', visited: '', rating: '', tag: '', quickVisited: [], quickCountries: [], quickMinRatings: [] }
9  };
10
11  document.addEventListener('DOMContentLoaded', () => {
12    document.querySelectorAll('[data-bmj-app]').forEach(initApp);
13  });
14
15  async function initApp(app) {
16    if (!app || !window.BMJ_DATA) return;
17    app._bmj = { app };
18    bindTabs(app);
19    bindFilterEvents(app);
20    bindResponsiveLayout(app);
21
22    try {
23      const [itemsRes, statsRes] = await Promise.all([
24        fetch(BMJ_DATA.restUrl),
25        fetch(BMJ_DATA.statsUrl)
26      ]);
27      const allItems = await itemsRes.json();
28      const stats = await statsRes.json();
29
30      const singleId = Number(app.getAttribute('data-bmj-single-id') || 0);
31      const mode = app.getAttribute('data-bmj-mode') || 'full';
32
33      app._bmj.items = singleId ? allItems.filter(i => Number(i.id) === singleId) : allItems;
34      app._bmj.stats = stats;
35      state.items = app._bmj.items;
36      state.stats = stats;
37
38      if (mode !== 'single') {
39        fillFilterOptions(app, state.items);
40      }
41      applyFilters(app);
42      if (mode !== 'single') renderStats(app, stats);
43    } catch (e) {
44      console.error('BMJ init error', e);
45    }
46  }
47
48  function bindResponsiveLayout(app) {
49    const apply = () => {
50      const width = app.clientWidth || app.offsetWidth || 0;
51      app.classList.toggle('bmj-shell--narrow', width > 0 && width < 680);
52      if (width >= 760) {
53        app.querySelectorAll('[data-bmj-panel]').forEach(panel => panel.classList.add('is-active'));
54        app.querySelectorAll('[data-bmj-tab]').forEach(button => button.classList.remove('is-active'));
55      } else if (!app.querySelector('[data-bmj-tab].is-active')) {
56        const firstTab = app.querySelector('[data-bmj-tab="map"]');
57        const mapPanel = app.querySelector('[data-bmj-panel="map"]');
58        const listPanel = app.querySelector('[data-bmj-panel="list"]');
59        if (firstTab && mapPanel && listPanel) {
60          firstTab.classList.add('is-active');
61          mapPanel.classList.add('is-active');
62          listPanel.classList.remove('is-active');
63        }
64      }
65    };
66    if ('ResizeObserver' in window) {
67      const ro = new ResizeObserver(apply);
68      ro.observe(app);
69    }
70    window.addEventListener('resize', apply);
71    apply();
72  }
73
74  function bindTabs(app) {
75    app.querySelectorAll('[data-bmj-tab]').forEach(button => {
76      button.addEventListener('click', () => {
77        const tab = button.getAttribute('data-bmj-tab');
78        app.querySelectorAll('[data-bmj-tab]').forEach(b => b.classList.toggle('is-active', b === button));
79        app.querySelectorAll('[data-bmj-panel]').forEach(panel => panel.classList.toggle('is-active', panel.getAttribute('data-bmj-panel') === tab));
80      });
81    });
82  }
83
84  function bindFilterEvents(app) {
85    const filters = app.querySelector('[data-bmj-filters]');
86    if (!filters) return;
87    filters.querySelectorAll('[data-filter]').forEach(el => {
88      el.addEventListener('input', (e) => onFilterChange(app, e));
89      el.addEventListener('change', (e) => onFilterChange(app, e));
90    });
91    const reset = filters.querySelector('[data-bmj-reset]');
92    if (reset) {
93      reset.addEventListener('click', () => {
94        filters.querySelectorAll('[data-filter]').forEach(el => el.value = '');
95        app._bmj.filters = { search: '', country: '', city: '', visited: '', rating: '', tag: '', quickVisited: [], quickCountries: [], quickMinRatings: [] };
96        applyFilters(app);
97      });
98    }
99  }
100
101  function onFilterChange(app, e) {
102    if (!app._bmj.filters) app._bmj.filters = { search: '', country: '', city: '', visited: '', rating: '', tag: '', quickVisited: [], quickCountries: [], quickMinRatings: [] };
103    app._bmj.filters[e.target.getAttribute('data-filter')] = e.target.value.trim();
104    applyFilters(app);
105  }
106
107  function fillFilterOptions(app, items) {
108    fillSelect(app, 'country', uniq(items.map(i =>
108 i.country).filter(Boolean)).sort(localeSort));
109    fillSelect(app, 'city', uniq(items.map(i => i.city).filter(Boolean)).sort(localeSort));
110    fillSelect(app, 'tag', uniq(items.flatMap(i => Array.isArray(i.tags) ? i.tags : []).filter(Boolean)).sort(localeSort));
111    fillQuickCountryFilters(app, items);
112  }
113
114  function fillQuickCountryFilters(app, items) {
115    const root = app.querySelector('[data-bmj-country-boxes]');
116    if (!root) return;
117    root.querySelectorAll('label').forEach(el => el.remove());
118    uniq(items.map(i => i.country).filter(Boolean)).sort(localeSort).forEach(value => {
119      const label = document.createElement('label');
120      label.innerHTML = `<input type="checkbox" data-bmj-quick="country" value="${escapeAttr(value)}"> ${escapeHtml(value)}`;
121      label.querySelector('input').addEventListener('change', () => onQuickFilterChange(app));
122      root.append(label);
123    });
124  }
125
126  function fillSelect(app, name, values) {
127    const select = app.querySelector(`[data-filter="${name}"]`);
128    if (!select) return;
129    const first = select.querySelector('option');
130    select.innerHTML = '';
131    if (first) select.append(first);
132    values.forEach(value => {
133      const option = document.createElement('option');
134      option.value = value;
135      option.textContent = value;
136      select.append(option);
137    });
138  }
139
140  function applyFilters(app) {
141    const items = app._bmj.items || [];
142    const filters = app._bmj.filters || { search: '', country: '', city: '', visited: '', rating: '', tag: '', quickVisited: [], quickCountries: [], quickMinRatings: [] };
143
144    app._bmj.filtered = items.filter(item => {
145      const haystack = [item.title, item.country, item.city, item.subtitle, item.address, ...(item.tags || [])].join(' ').toLowerCase();
146      if (filters.search && !haystack.includes(filters.search.toLowerCase())) return false;
147      if (filters.country && item.country !== filters.country) return false;
148      if (filters.city && item.city !== filters.city) return false;
149      if (filters.visited !== '' && String(Number(item.visited)) !== filters.visited) return false;
150      if (filters.rating && Number(item.rating || 0) < Number(filters.rating)) return false;
151      if (filters.tag && !(item.tags || []).includes(filters.tag)) return false;
152      if (filters.quickVisited?.length && !filters.quickVisited.includes(String(Number(item.visited)))) return false;
153      if (filters.quickCountries?.length && !filters.quickCountries.includes(item.country)) return false;
154      if (filters.quickMinRatings?.length && !filters.quickMinRatings.some(min => Number(item.rating || 0) >= Number(min))) return false;
155      return true;
156    });
157
158    renderList(app);
159    renderMap(app);
160  }
161
162  function renderStats(app, stats) {
163    const root = app.querySelector('[data-bmj-stats]');
164    if (!root || !stats) return;
165    const cards = [
166      ['Всего', stats.total],
167      ['Посещено', stats.visited],
168      ['Не посещено', stats.notVisited],
169      ['Стран', stats.countries],
170      ['Городов', stats.cities]
171    ];
172    root.innerHTML = cards.map(([label, value]) => `
173      <div class="bmj-stat">
174        <span class="bmj-stat__label">${escapeHtml(label)}</span>
175        <span class="bmj-stat__value">${escapeHtml(String(value))}</span>
176      </div>`).join('');
177  }
178
179  function renderList(app) {
180    const root = app.querySelector('[data-bmj-list]');
181    if (!root) return;
182    const items = app._bmj.filtered || [];
183    if (!items.length) {
184      root.innerHTML = `<div class="bmj-empty">${escapeHtml(BMJ_DATA.strings.empty)}</div>`;
185      return;
186    }
187
188    root.innerHTML = items.map(item => `
189      <a class="bmj-card bmj-card--compact bmj-card--list-link" href="${escapeAttr(item.url)}">
190        <div class="bmj-catalog-card-top">
191          <span class="bmj-card__status ${item.visited ? 'is-visited' : 'is-not-visited'}">${item.visited ? escapeHtml(BMJ_DATA.strings.visited) : escapeHtml(BMJ_DATA.strings.notVisited)}</span>
192          <span class="bmj-catalog-mini-thumb ${item.image ? '' : 'bmj-catalog-mini-thumb--empty'}">${item.image ? `<img src="${escapeAttr(item.image)}" alt="" loading="lazy">` : ''}</span>
193        </div>
194        <h3>${escapeHtml(item.title)}</h3>
195        <p class="bmj-card__geo">${escapeHtml([item.country, item.city].filter(Boolean).join(', '))}</p>
196      </a>
197    `).join('');
198
199    root.querySelectorAll('[data-bmj-toggle]').forEach(button => {
200      button.addEventListener('click', () => toggleVisited(app, button.getAttribute('data-bmj-toggle')));
201    });
202    root.querySelectorAll('[data-bmj-focus]').forEach(button => {
203      button.addEventListener('click', () => focusOnItem(app, Number(button.getAttribute('data-bmj-focus'))));
204    });
205  }
206
207  function renderMap(app) {
208    const root = app.querySelector('[data-bmj-map]');
209    if (!root) return;
210    const items = app._bmj.filtered || [];
211    const mode = app.getAttribute('data-bmj-mode') || 'full';
212    const plotted = items.filter(i => isFinite(Number(i.lat)) && isFinite(Number(i.lng)) && (Number(i.lat) || Number(i.lng)));
213    const provider = (BMJ_DATA.provider || '').toLowerCase();
214
215    if (!root.dataset.bmjHeightLocked) {
216      const h = root.clientHeight || parseInt(app.getAttribute('data-height') || '560', 10) || 560;
217      root.style.minHeight = `${h}px`;
218      root.dataset.bmjHeightLocked = '1';
219    }
220
221    if (provider === 'yandex' && BMJ_DATA.yandexKey) {
222      ensureYandexReady()
223        .then(() => renderYandexMap(root, plotted, app, mode))
224        .catch(() => renderLeafletFallback(root, plotted, app, mode));
225      return;
226    }
227
228    if (provider === 'google' && BMJ_DATA.googleKey) {
229      ensureGoogleReady()
230        .then(() => renderGoogleMap(root, plotted, app, mode))
231        .catch(() => renderLeafletFallback(root, plotted, app, mode));
232      return;
233    }
234
235    renderLeafletFallback(root, plotted, app, mode);
236  }
237
238  function ensureYandexReady() {
239    return new Promise((resolve, reject) => {
240      if (window.ymaps && typeof window.ymaps.ready === 'function') {
241        window.ymaps.ready(resolve);
242        return;
243      }
244      let tries = 0;
245      const timer = setInterval(() => {
246        tries += 1;
247        if (window.ymaps && typeof window.ymaps.ready === 'function') {
248          clearInterval(timer);
249          window.ymaps.ready(resolve);
250          return;
251        }
252        if (tries >= 20) {
253          clearInterval(timer);
254          reject(new Error('Yandex Maps not ready'));
255        }
256      }, 300);
257    });
258  }
259
260  function ensureGoogleReady() {
261    return new Promise((resolve, reject) => {
262      if (window.google && window.google.maps) {
263        resolve();
264        return;
265      }
266      let tries = 0;
267      const timer = setInterval(() => {
268        tries += 1;
269        if (window.google && window.google.maps) {
270          clearInterval(timer);
271          resolve();
272          return;
273        }
274        if (tries >= 20) {
275          clearInterval(timer);
276          reject(new Error('Google Maps not ready'));
277        }
278      }, 300);
279    });
280  }
281
282  function renderLeafletFallback(root, items, app, mode) {
283    if (window.L) {
284      renderLeafletMap(root, items, app, mode);
285      return;
286    }
287    const canvas = document.createElement('div');
288    canvas.className = 'bmj-map__canvas';
289    const note = document.createElement('div');
290    note.className = 'bmj-map__fallback-note';
291    note.textContent = BMJ_DATA.strings.mapFallback;
292    canvas.append(note);
293
294    if (!items.length) {
295      canvas.innerHTML += `<div class="bmj-empty" style="position:absolute;
295left:16px;right:16px;top:60px;">${escapeHtml(BMJ_DATA.strings.empty)}</div>`;
296    } else {
297      const clusters = clusterItems(items, Number(BMJ_DATA.clusterDistance || 0.45));
298      clusters.forEach(cluster => {
299        const marker = document.createElement('button');
300        marker.type = 'button';
301        marker.className = 'bmj-marker ' + (cluster.visited ? 'is-visited' : 'is-not-visited') + (cluster.items.length > 1 ? ' is-cluster' : '') + ' ' + (cluster.items.length === 1 ? ('bmj-marker--' + (cluster.items[0].markerStyle || 'default')) : '');
302        marker.style.left = `${cluster.x}%`;
303        marker.style.top = `${cluster.y}%`;
304        marker.innerHTML = `<span class="bmj-marker__dot">${cluster.items.length > 1 ? escapeHtml(String(cluster.items.length)) : (cluster.items[0].markerStyle==='custom' && cluster.items[0].markerIcon ? `<img src="${cluster.items[0].markerIcon}" style="width:20px;height:20px;">` : getMarkerSymbol(cluster.items[0]))}</span>`;
305        marker.addEventListener('click', () => {
306          if (cluster.items.length > 1) {
307            renderClusterCard(canvas, cluster, app);
308          } else {
309            renderMapCard(canvas, cluster.items[0], app);
310          }
311        });
312        canvas.append(marker);
313      });
314    }
315
316    root.innerHTML = '';
317    root.append(canvas);
318  }
319
320  function renderYandexMap(root, items, app, mode) {
321    root.innerHTML = '<div class="bmj-yandex-map" style="width:100%;height:100%;min-height:inherit;"></div>';
322    const mapEl = root.querySelector('.bmj-yandex-map');
323    const defaults = BMJ_DATA.mapDefaults || { lat: 54.6872, lng: 25.2797, zoom: 4 };
324    const first = items[0];
325    const center = first ? [Number(first.lat), Number(first.lng)] : [Number(defaults.lat), Number(defaults.lng)];
326    const zoom = mode === 'single' && first ? 13 : Number(defaults.zoom || 4);
327
328    try {
329      const map = new ymaps.Map(mapEl, {
330        center,
331        zoom,
332        controls: ['zoomControl']
333      });
334
335      const bounds = [];
336      items.forEach((item) => {
337        const lat = Number(item.lat);
338        const lng = Number(item.lng);
339        if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
340
341        const builtInIcon = getBuiltInMarkerIcon(item);
342        const options = builtInIcon ? {
343          iconLayout: 'default#image',
344          iconImageHref: builtInIcon,
345          iconImageSize: [30, 30],
346          iconImageOffset: [-15, -30]
347        } : {
348          preset: item.visited ? 'islands#greenDotIcon' : 'islands#blueDotIcon',
349          iconColor: item.visited ? '#2e9d51' : '#2b78e4'
350        };
351        const placemark = new ymaps.Placemark([lat, lng], {
352          balloonContentHeader: escapeHtml(item.title),
353          balloonContentBody: `${escapeHtml([item.country, item.city].filter(Boolean).join(', '))}${item.rating ? `<br>Рейтинг: ${escapeHtml(String(item.rating))}` : ''}<br><a href="${escapeAttr(item.url)}">${escapeHtml(BMJ_DATA.strings.openPage)}</a>`
354        }, options);
355
356        map.geoObjects.add(placemark);
357        bounds.push([lat, lng]);
358      });
359
360      if (bounds.length > 1 && mode !== 'single') {
361        map.setBounds(bounds, { checkZoomRange: true, zoomMargin: 30 });
362      } else if (bounds.length === 1) {
363        map.setCenter(bounds[0], mode === 'single' ? 13 : 11);
364        try {
365          const firstGeoObject = map.geoObjects.get(0);
366          if (firstGeoObject) {
367            firstGeoObject.balloon.open();
368          }
369        } catch (e) {}
370      }
371
372      setTimeout(() => {
373        try { map.container.fitToViewport(); } catch (e) {}
374      }, 120);
375    } catch (e) {
376      renderLeafletFallback(root, items, app, mode);
377    }
378  }
379
380  function renderGoogleMap(root, items, app, mode) {
381    root.innerHTML = '<div class="bmj-google-map" style="width:100%;height:100%;min-height:inherit;"></div>';
382    const mapEl = root.querySelector('.bmj-google-map');
383    const defaults = BMJ_DATA.mapDefaults || { lat: 54.6872, lng: 25.2797, zoom: 4 };
384    const first = items[0];
385    const center = first ? { lat: Number(first.lat), lng: Number(first.lng) } : { lat: Number(defaults.lat), lng: Number(defaults.lng) };
386    const zoom = mode === 'single' && first ? 13 : Number(defaults.zoom || 4);
387
388    try {
389      const map = new google.maps.Map(mapEl, { center, zoom });
390      const bounds = new google.maps.LatLngBounds();
391      let hasBounds = false;
392
393      items.forEach((item) => {
394        const lat = Number(item.lat);
395        const lng = Number(item.lng);
396        if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
397        const builtInIcon = getBuiltInMarkerIcon(item);
398        const marker = new google.maps.Marker({
399          position: { lat, lng },
400          map,
401          title: item.title,
402          icon: builtInIcon ? {
403            url: builtInIcon,
404            scaledSize: new google.maps.Size(30, 30)
405          } : {
406            path: google.maps.SymbolPath.CIRCLE,
407            scale: 8,
408            fillColor: item.visited ? '#2e9d51' : '#2b78e4',
409            fillOpacity: 1,
410            strokeColor: '#ffffff',
411            strokeWeight: 2
412          }
413        });
414        const info = new google.maps.InfoWindow({
415          content: `<div><strong>${escapeHtml(item.title)}</strong><br>${escapeHtml([item.country, item.city].filter(Boolean).join(', '))}${item.rating ? `<br>Рейтинг: ${escapeHtml(String(item.rating))}` : ''}<br><a href="${escapeAttr(item.url)}">${escapeHtml(BMJ_DATA.strings.openPage)}</a></div>`
416        });
417        marker.addListener('click', () => info.open({ anchor: marker, map }));
418        if (mode === 'single') {
419          info.open({ anchor: marker, map });
420        }
421        bounds.extend({ lat, lng });
422        hasBounds = true;
423      });
424
425      if (hasBounds && items.length > 1 && mode !== 'single') {
426        map.fitBounds(bounds);
427      } else if (hasBounds && items.length === 1) {
428        map.setCenter(bounds.getCenter());
429        map.setZoom(mode === 'single' ? 13 : 11);
430      }
431    } catch (e) {
432      renderLeafletFallback(root, items, app, mode);
433    }
434  }
435
436  function renderClusterCard(canvas, cluster, app) {
437    removeExistingCard(canvas);
438    const card = document.createElement('div');
439    card.className = 'bmj-map-card';
440    card.innerHTML = `
441      <div class="bmj-card__status ${cluster.visited ? 'is-visited' : 'is-not-visited'}">${escapeHtml(String(cluster.items.length))} ${escapeHtml(BMJ_DATA.strings.clusterLabel)}</div>
442      <div class="bmj-tag-list" style="margin-top:12px;">
443        ${cluster.items.slice(0, 8).map(item => `<span>${escapeHtml(item.title)}</span>`).join('')}
444      </div>
445    `;
446    canvas.append(card);
447  }
448
449  function renderMapCard(canvas, item, app) {
450    removeExistingCard(canvas);
451    const card = document.createElement('div');
452    card.className = 'bmj-map-card';
453    card.innerHTML = `
454      <div class="bmj-card__status ${item.visited ? 'is-visited' : 'is-not-visited'}">
455        ${item.visited ? escapeHtml(BMJ_DATA.strings.visited) : escapeHtml(BMJ_DATA.strings.notVisited)}
456      </div>
457      <h3 style="margin:12px 0 8px;">${escapeHtml(item.title)}</h3>
458      <p class="bmj-card__geo">${escapeHtml([item.country, item.city].filter(Boolean).join(', '))}</p>
459      ${item.subtitle ? `<p>${escapeHtml(item.subtitle)}</p>` : ''}
460      <div class="bmj-card__meta">
461        <span>Рейтинг: <strong>${escapeHtml(item.rating || '—')}</strong></span>
462        <span>Дата: <strong>${escapeHtml(item.visitDate || '—')}</strong></span>
463      </div>
464      ${item.tags?.length ? `<div class="bmj-tag-list">${item.tags.map(tag => `<span>${escapeHtml(tag)}</span>`).join('')}</div>` : ''}
465      <div class="bmj-card__meta">
466        <a class="bmj-button" href="${escapeAttr(item.url)}">${escapeHtml(BMJ_DATA.strings.openPage)}</a>
467      </div>
468    `;
469    canvas.append(card);
470  }
471
472  async function toggleVisited(app, id) {
473    try {
474      const res = await fetch(BMJ_DATA.toggleUrl, {
475        method: 'POST',
476        headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': BMJ_DATA.nonce },
477        body: JSON.stringify({ id: Number(id) })
478      });
479      if (!res.ok) return;
480      const payload = await res.json();
481      const target = (app._bmj.items || []).find(i => Number(i.id) === Number(id));
482      if (target) target.visited = !!payload.visited;
483      applyFilters(app);
484    } catch (e) {
485      console.error('BMJ toggle error', e);
486    }
487  }
488
489  function focusOnItem(app, id) {
490    app._bmj.focusItemId = id;
491    const mapPanel = app.querySelector('[data-bmj-panel="map"]');
492    const listPanel = app.querySelector('[data-bmj-panel="list"]');
493    const tab = app.querySelector('[data-bmj-tab="map"]');
494    if (window.innerWidth < 760 && mapPanel && listPanel && tab) {
495      app.querySelectorAll('[data-bmj-tab]').forEach(b => b.classList.remove('is-active'));
496      tab.classList.add('is-active');
497      mapPanel.classList.add('is-active');
498      listPanel.classList.remove('is-active');
499    }
500    renderMap(app);
501    app.querySelector('[data-bmj-map]')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
502  }
503
504  function clusterItems(items, threshold) {
505    const valid = items.map(item => ({
506      ...item,
507      _x: normalize(item.lng, -180, 180, 3, 97),
508      _y: normalize(item.lat, 85, -85, 6, 94)
509    }));
510
511    const clusters = [];
512    valid.forEach(item => {
513      let cluster = clusters.find(c => distance(c.lat, c.lng, item.lat, item.lng) <= threshold);
514      if (!cluster) {
515        cluster = { items: [], lat: item.lat, lng: item.lng, x: item._x, y: item._y, visited: item.visited };
516        clusters.push(cluster);
517      }
518      cluster.items.push(item);
519      cluster.visited = cluster.items.some(i => i.visited);
520      cluster.lat = avg(cluster.items.map(i => Number(i.lat)));
521      cluster.lng = avg(cluster.items.map(i => Number(i.lng)));
522      cluster.x = avg(cluster.items.map(i => i._x));
523      cluster.y = avg(cluster.items.map(i => i._y));
524    });
525    return clusters;
526  }
527
528  function removeExistingCard(canvas) {
529    const card = canvas.querySelector('.bmj-map-card');
530    if (card) card.remove();
531  }
532
533  function normalize(value, min, max, outMin, outMax) {
534    const ratio = (Number(value) - min) / (max - min);
535    return outMin + Math.max(0, Math.min(1, ratio)) * (outMax - outMin);
536  }
537  function distance(lat1, lng1, lat2, lng2) {
538    const dx = Number(lat1) - Number(lat2);
539    const dy = Number(lng1) - Number(lng2);
540    return Math.sqrt(dx * dx + dy * dy);
541  }
542  function avg(arr) { return arr.reduce((a,b)=>a+b,0) / (arr.length || 1); }
543  function uniq(arr) { return [...new Set(arr)]; }
544  function localeSort(a, b) { return a.localeCompare(b, 'ru'); }
545  function escapeHtml(str) {
546    return String(str).replace(/[&<>"']/g, m => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;', "'":'&#039;' }[m]));
547  }
548  
549  
550  function getBuiltInMarkerIcon(item) {
551    const style = (item && item.markerStyle) || 'default';
552    const base = (window.BMJ_DATA && BMJ_DATA.iconBaseUrl) ? BMJ_DATA.iconBaseUrl : '';
553    if (!base) return '';
554    if (style === 'brewery') return `${base}marker-brewery.svg`;
555    if (style === 'brewpub') return `${base}marker-brewpub.svg`;
556    return '';
557  }
558
559function getMarkerSymbol(item) {
560    switch ((item && item.markerStyle) || 'default') {
561      case 'brewery': return '⚗';
562      case 'brewpub': return '🍻';
563      case 'beer': return '🍺';
564      case 'star': return '★';
565      case 'pin': return '⬤';
566      case 'dot': return '•';
567      default: return '●';
568    }
569  }
570
571function escapeAttr(str) { return escapeHtml(str); }
572})();

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.