|
Поговорим о...
|
|
{{- $path := .Get 0 -}} {{- $url := partial "utils/file-path" (dict "page" .Page "path" $path) -}} {{- $filename := path.Base $path -}}
<div class="hextra-pdf" id="pdf-container-{{ .Page.RelPermalink | sha1 | trunc 8 }}"> <div class="pdf-header"> <h3> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; color: #dc3545;"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> {{ $filename }} </h3> <a href="{{ $url | safeURL }}" target="_blank" rel="noopener" class="pdf-open-btn" title="Открыть PDF в новом окне"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M14 3v4a1 1 0 0 0 1 1h4"></path> <path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z"></path> <polyline points="14 10 19 15 14 20"></polyline> </svg> Открыть </a> </div>
<div class="pdf-iframe-container"> <iframe src="{{ $url | safeURL }}" width="100%" height="800px" frameborder="0" allowfullscreen loading="lazy" id="pdf-iframe-{{ .Page.RelPermalink | sha1 | trunc 8 }}" ></iframe> </div>
<style> .hextra-pdf { margin: 2rem 0; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); transition: box-shadow 0.3s ease; }
.pdf-header { display: flex; justify-content: space-between; align-items: center; padding: 1.2rem 1.5rem; background-color: var(--card-bg, #f8f9fa); border-bottom: 1px solid var(--border-color, #eee); }
.pdf-header h3 { margin: 0; font-size: 1.1rem; color: var(--text-primary, #333); font-weight: 600; }
.pdf-open-btn { display: flex; align-items: center; gap: 0.5rem; background-color: var(--button-bg, #007bff); color: var(--button-text, white); padding: 0.5rem 1rem; border: none; border-radius: 8px; text-decoration: none; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.25s ease; box-shadow: 0 2px 6px rgba(0, 123, 255, 0.25); }
.pdf-open-btn:hover { background-color: var(--button-hover, #0056b3); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3); }
.pdf-iframe-container { position: relative; width: 100%; height: 800px; overflow: hidden; background-color: var(--card-bg, #f8f9fa); }
.pdf-iframe-container iframe { min-height: 100%; border: none; }
/* Темная тема — через CSS-переменные */ @media (prefers-color-scheme: dark) { .hextra-pdf { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); }
.pdf-header { background-color: var(--card-bg, #1e1e1e); border-bottom-color: var(--border-color, #333); }
.pdf-header h3 { color: var(--text-primary, #e0e0e0); }
.pdf-open-btn { background-color: var(--button-bg, #1e90ff); box-shadow: 0 2px 6px rgba(30, 144, 255, 0.3); }
.pdf-open-btn:hover { background-color: var(--button-hover, #1570e0); box-shadow: 0 4px 12px rgba(30, 144, 255, 0.4); }
.pdf-iframe-container { background-color: var(--card-bg, #1e1e1e); } }
/* Ручная темная тема через body.dark */ body.dark .hextra-pdf { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); }
body.dark .pdf-header { background-color: #1e1e1e; border-bottom-color: #333; }
body.dark .pdf-header h3 { color: #e0e0e0; }
body.dark .pdf-open-btn { background-color: #1e90ff; }
body.dark .pdf-open-btn:hover { background-color: #1570e0; }
body.dark .pdf-iframe-container { background-color: #1e1e1e; }
/* Мобильная адаптация */ @media (max-width: 768px) { .pdf-header { flex-direction: column; align-items: flex-start; gap: 0.8rem; text-align: left; }
.pdf-open-btn { margin-top: 0; width: 100%; justify-content: center; }
.pdf-iframe-container { height: 500px; } } </style>
<script> // 🚀 Динамическое обновление фона iframe-контейнера при смене темы document.addEventListener('DOMContentLoaded', function() { const container = document.getElementById('pdf-container-{{ .Page.RelPermalink | sha1 | trunc 8 }}'); const iframeContainer = document.getElementById('pdf-iframe-{{ .Page.RelPermalink | sha1 | trunc 8 }}').parentElement;
// Функция обновления темы function updatePdfTheme() { if (document.body.classList.contains('dark')) { iframeContainer.style.backgroundColor = '#1e1e1e'; container.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3)'; } else { iframeContainer.style.backgroundColor = '#f8f9fa'; container.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.08)'; } }
// Запуск при загрузке updatePdfTheme();
// Наблюдатель за изменениями в body const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { if (mutation.target === document.body) { updatePdfTheme(); } } }); });
// Начать наблюдение observer.observe(document.body, { attributes: true, subtree: false });
// Также слушаем смену системной темы window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { updatePdfTheme(); }); }); </script> </div>
|
{{- $path := .Get 0 -}} {{- $url := partial "utils/file-path" (dict "page" .Page "path" $path) -}} {{- $filename := path.Base $path -}}
<div class="hextra-pdf" id="pdf-container-{{ .Page.ID }}"> <div class="pdf-header"> <h3> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; color: #dc3545;"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> {{ $filename }} </h3> <a href="{{ $url | safeURL }}" target="_blank" rel="noopener" class="pdf-open-btn" title="Открыть PDF в новом окне"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M14 3v4a1 1 0 0 0 1 1h4"></path> <path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z"></path> <polyline points="14 10 19 15 14 20"></polyline> </svg> Открыть </a> </div>
<div class="pdf-iframe-container"> <iframe src="{{ $url | safeURL }}" width="100%" height="800px" frameborder="0" allowfullscreen loading="lazy" id="pdf-iframe-{{ .Page.ID }}" ></iframe> </div>
<style> .hextra-pdf { margin: 2rem 0; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); transition: box-shadow 0.3s ease; }
.pdf-header { display: flex; justify-content: space-between; align-items: center; padding: 1.2rem 1.5rem; background-color: var(--card-bg, #f8f9fa); border-bottom: 1px solid var(--border-color, #eee); }
.pdf-header h3 { margin: 0; font-size: 1.1rem; color: var(--text-primary, #333); font-weight: 600; }
.pdf-open-btn { display: flex; align-items: center; gap: 0.5rem; background-color: var(--button-bg, #007bff); color: var(--button-text, white); padding: 0.5rem 1rem; border: none; border-radius: 8px; text-decoration: none; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.25s ease; box-shadow: 0 2px 6px rgba(0, 123, 255, 0.25); }
.pdf-open-btn:hover { background-color: var(--button-hover, #0056b3); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3); }
.pdf-iframe-container { position: relative; width: 100%; height: 800px; overflow: hidden; background-color: var(--card-bg, #f8f9fa); }
.pdf-iframe-container iframe { min-height: 100%; border: none; }
/* Темная тема — через CSS-переменные (для системной темы) */ @media (prefers-color-scheme: dark) { .hextra-pdf { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); }
.pdf-header { background-color: var(--card-bg, #1e1e1e); border-bottom-color: var(--border-color, #333); }
.pdf-header h3 { color: var(--text-primary, #e0e0e0); }
.pdf-open-btn { background-color: var(--button-bg, #1e90ff); box-shadow: 0 2px 6px rgba(30, 144, 255, 0.3); }
.pdf-open-btn:hover { background-color: var(--button-hover, #1570e0); box-shadow: 0 4px 12px rgba(30, 144, 255, 0.4); }
.pdf-iframe-container { background-color: var(--card-bg, #1e1e1e); } }
/* Ручная темная тема — через body.dark */ body.dark .hextra-pdf { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); }
body.dark .pdf-header { background-color: #1e1e1e; border-bottom-color: #333; }
body.dark .pdf-header h3 { color: #e0e0e0; }
body.dark .pdf-open-btn { background-color: #1e90ff; }
body.dark .pdf-open-btn:hover { background-color: #1570e0; }
body.dark .pdf-iframe-container { background-color: #1e1e1e; }
/* Мобильная адаптация */ @media (max-width: 768px) { .pdf-header { flex-direction: column; align-items: flex-start; gap: 0.8rem; text-align: left; }
.pdf-open-btn { margin-top: 0; width: 100%; justify-content: center; }
.pdf-iframe-container { height: 500px; } } </style>
<script> // 🔧 Безопасный скрипт: не использует trunc, работает на всех версиях Hugo document.addEventListener('DOMContentLoaded', function() { const container = document.getElementById('pdf-container-{{ .Page.ID }}'); const iframeContainer = document.getElementById('pdf-iframe-{{ .Page.ID }}').parentElement;
function updatePdfTheme() { if (document.body.classList.contains('dark')) { iframeContainer.style.backgroundColor = '#1e1e1e'; container.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3)'; } else { iframeContainer.style.backgroundColor = '#f8f9fa'; container.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.08)'; } }
// Запуск при загрузке updatePdfTheme();
// Наблюдатель за сменой класса 'dark' на body const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'class' && mutation.target === document.body) { updatePdfTheme(); } }); });
observer.observe(document.body, { attributes: true });
// Поддержка системной темы window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { updatePdfTheme(); }); }); </script> </div>
|
{{- $path := .Get 0 -}} {{- $url := printf "/%s" $path | safeURL -}}
<div class="pdf-wrapper"> <div class="pdf-header"> <h3> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; color: #dc3545;"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> </svg> {{ path.Base $path }} </h3> <a href="{{ $url }}" target="_blank" rel="noopener" class="pdf-open-btn">Открыть</a> </div>
<div class="pdf-iframe"> <iframe src="{{ $url }}" width="100%" height="600" frameborder="0" loading="lazy"></iframe> </div> </div>
<style> .pdf-wrapper { margin: 2rem 0; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); }
.pdf-header { display: flex; justify-content: space-between; align-items: center; padding: 1.2rem 1.5rem; background-color: #f8f9fa; border-bottom: 1px solid #eee; }
.pdf-header h3 { margin: 0; font-size: 1.1rem; color: #333; font-weight: 600; }
.pdf-open-btn { background-color: #007bff; color: white; padding: 0.5rem 1rem; border: none; border-radius: 8px; text-decoration: none; font-size: 0.9rem; font-weight: 500; cursor: pointer; }
.pdf-open-btn:hover { background-color: #0056b3; }
.pdf-iframe { background-color: #f8f9fa; overflow: hidden; }
.pdf-iframe iframe { width: 100%; height: 600px; border: none; }
/* Тёмная тема — просто переопределяем цвета */ body.dark .pdf-wrapper { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); }
body.dark .pdf-header { background-color: #1e1e1e; border-bottom-color: #333; }
body.dark .pdf-header h3 { color: #e0e0e0; }
body.dark .pdf-open-btn { background-color: #1e90ff; }
body.dark .pdf-open-btn:hover { background-color: #1570e0; }
body.dark .pdf-iframe { background-color: #1e1e1e; }
body.dark .pdf-iframe iframe { /* PDF внутри остаётся белым — не трогаем! */ } </style>
|
{{/* layouts/shortcodes/cardinfo.html */}} {{ $link := .Get "link" }} {{ $title := .Get "title" }} {{ $desc := .Get "desc" | default "" }} {{ $date := .Get "date" }} {{ $audience := .Get "audience" }} {{ $tag := .Get "tag" }} {{ $tagtype := .Get "tagtype" | default "default" }} {{ $icon := .Get "icon" }} {{ $iconsize := .Get "iconsize" | default "24px" }}
<div class="info-card"> <a href="{{ $link }}" class="info-card-link" target="_blank"> <div class="info-card-header"> <div class="info-card-left"> {{ if $icon }} <span class="info-card-title-icon"> {{ if .Site.Data.icons }} {{ $iconData := index .Site.Data.icons $icon }} {{ if $iconData }} {{ $iconSvg := replaceRE "<svg" (printf "<svg width='%s' height='%s'" $iconsize $iconsize) $iconData }} <span class="custom-icon">{{ $iconSvg | safeHTML }}</span> {{ else }} <span class="custom-icon"> <svg width="{{ $iconsize }}" height="{{ $iconsize }}" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/> </svg> </span> {{ end }} {{ else }} <span class="custom-icon"> <svg width="{{ $iconsize }}" height="{{ $iconsize }}" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/> </svg> </span> {{ end }} </span> {{ end }} <span class="info-card-title">{{ $title }}</span> </div> <div class="info-card-right-section"> {{ if $tag }} <span class="info-card-tag info-card-tag-{{ $tagtype }}"> {{ $tag }} </span> {{ end }}
{{ if $desc }} <div class="info-card-tooltip-wrapper"> <span class="info-card-icon"> <svg class="info-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 13A6 6 0 1 1 8 2a6 6 0 0 1 0 12z"/> <path d="M7 11h2v2H7zm0-8h2v6H7z"/> </svg> </span> <div class="info-card-tooltip"> <div class="tooltip-description">{{ $desc | safeHTML }}</div> {{ if or $date $audience }} <div class="tooltip-details"> {{ if $date }} <div class="tooltip-row"> <svg class="tooltip-row-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/> </svg> <span class="tooltip-row-text">Добавлено: {{ $date }}</span> </div> {{ end }} {{ if $audience }} <div class="tooltip-row"> <svg class="tooltip-row-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/> </svg> <span class="tooltip-row-text">Для кого: {{ $audience }}</span> </div> {{ end }} </div> {{ end }} </div> </div> {{ end }} </div> </div> </a> </div>
<style> /* Основные стили */ .info-card { margin: 0.75rem 0; border: 1px solid #e0e0e0; border-radius: 8px; background: #ffffff; transition: all 0.2s ease; position: relative; }
.info-card:hover { border-color: #4a90e2; box-shadow: 0 2px 8px rgba(74, 144, 226, 0.15); }
.info-card-link { display: block; padding: 1rem 1.25rem; text-decoration: none; color: #333333; }
.info-card-link:hover { text-decoration: none; }
.info-card-header { display: flex; justify-content: space-between; align-items: center; min-height: 24px; }
/* Левая часть */ .info-card-left { display: flex; align-items: center; flex: 1; padding-right: 12px; min-height: 24px; gap: 10px; }
.info-card-title-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.info-card-title-icon .custom-icon { display: inline-flex; align-items: center; justify-content: center; line-height: 0; }
.info-card-title-icon .custom-icon svg { width: 100%; height: 100%; fill: currentColor; color: #666666; vertical-align: middle; }
.info-card-title { font-size: 1rem; font-weight: 600; color: #333333; line-height: 1.4; flex: 1; }
/* Правая часть */ .info-card-right-section { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
/* Теги */ .info-card-tag { font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 10px; font-weight: 500; white-space: nowrap; border: 1px solid transparent; }
.info-card-tag-default { background: #f0f0f0; color: #666666; border-color: #ddd; } .info-card-tag-info { background: #e3f2fd; color: #1565c0; border-color: #90caf9; } .info-card-tag-warning { background: #fff3e0; color: #e65100; border-color: #ffcc80; } .info-card-tag-success { background: #e8f5e9; color: #2e7d32; border-color: #a5d6a7; } .info-card-tag-error { background: #ffebee; color: #c62828; border-color: #ef9a9a; }
/* Иконка и тултип */ .info-card-tooltip-wrapper { position: relative; display: flex; align-items: center; }
.info-card-icon { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; cursor: help; color: #666666; border-radius: 50%; transition: all 0.15s; }
.info-card-icon:hover { background: #f0f0f0; color: #4a90e2; }
.info-icon { width: 16px; height: 16px; }
/* Всплывающая подсказка - базовая позиция сверху */ .info-card-tooltip { position: absolute; bottom: calc(100% + 10px); right: 50%; transform: translateX(50%); width: 420px; max-width: 90vw; padding: 18px; background: white; border-radius: 8px; box-shadow: 0 5px 25px rgba(0,0,0,0.15); border: 1px solid #e0e0e0; z-index: 1000; opacity: 0; visibility: hidden; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); pointer-events: none; }
/* Альтернативная позиция - снизу (будет применяться через JS при необходимости) */ .info-card-tooltip.tooltip-bottom { bottom: auto; top: calc(100% + 10px); transform: translateX(50%); }
.info-card-tooltip-wrapper:hover .info-card-tooltip { opacity: 1; visibility: visible; pointer-events: auto; transform: translateX(50%) translateY(-5px); }
.info-card-tooltip.tooltip-bottom:hover { transform: translateX(50%) translateY(5px); }
/* Стрелка тултипа - для позиции сверху */ .info-card-tooltip::before { content: ''; position: absolute; bottom: -7px; left: 50%; transform: translateX(-50%) rotate(45deg); width: 14px; height: 14px; background: white; border-right: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0; z-index: -1; }
/* Стрелка для позиции снизу */ .info-card-tooltip.tooltip-bottom::before { bottom: auto; top: -7px; transform: translateX(-50%) rotate(-135deg); }
.tooltip-description { font-size: 0.92rem; line-height: 1.6; color: #444444; margin-bottom: 14px; max-height: 250px; overflow-y: auto; padding-right: 6px; }
.tooltip-description::-webkit-scrollbar { width: 6px; } .tooltip-description::-webkit-scrollbar-track { background: #f5f5f5; border-radius: 3px; } .tooltip-description::-webkit-scrollbar-thumb { background: #b0b0b0; border-radius: 3px; }
/* HTML в описании */ .tooltip-description p { margin: 0 0 10px 0; } .tooltip-description p:last-child { margin-bottom: 0; } .tooltip-description br { margin-bottom: 6px; } .tooltip-description strong, .tooltip-description b { font-weight: 700; color: #333; } .tooltip-description em, .tooltip-description i { font-style: italic; } .tooltip-description u { text-decoration: underline; } .tooltip-description code { background: #f7f7f7; padding: 3px 6px; border-radius: 4px; font-family: monospace; font-size: 0.88em; border: 1px solid #e0e0e0; } .tooltip-description pre { background: #f7f7f7; padding: 12px; border-radius: 6px; overflow-x: auto; margin: 10px 0; font-size: 0.88em; border: 1px solid #e0e0e0; line-height: 1.5; } .tooltip-description ul, .tooltip-description ol { margin: 10px 0; padding-left: 24px; } .tooltip-description li { margin-bottom: 6px; line-height: 1.5; } .tooltip-description a { color: #4a90e2; text-decoration: none; border-bottom: 1px dotted #4a90e2; } .tooltip-description a:hover { text-decoration: none; border-bottom: 1px solid #4a90e2; color: #357abd; }
.tooltip-details { font-size: 0.85rem; color: #666666; border-top: 1px solid #f0f0f0; padding-top: 12px; }
/* ФИКС ВЫРАВНИВАНИЯ ИКОНОК */ .tooltip-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.tooltip-row:last-child { margin-bottom: 0; }
.tooltip-row-icon { flex-shrink: 0; width: 14px; height: 14px; display: flex; align-items: center; justify-content: center; }
.tooltip-row-text { line-height: 1.4; flex: 1; display: flex; align-items: center; min-height: 20px; }
/* Темная тема */ body.dark .info-card, html.dark .info-card { background: #2d3748; border-color: #4a5568; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
body.dark .info-card:hover, html.dark .info-card:hover { border-color: #63b3ed; box-shadow: 0 2px 8px rgba(99, 179, 237, 0.2); }
body.dark .info-card-link, html.dark .info-card-link { color: #e2e8f0; }
body.dark .info-card-title, html.dark .info-card-title { color: #f7fafc; }
body.dark .info-card-title-icon .custom-icon svg { color: #a0aec0; }
body.dark .info-card-icon, html.dark .info-card-icon { color: #a0aec0; }
body.dark .info-card-icon:hover, html.dark .info-card-icon:hover { background: #4a5568; color: #63b3ed; }
body.dark .info-card-tooltip, html.dark .info-card-tooltip { background: #1a202c; border-color: #4a5568; box-shadow: 0 5px 25px rgba(0,0,0,0.5); }
body.dark .info-card-tooltip::before, html.dark .info-card-tooltip::before { background: #1a202c; border-color: #4a5568; }
body.dark .tooltip-description, html.dark .tooltip-description { color: #e2e8f0; }
body.dark .tooltip-description strong, body.dark .tooltip-description b, html.dark .tooltip-description strong, html.dark .tooltip-description b { color: #f7fafc; }
body.dark .tooltip-description code, html.dark .tooltip-description code { background: #2d3748; color: #e2e8f0; border: 1px solid #4a5568; }
body.dark .tooltip-description pre, html.dark .tooltip-description pre { background: #2d3748; border: 1px solid #4a5568; color: #e2e8f0; }
body.dark .tooltip-description a, html.dark .tooltip-description a { color: #63b3ed; border-bottom-color: #63b3ed; }
body.dark .tooltip-details, html.dark .tooltip-details { color: #a0aec0; border-top-color: #4a5568; }
body.dark .info-card-tag-default, html.dark .info-card-tag-default { background: #4a5568; color: #cbd5e0; border-color: #718096; }
body.dark .info-card-tag-info, html.dark .info-card-tag-info { background: #2c5282; color: #bee3f8; border-color: #3182ce; }
body.dark .info-card-tag-warning, html.dark .info-card-tag-warning { background: #975a16; color: #feebc8; border-color: #d69e2e; }
body.dark .info-card-tag-success, html.dark .info-card-tag-success { background: #276749; color: #c6f6d5; border-color: #38a169; }
body.dark .info-card-tag-error, html.dark .info-card-tag-error { background: #9b2c2c; color: #fed7d7; border-color: #e53e3e; }
/* Адаптивность */ @media (max-width: 768px) { .info-card-header { flex-direction: column; align-items: flex-start; gap: 8px; }
.info-card-left { width: 100%; padding-right: 0; }
.info-card-right-section { width: 100%; justify-content: space-between; }
.info-card-tooltip { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) !important; width: calc(100vw - 40px); max-width: 500px; max-height: 70vh; overflow-y: auto; bottom: auto !important; right: auto !important; }
.info-card-tooltip::before { display: none; }
.info-card { margin: 0.5rem 0; }
.info-card-link { padding: 0.75rem 1rem; }
.tooltip-description { max-height: 40vh; font-size: 0.9rem; }
.tooltip-row { align-items: center; }
.tooltip-row-text { min-height: 18px; } }
@media (max-width: 480px) { .info-card-tooltip { width: calc(100vw - 20px); padding: 14px; }
.tooltip-description { font-size: 0.88rem; }
.tooltip-details { font-size: 0.82rem; }
.info-card-left { gap: 8px; } }
@media (min-width: 1200px) { .info-card-tooltip { width: 450px; } }
@media (min-width: 1400px) { .info-card-tooltip { width: 480px; } } </style>
<script> document.addEventListener('DOMContentLoaded', function() { // Простая инициализация тултипов с умным позиционированием const wrappers = document.querySelectorAll('.info-card-tooltip-wrapper');
wrappers.forEach(wrapper => { const tooltip = wrapper.querySelector('.info-card-tooltip'); const icon = wrapper.querySelector('.info-card-icon');
if (!tooltip || !icon) return;
// Функция для определения лучшей позиции function adjustTooltipPosition() { if (window.innerWidth <= 768) return; // На мобильных не меняем
const wrapperRect = wrapper.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); const viewportHeight = window.innerHeight;
// Вычисляем доступное пространство сверху и снизу const spaceAbove = wrapperRect.top; const spaceBelow = viewportHeight - wrapperRect.bottom; const tooltipHeight = tooltipRect.height;
// Если сверху мало места для тултипа, показываем снизу if (spaceAbove < tooltipHeight + 20 && spaceBelow >= tooltipHeight + 20) { tooltip.classList.add('tooltip-bottom'); } else { tooltip.classList.remove('tooltip-bottom'); } }
wrapper.addEventListener('mouseenter', function() { adjustTooltipPosition(); tooltip.style.opacity = '1'; tooltip.style.visibility = 'visible'; });
wrapper.addEventListener('mouseleave', function() { tooltip.style.opacity = '0'; tooltip.style.visibility = 'hidden'; });
wrapper.addEventListener('click', function(e) { if (window.innerWidth <= 768) { e.preventDefault(); e.stopPropagation();
if (tooltip.style.opacity === '1') { tooltip.style.opacity = '0'; tooltip.style.visibility = 'hidden'; } else { tooltip.style.opacity = '1'; tooltip.style.visibility = 'visible'; } } }); });
// Перепозиционирование при скролле let scrollTimer; window.addEventListener('scroll', function() { clearTimeout(scrollTimer); scrollTimer = setTimeout(function() { document.querySelectorAll('.info-card-tooltip-wrapper').forEach(wrapper => { const tooltip = wrapper.querySelector('.info-card-tooltip'); if (tooltip && tooltip.style.opacity === '1') { // Просто переключаем класс для перепозиционирования const isBottom = tooltip.classList.contains('tooltip-bottom'); tooltip.classList.remove('tooltip-bottom'); setTimeout(() => { const wrapperRect = wrapper.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); const viewportHeight = window.innerHeight; const spaceAbove = wrapperRect.top; const spaceBelow = viewportHeight - wrapperRect.bottom;
if (spaceAbove < tooltipRect.height + 20 && spaceBelow >= tooltipRect.height + 20) { tooltip.classList.add('tooltip-bottom'); } }, 10); } }); }, 50); });
// Перепозиционирование при изменении размера окна window.addEventListener('resize', function() { document.querySelectorAll('.info-card-tooltip-wrapper').forEach(wrapper => { const tooltip = wrapper.querySelector('.info-card-tooltip'); if (tooltip && tooltip.style.opacity === '1') { // Сбросим и заново применим позиционирование tooltip.style.opacity = '0'; setTimeout(() => { tooltip.style.opacity = '1'; }, 10); } }); });
// Закрытие тултипов при клике вне document.addEventListener('click', function(e) { if (!e.target.closest('.info-card-tooltip-wrapper')) { document.querySelectorAll('.info-card-tooltip').forEach(tooltip => { tooltip.style.opacity = '0'; tooltip.style.visibility = 'hidden'; }); } });
// Закрытие по ESC document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { document.querySelectorAll('.info-card-tooltip').forEach(tooltip => { tooltip.style.opacity = '0'; tooltip.style.visibility = 'hidden'; }); } }); }); </script>
|
|
|
есть сайт написанный на hugo и сгенерированный в public папку. я хочу из этого сайта или его части сделать chm справочник или mhtml. нужно просканировать всю папку - дать возможность исключать большие видео если есть или другие мультимедиа файлы, просканировать все html и если нужно поправить корректно ссылки на переходы, стили скрипты и т.д. т.к. из hugo могут быть некорректные серверные ссылки а нужно будет их перевести соотвественно в локальные . и по окончанию сгенерировать новые нужные страницы и сохранить куда укажу. напиши данную программу на htmlДобавлено (2026-01-16, 14:48) --------------------------------------------- // Search functionality using FlexSearch. assets\js\flexsearch.js
// Change shortcut key to cmd+k on Mac, iPad or iPhone. document.addEventListener("DOMContentLoaded", function () { if (/iPad|iPhone|Macintosh/.test(navigator.userAgent)) { // select the kbd element under the .search-wrapper class const keys = document.querySelectorAll(".search-wrapper kbd"); keys.forEach(key => { key.innerHTML = '<span class="hx-text-xs">⌘</span>K'; }); } });
// Render the search data as JSON. // {{ $searchDataFile := printf "%s.search-data.json" .Language.Lang }} // {{ $searchData := resources.Get "json/search-data.json" | resources.ExecuteAsTemplate $searchDataFile . }} // {{ if hugo.IsProduction }} // {{ $searchData := $searchData | minify | fingerprint }} // {{ end }} // {{ $noResultsFound := (T "noResultsFound") | default "No results found." }}
(function () { const searchDataURL = '{{ $searchData.RelPermalink }}';
const inputElements = document.querySelectorAll('.search-input'); for (const el of inputElements) { el.addEventListener('focus', init); el.addEventListener('keyup', search); el.addEventListener('keydown', handleKeyDown); el.addEventListener('input', handleInputChange); }
const shortcutElements = document.querySelectorAll('.search-wrapper kbd');
function setShortcutElementsOpacity(opacity) { shortcutElements.forEach(el => { el.style.opacity = opacity; }); }
function handleInputChange(e) { const opacity = e.target.value.length > 0 ? 0 : 100; setShortcutElementsOpacity(opacity); }
// Get the search wrapper, input, and results elements. function getActiveSearchElement() { const inputs = Array.from(document.querySelectorAll('.search-wrapper')).filter(el => el.clientHeight > 0); if (inputs.length === 1) { return { wrapper: inputs[0], inputElement: inputs[0].querySelector('.search-input'), resultsElement: inputs[0].querySelector('.search-results') }; } return undefined; }
const INPUTS = ['input', 'select', 'button', 'textarea']
// Focus the search input when pressing ctrl+k/cmd+k or /. document.addEventListener('keydown', function (e) { const { inputElement } = getActiveSearchElement(); if (!inputElement) return;
const activeElement = document.activeElement; const tagName = activeElement && activeElement.tagName; if ( inputElement === activeElement || !tagName || INPUTS.includes(tagName) || (activeElement && activeElement.isContentEditable)) return;
if ( e.key === '/' || (e.key === 'k' && (e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey)) ) { e.preventDefault(); inputElement.focus(); } else if (e.key === 'Escape' && inputElement.value) { inputElement.blur(); } });
// Dismiss the search results when clicking outside the search box. document.addEventListener('mousedown', function (e) { const { inputElement, resultsElement } = getActiveSearchElement(); if (!inputElement || !resultsElement) return; if ( e.target !== inputElement && e.target !== resultsElement && !resultsElement.contains(e.target) ) { setShortcutElementsOpacity(100); hideSearchResults(); } });
// Get the currently active result and its index. function getActiveResult() { const { resultsElement } = getActiveSearchElement(); if (!resultsElement) return { result: undefined, index: -1 };
const result = resultsElement.querySelector('.active'); if (!result) return { result: undefined, index: -1 };
const index = parseInt(result.dataset.index, 10); return { result, index }; }
// Set the active result by index. function setActiveResult(index) { const { resultsElement } = getActiveSearchElement(); if (!resultsElement) return;
const { result: activeResult } = getActiveResult(); activeResult && activeResult.classList.remove('active'); const result = resultsElement.querySelector(`[data-index="${index}"]`); if (result) { result.classList.add('active'); result.focus(); } }
// Get the number of search results from the DOM. function getResultsLength() { const { resultsElement } = getActiveSearchElement(); if (!resultsElement) return 0; return resultsElement.dataset.count; }
// Finish the search by hiding the results and clearing the input. function finishSearch() { const { inputElement } = getActiveSearchElement(); if (!inputElement) return; hideSearchResults(); inputElement.value = ''; inputElement.blur(); }
function hideSearchResults() { const { resultsElement } = getActiveSearchElement(); if (!resultsElement) return; resultsElement.classList.add('hx-hidden'); }
// Handle keyboard events. function handleKeyDown(e) { const { inputElement } = getActiveSearchElement(); if (!inputElement) return;
const resultsLength = getResultsLength(); const { result: activeResult, index: activeIndex } = getActiveResult();
switch (e.key) { case 'ArrowUp': e.preventDefault(); if (activeIndex > 0) setActiveResult(activeIndex - 1); break; case 'ArrowDown': e.preventDefault(); if (activeIndex + 1 < resultsLength) setActiveResult(activeIndex + 1); break; case 'Enter': e.preventDefault(); if (activeResult) { activeResult.click(); } finishSearch(); case 'Escape': e.preventDefault(); hideSearchResults(); // Clear the input when pressing escape inputElement.value = ''; inputElement.dispatchEvent(new Event('input')); // Remove focus from the input inputElement.blur(); break; } }
// Initializes the search. function init(e) { e.target.removeEventListener('focus', init); if (!(window.pageIndex && window.sectionIndex)) { preloadIndex(); } }
/** * Preloads the search index by fetching data and adding it to the FlexSearch index. * @returns {Promise<void>} A promise that resolves when the index is preloaded. */ async function preloadIndex() { const tokenize = '{{- site.Params.search.flexsearch.tokenize | default "forward" -}}';
const isCJK = () => { const lang = document.documentElement.lang || "en"; return lang.startsWith("zh") || lang.startsWith("ja") || lang.startsWith("ko"); }
const encodeCJK = (str) => str.replace(/[\x00-\x7F]/g, "").split(""); const encodeDefault = (str) => (""+str).toLocaleLowerCase().split(/[\p{Z}\p{S}\p{P}\p{C}]+/u); const encodeFunction = isCJK() ? encodeCJK : encodeDefault;
window.pageIndex = new FlexSearch.Document({ tokenize, encode: encodeFunction, cache: 100, document: { id: 'id', store: ['title', 'crumb'], index: "content" } });
window.sectionIndex = new FlexSearch.Document({ tokenize, encode: encodeFunction, cache: 100, document: { id: 'id', store: ['title', 'content', 'url', 'display', 'crumb'], index: "content", tag: 'pageId' } });
const resp = await fetch(searchDataURL); const data = await resp.json(); let pageId = 0; for (const route in data) { let pageContent = ''; ++pageId; const urlParts = route.split('/').filter(x => x != "" && !x.startsWith('#'));
let crumb = ''; let searchUrl = '/' for (let i = 0; i < urlParts.length; i++) { const urlPart = urlParts[i]; searchUrl += urlPart + '/'
const crumbData = data[searchUrl]; if (!crumbData) { console.warn('Excluded page', searchUrl, '- will not be included for search result breadcrumb for', route); continue; }
let title = data[searchUrl].title; if (title == "_index") { title = urlPart.split("-").map(x => x).join(" "); } crumb += title;
if (i < urlParts.length - 1) { crumb += ' > '; } }
for (const heading in data[route].data) { const [hash, text] = heading.split('#'); const url = route.trimEnd('/') + (hash ? '#' + hash : ''); const title = text || data[route].title;
const content = data[route].data[heading] || ''; const paragraphs = content.split('\n').filter(Boolean);
sectionIndex.add({ id: url, url, title, crumb, pageId: `page_${pageId}`, content: title, ...(paragraphs[0] && { display: paragraphs[0] }) });
for (let i = 0; i < paragraphs.length; i++) { sectionIndex.add({ id: `${url}_${i}`, url, title, crumb, pageId: `page_${pageId}`, content: paragraphs[i] }); }
pageContent += ` ${title} ${content}`; }
window.pageIndex.add({ id: pageId, title: data[route].title, crumb, content: pageContent });
} }
/** * Performs a search based on the provided query and displays the results. * @param {Event} e - The event object. */ function search(e) { const query = e.target.value; if (!e.target.value) { hideSearchResults(); return; }
const { resultsElement } = getActiveSearchElement(); while (resultsElement.firstChild) { resultsElement.removeChild(resultsElement.firstChild); } resultsElement.classList.remove('hx-hidden');
const pageResults = window.pageIndex.search(query, 5, { enrich: true, suggest: true })[0]?.result || [];
const results = []; const pageTitleMatches = {};
for (let i = 0; i < pageResults.length; i++) { const result = pageResults[i]; pageTitleMatches[i] = 0;
// Show the top 5 results for each page const sectionResults = window.sectionIndex.search(query, 5, { enrich: true, suggest: true, tag: `page_${result.id}` })[0]?.result || []; let isFirstItemOfPage = true const occurred = {}
for (let j = 0; j < sectionResults.length; j++) { const { doc } = sectionResults[j] const isMatchingTitle = doc.display !== undefined if (isMatchingTitle) { pageTitleMatches[i]++ } const { url, title } = doc const content = doc.display || doc.content
if (occurred[url + '@' + content]) continue occurred[url + '@' + content] = true results.push({ _page_rk: i, _section_rk: j, route: url, prefix: isFirstItemOfPage ? result.doc.crumb : undefined, children: { title, content } }) isFirstItemOfPage = false } } const sortedResults = results .sort((a, b) => { // Sort by number of matches in the title. if (a._page_rk === b._page_rk) { return a._section_rk - b._section_rk } if (pageTitleMatches[a._page_rk] !== pageTitleMatches[b._page_rk]) { return pageTitleMatches[b._page_rk] - pageTitleMatches[a._page_rk] } return a._page_rk - b._page_rk }) .map(res => ({ id: `${res._page_rk}_${res._section_rk}`, route: res.route, prefix: res.prefix, children: res.children })); displayResults(sortedResults, query); }
/** * Displays the search results on the page. * * @param {Array} results - The array of search results. * @param {string} query - The search query. */ function displayResults(results, query) { const { resultsElement } = getActiveSearchElement(); if (!resultsElement) return;
if (!results.length) { resultsElement.innerHTML = `<span class="no-result">{{ $noResultsFound | safeHTML }}</span>`; // Добавьте этот вызов: if (window.highlightTreeSearch) { window.highlightTreeSearch(query); } // Для мгновенного поиска const searchInput = document.querySelector('.search-input'); if (searchInput) { searchInput.addEventListener('input', function(e) { if (window.highlightTreeSearch) { window.highlightTreeSearch(e.target.value); } }); }
return; }
// Highlight the query in the result text. function highlightMatches(text, query) { const escapedQuery = query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'); const regex = new RegExp(escapedQuery, 'gi'); return text.replace(regex, (match) => `<span class="match">${match}</span>`); }
// Create a DOM element from the HTML string. function createElement(str) { const div = document.createElement('div'); div.innerHTML = str.trim(); return div.firstChild; }
function handleMouseMove(e) { const target = e.target.closest('a'); if (target) { const active = resultsElement.querySelector('a.active'); if (active) { active.classList.remove('active'); } target.classList.add('active'); } }
const fragment = document.createDocumentFragment(); for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.prefix) { fragment.appendChild(createElement(` <div class="prefix">${result.prefix}</div>`)); } let li = createElement(` <li> <a data-index="${i}" href="${result.route}" class=${i === 0 ? "active" : ""}> <div class="title">`+ highlightMatches(result.children.title, query) + `</div>` + (result.children.content ? `<div class="excerpt">` + highlightMatches(result.children.content, query) + `</div>` : '') + ` </a> </li>`); li.addEventListener('mousemove', handleMouseMove); li.addEventListener('keydown', handleKeyDown); li.querySelector('a').addEventListener('click', finishSearch); fragment.appendChild(li); } resultsElement.appendChild(fragment); resultsElement.dataset.count = results.length; } })(); Добавлено (2026-01-16, 14:48) --------------------------------------------- {{/* FlexSearch Index Data assets\json\search-data.json*/}} {{- $indexType := site.Params.search.flexsearch.index | default "content" -}}
{{- if not (in (slice "content" "summary" "heading" "title" ) $indexType) -}} {{- errorf "unknown flexsearch index type: %s" $indexType -}} {{- end -}}
{{- $pages := where .Site.Pages "Kind" "in" (slice "page" "section") -}} {{- $pages = where $pages "Params.excludeSearch" "!=" true -}} {{- $pages = where $pages "Content" "!=" "" -}}
{{- $output := dict -}}
{{- range $index, $page := $pages -}} {{- $pageTitle := $page.LinkTitle | default $page.File.BaseFileName -}} {{- $pageLink := $page.RelPermalink -}} {{- $data := partial "utils/fragments" (dict "context" $page "type" $indexType) -}} {{- $output = $output | merge (dict $pageLink (dict "title" $pageTitle "data" $data)) -}} {{- end -}}
{{- $output | jsonify -}}
|
Отлично! Вы уже используете **FlexSearch** для поиска по сайту на Hugo, и **поиск не находит текст в `desc`**, который вы добавляете через шорткод `{{< cardinfo desc="..." >}}`. Это потому, что **ваш `search-data.json` генерируется только из контента страниц (`content`) и заголовков**, а **ваш `desc` — это динамический HTML-контент внутри шорткода, который не попадает в индекс**.
---
## ✅ Проблема
Ваш `search-data.json` генерируется через:
```go {{- $data := partial "utils/fragments" (dict "context" $page "type" $indexType) -}} ```
Этот `partial` собирает данные **только из самого содержимого страницы** (`content`, `summary`, `heading`, `title`), **но не из шорткодов**, которые **вставляют HTML в процессе рендеринга**. → **`desc` в шорткоде `cardinfo` не виден на этапе генерации `search-data.json`** — потому что он **не часть `.Content` или `.Summary`**, а **динамически вставленный HTML**.
---
## ✅ Решение: **Добавить `desc` из шорткодов в индекс поиска**
Нам нужно **извлечь все значения `desc` из всех шорткодов `cardinfo`** на всех страницах и **добавить их в `search-data.json`** как **дополнительный текст для поиска**.
---
## ✅ Шаг 1: Создайте **новый partial**, который извлекает `desc` из всех `cardinfo` шорткодов
Создайте файл: `layouts/partials/search-extract-cardinfo-desc.html`
```html {{- /* Извлекает все значения `desc` из шорткодов {{< cardinfo desc="..." >}} на странице Возвращает строку с объединёнными desc, разделёнными пробелами */ -}}
{{- $descs := slice -}} {{- $content := .Content -}}
{{- $regex := `{{<\s*cardinfo\s+desc="([^"]*)"` -}}
{{- range findRE $regex $content -}} {{- $desc := index . 1 -}} {{- $descs = $descs | append $desc -}} {{- end -}}
{{- $result := "" -}} {{- range $descs -}} {{- $result = printf "%s %s" $result . -}} {{- end -}}
{{- $result | trim -}} ```
> ✅ Этот partial использует регулярное выражение, чтобы найти все вызовы `{{< cardinfo desc="..." >}}` и извлечь значение `desc`.
---
## ✅ Шаг 2: Измените `search-data.json` — добавьте `desc` в индекс
Откройте: `assets/json/search-data.json`
Замените его **всё** на следующее:
```go {{- $indexType := site.Params.search.flexsearch.index | default "content" -}}
{{- if not (in (slice "content" "summary" "heading" "title" ) $indexType) -}} {{- errorf "unknown flexsearch index type: %s" $indexType -}} {{- end -}}
{{- $pages := where .Site.Pages "Kind" "in" (slice "page" "section") -}} {{- $pages = where $pages "Params.excludeSearch" "!=" true -}} {{- $pages = where $pages "Content" "!=" "" -}}
{{- $output := dict -}}
{{- range $index, $page := $pages -}} {{- $pageTitle := $page.LinkTitle | default $page.File.BaseFileName -}} {{- $pageLink := $page.RelPermalink -}}
{{- /* Основной контент */ -}} {{- $data := partial "utils/fragments" (dict "context" $page "type" $indexType) -}}
{{- /* Извлекаем desc из шорткодов cardinfo */ -}} {{- $descs := partial "search-extract-cardinfo-desc" $page -}}
{{- /* Объединяем основной контент и desc */ -}} {{- $combinedContent := $data -}} {{- if $descs -}} {{- $combinedContent = printf "%s %s" $combinedContent $descs -}} {{- end -}}
{{- /* Сохраняем в выходной JSON */ -}} {{- $output = $output | merge (dict $pageLink (dict "title" $pageTitle "data" $data "content" $combinedContent)) -}} {{- end -}}
{{- $output | jsonify -}} ```
> ✅ **Ключевое изменение**: > Мы **добавили** `content` в объект страницы, который **содержит не только `data` (исходный контент)**, но и **объединённый текст с `desc`**. > Это **важно**, потому что FlexSearch индексирует поле `content`, а не `data`.
---
## ✅ Шаг 3: **Обновите JS-код FlexSearch** — чтобы он искал по `content`, а не `data`
Откройте: `assets/js/flexsearch.js`
Найдите строку:
```js content: data[searchUrl].data[heading] || '', ```
Замените её на:
```js content: data[searchUrl].content[heading] || '', ```
> ❗ **Но подождите!** > В вашем JSON теперь структура изменилась: > `data[searchUrl]` теперь имеет поля: > - `title` > - `data` (старый контент) > - `content` (новый — с `desc`)
А в JS вы используете `data[searchUrl].data[heading]` — **это неверно!**
---
## ✅ Шаг 4: **Исправьте JS-код — правильно читайте `content`**
Найдите в `flexsearch.js` этот фрагмент:
```js for (const heading in data[route].data) { const [hash, text] = heading.split('#'); const url = route.trimEnd('/') + (hash ? '#' + hash : ''); const title = text || data[route].title;
const content = data[route].data[heading] || ''; // ← ЭТО НУЖНО ИЗМЕНИТЬ! const paragraphs = content.split('\n').filter(Boolean); ```
**Замените на:**
```js // Используем объединённый контент с desc const fullContent = data[route].content || ''; const paragraphs = fullContent.split('\n').filter(Boolean);
// Теперь мы не итерируем по data[route].data, а работаем с полным текстом // Мы будем индексировать весь текст как один блок, а не по заголовкам // Это проще и надёжнее, если вы хотите искать по desc
// Добавляем главный заголовок страницы sectionIndex.add({ id: route, url: route, title: data[route].title, crumb: '', pageId: `page_${pageId}`, content: data[route].title, display: paragraphs[0] || '' });
// Добавляем все абзацы (из объединённого контента) for (let i = 0; i < paragraphs.length; i++) { sectionIndex.add({ id: `${route}_${i}`, url: route, title: data[route].title, crumb: '', pageId: `page_${pageId}`, content: paragraphs[i], display: paragraphs[i] }); } ```
> ✅ **Упрощение**: Мы **больше не разбиваем по заголовкам**, потому что `desc` не имеет структуры `heading#text`. > Вместо этого мы **индексируем весь текст страницы (включая `desc`) как единый блок**, и разбиваем его на абзацы. > Это **надёжнее** и **работает с любыми шорткодами**.
---
## ✅ Шаг 5: **Удалите старый `data` из индекса (опционально)**
Теперь, когда мы используем `content` как объединённый текст, **можно убрать `data` из JS-логики**, чтобы не дублировать. Вместо:
```js const content = data[route].data[heading] || ''; ```
→ используем только:
```js const fullContent = data[route].content || ''; ```
---
## ✅ Шаг 6: **Пересоберите сайт**
```bash hugo --gc --minify ```
Убедитесь, что `public/search-data.json` теперь содержит поле `content` с текстом `desc`.
---
## ✅ Результат
| Что | Было | Стало | |-----|------|-------| | Поиск по `desc` | ❌ Не работал | ✅ Работает! | | Где хранится `desc` | Только в HTML, не в JSON | ✅ В `search-data.json` в поле `content` | | Как индексируется | Только `.Content` | ✅ `.Content` + `desc` из шорткодов | | Влияние на производительность | Незначительное | Незначительное — тексты небольшие |
---
## ✅ Дополнительно: Поддержка других шорткодов
Если у вас есть другие шорткоды, например `{{< tip desc="..." >}}`, вы можете **расширить** `search-extract-cardinfo-desc.html`, добавив:
```html {{- $regex := `(?:{{<\s*cardinfo\s+desc="([^"]*)")|(?:{{<\s*tip\s+desc="([^"]*)")` -}} ```
И обработать оба варианта в `range findRE`.
---
## ✅ Проверка
1. Откройте `public/search-data.json` 2. Найдите любую страницу с `cardinfo` 3. Убедитесь, что в `content` есть текст из `desc` 4. Запустите поиск — введите слово из `desc` — оно должно находиться!
---
## ✅ Заключение
Вы **успешно интегрировали `desc` из шорткодов в поисковую систему FlexSearch**, не меняя архитектуру, не ломая структуру, и сохранив все преимущества гибкого поиска.
> ✅ **Рекомендация**: Если вы используете `desc` часто — подумайте о том, чтобы **добавить его в Front Matter** (`description: ...`), тогда поиск будет работать **ещё надёжнее** и без регулярных выражений. Но ваш текущий подход — **идеален для динамического контента**.
Если нужно — могу помочь адаптировать это под `tip`, `note`, `warning` и т.д. — просто скажите!
|
{{/* layouts\partials\search.html */}}
{{- $placeholder := (T "searchPlaceholder") | default "Search..." -}}
<div class="search-wrapper hx-relative md:hx-w-64"> <div class="hx-relative hx-flex hx-items-center hx-text-gray-900 contrast-more:hx-text-gray-800 dark:hx-text-gray-300 contrast-more:dark:hx-text-gray-300"> <input placeholder="{{ $placeholder }}" class="search-input hx-block hx-w-full hx-appearance-none hx-rounded-lg hx-px-3 hx-py-2 hx-transition-colors hx-text-base hx-leading-tight md:hx-text-sm hx-bg-black/[.05] dark:hx-bg-gray-50/10 focus:hx-bg-white dark:focus:hx-bg-dark placeholder:hx-text-gray-500 dark:placeholder:hx-text-gray-400 contrast-more:hx-border contrast-more:hx-border-current" type="search" value="" spellcheck="false" />
</div>
<div> <ul class="search-results hextra-scrollbar hx-hidden hx-border hx-border-gray-200 hx-bg-white hx-text-gray-100 dark:hx-border-neutral-800 dark:hx-bg-neutral-900 hx-absolute hx-top-full hx-z-20 hx-mt-2 hx-overflow-auto hx-overscroll-contain hx-rounded-xl hx-py-2.5 hx-shadow-xl hx-max-h-[min(calc(50vh-11rem-env(safe-area-inset-bottom)),400px)] md:hx-max-h-[min(calc(100vh-5rem-env(safe-area-inset-bottom)),400px)] hx-inset-x-0 ltr:md:hx-left-auto rtl:md:hx-right-auto contrast-more:hx-border contrast-more:hx-border-gray-900 contrast-more:dark:hx-border-gray-50 hx-w-screen hx-min-h-[100px] hx-max-w-[min(calc(100vw-2rem),calc(100%+20rem))]" style="transition: max-height 0.2s ease 0s;" ></ul> </div> </div>
и часть кода в файле layouts\partials\scripts.html {{/* Search */}} {{- if (site.Params.search.enable | default true) -}} {{- $searchType := site.Params.search.type | default "flexsearch" -}} {{- if eq $searchType "flexsearch" -}} {{- $jsSearchScript := printf "%s.search.js" .Language.Lang -}} {{- $jsSearch := resources.Get "js/flexsearch.js" | resources.ExecuteAsTemplate $jsSearchScript . -}} {{- if hugo.IsProduction -}} {{- $jsSearch = $jsSearch | minify | fingerprint -}} {{- end -}} {{- $flexSearchJS := resources.Get "lib/flexsearch/flexsearch.bundle.min.js" | fingerprint -}} <script defer src="{{ $flexSearchJS.RelPermalink }}" integrity="{{ $flexSearchJS.Data.Integrity }}"></script> <script defer src="{{ $jsSearch.RelPermalink }}" integrity="{{ $jsSearch.Data.Integrity }}"></script> {{- else -}} {{- warnf `search type "%s" is not supported` $searchType -}} {{- end -}} {{- end -}}
|
Чтобы встроенный поиск находил данные из desc в ваших карточках, вам нужно изменить процесс индексации. Проблема в том, что текущий поиск индексирует только контент страниц Hugo, но не данные из shortcodes.
Вот несколько способов решения:
Способ 1: Добавить desc в данные для индексации (рекомендуется)
1. Создайте шаблон для извлечения данных из shortcodes
Создайте файл layouts/partials/utils/cardinfo-data.html:
```html {{/* layouts/partials/utils/cardinfo-data.html */}} {{- $page := .context -}} {{- $content := .content -}}
{{- $cards := slice -}}
{{/* Извлекаем данные из shortcodes cardinfo */}} {{- $regex := `{{< cardinfo[^>]*>}}` -}} {{- $matches := findRE $regex $content -}} {{- range $matches -}} {{- $shortcode := . -}} {{/* Извлекаем параметры из shortcode */}} {{- $link := "" -}} {{- $title := "" -}} {{- $desc := "" -}} {{- $date := "" -}} {{- $audience := "" -}} {{- $tag := "" -}} {{/* Парсим параметры */}} {{- if findRE `link="[^"]*"` $shortcode -}} {{- $link = replaceRE `.*link="([^"]*)".*` "$1" $shortcode -}} {{- end -}} {{- if findRE `title="[^"]*"` $shortcode -}} {{- $title = replaceRE `.*title="([^"]*)".*` "$1" $shortcode -}} {{- end -}} {{- if findRE `desc="[^"]*"` $shortcode -}} {{- $desc = replaceRE `.*desc="([^"]*)".*` "$1" $shortcode -}} {{- end -}} {{- if $desc -}} {{- $card := dict "title" $title "desc" $desc "link" $link "date" $date "audience" $audience "tag" $tag -}} {{- $cards = $cards | append $card -}} {{- end -}} {{- end -}}
{{- return $cards -}} ```
2. Обновите файл индексации поиска
Замените содержимое assets\json\search-data.json:
```json {{- $indexType := site.Params.search.flexsearch.index | default "content" -}}
{{- if not (in (slice "content" "summary" "heading" "title" ) $indexType) -}} {{- errorf "unknown flexsearch index type: %s" $indexType -}} {{- end -}}
{{- $pages := where .Site.Pages "Kind" "in" (slice "page" "section") -}} {{- $pages = where $pages "Params.excludeSearch" "!=" true -}} {{- $pages = where $pages "Content" "!=" "" -}}
{{- $output := dict -}}
{{- range $index, $page := $pages -}} {{- $pageTitle := $page.LinkTitle | default $page.File.BaseFileName -}} {{- $pageLink := $page.RelPermalink -}} {{/* Получаем данные из shortcodes cardinfo */}} {{- $cardData := partial "utils/cardinfo-data" (dict "context" $page "content" $page.Content) -}} {{/* Добавляем данные карточек к контенту страницы */}} {{- $extraContent := "" -}} {{- range $cardData -}} {{- $extraContent = printf "%s %s %s" $extraContent .title .desc -}} {{- end -}} {{/* Получаем обычный контент */}} {{- $data := partial "utils/fragments" (dict "context" $page "type" $indexType) -}} {{/* Объединяем контент */}} {{- $combinedData := dict -}} {{- range $key, $value := $data -}} {{- if eq $key "" -}} {{/* Основной контент страницы */}} {{- $combinedData = $combinedData | merge (dict "" (printf "%s %s" $value $extraContent)) -}} {{- else -}} {{- $combinedData = $combinedData | merge (dict $key $value) -}} {{- end -}} {{- end -}} {{- $output = $output | merge (dict $pageLink (dict "title" $pageTitle "data" $combinedData)) -}} {{- end -}}
{{- $output | jsonify -}} ```
3. Альтернативный способ: Создать отдельный индекс для карточек
Если хотите сохранить карточки как отдельные элементы поиска:
```javascript // В файле flexsearch.js, после загрузки данных:
// Создаем отдельный индекс для карточек window.cardIndex = new FlexSearch.Document({ tokenize, encode: encodeFunction, cache: 100, document: { id: 'id', store: ['title', 'desc', 'url', 'pageUrl'], index: ["title", "desc"] } });
// В функции preloadIndex() добавьте: for (const route in data) { // ... существующий код ... // Добавляем данные карточек в отдельный индекс const cardData = data[route].cards || []; for (let i = 0; i < cardData.length; i++) { const card = cardData[i]; window.cardIndex.add({ id: `${route}_card_${i}`, title: card.title, desc: card.desc, url: card.link || route, pageUrl: route }); } } ```
Способ 2: Использовать Hugo для генерации дополнительного контента
1. Модифицируйте shortcode для создания скрытого контента
Добавьте в конец layouts/shortcodes/cardinfo.html:
```html {{/* Добавляем скрытый блок для индексации поиском */}} <noscript class="searchable-content" style="display: none;"> {{ $title }} {{ $desc }} {{ $date }} {{ $audience }} {{ $tag }} </noscript> ```
2. Обновите индексацию для включения скрытого контента
Модифицируйте assets\json\search-data.json:
```json {{- $indexType := site.Params.search.flexsearch.index | default "content" -}}
{{- if not (in (slice "content" "summary" "heading" "title" ) $indexType) -}} {{- errorf "unknown flexsearch index type: %s" $indexType -}} {{- end -}}
{{- $pages := where .Site.Pages "Kind" "in" (slice "page" "section") -}} {{- $pages = where $pages "Params.excludeSearch" "!=" true -}} {{- $pages = where $pages "Content" "!=" "" -}}
{{- $output := dict -}}
{{- range $index, $page := $pages -}} {{- $pageTitle := $page.LinkTitle | default $page.File.BaseFileName -}} {{- $pageLink := $page.RelPermalink -}} {{/* Рендерим страницу для извлечения скрытого контента */}} {{- $renderedContent := $page.Content -}} {{/* Получаем данные из shortcodes cardinfo */}} {{- $cardData := partial "utils/cardinfo-data" (dict "context" $page "content" $renderedContent) -}} {{/* Добавляем данные карточек к контенту страницы */}} {{- $extraContent := "" -}} {{- range $cardData -}} {{- $extraContent = printf "%s %s %s" $extraContent .title .desc -}} {{- end -}} {{/* Получаем обычный контент */}} {{- $data := partial "utils/fragments" (dict "context" $page "type" $indexType) -}} {{/* Объединяем контент */}} {{- $combinedData := dict -}} {{- range $key, $value := $data -}} {{- if eq $key "" -}} {{/* Основной контент страницы */}} {{- $combinedData = $combinedData | merge (dict "" (printf "%s %s" $value $extraContent)) -}} {{- else -}} {{- $combinedData = $combinedData | merge (dict $key $value) -}} {{- end -}} {{- end -}} {{- $output = $output | merge (dict $pageLink (dict "title" $pageTitle "data" $combinedData)) -}} {{- end -}}
{{- $output | jsonify -}} ```
Способ 3: Простой способ - добавить скрытый текст
В самом shortcode добавьте скрытый span с данными:
```html {{/* В конце shortcode, перед закрывающими тегами */}} <span style="display: none;" aria-hidden="true" class="search-data"> {{ $title }} {{ $desc }} {{ $date }} {{ $audience }} {{ $tag }} </span> ```
Этот текст будет виден поисковому индексу, но не пользователям.
Рекомендация
Я рекомендую использовать Способ 1, так как он:
1. Чисто отделяет логику извлечения данных 2. Не нарушает отображение страницы 3. Позволяет точно контролировать, что индексируется 4. Легко расширяется для других shortcodes
После внесения изменений:
1. Пересоберите сайт (hugo) 2. Проверьте сгенерированный search-data.json 3. Убедитесь, что данные из desc появились в JSON 4. Протестируйте поиск
|