Отзывы и предложения к софту от AleXStam
  • Страница 4 из 4
  • «
  • 1
  • 2
  • 3
  • 4
Поговорим о...
{{- $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>
Ррр
Прикрепления:
ttt.noext (16.0 Kb)
есть сайт написанный на 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. Протестируйте поиск
  • Страница 4 из 4
  • «
  • 1
  • 2
  • 3
  • 4
Поиск:
Новый ответ
Имя:
Текст сообщения: