feat(reader): добавить парсер RFC из RFC Editor и отдельную группу спецификаций на книжной полке

- Реализован парсер RFC в app/reader.py с авто-загрузкой с rfc-editor.org и datatracker.ietf.org
- Очистка пагинации ([Page X], \x0c) и разбиение спецификаций на структурированные главы (TOC, разделы)
- Эндпоинт POST /api/books/fetch-rfc с защитой от дублирования и поддержкой номеров/URL
- Визуальное разделение на книжной полке через hr-разделитель и блок спецификаций IETF & RFC
- Быстрые пресеты стандартов (HTTP/1.1, HTTP/2, TLS 1.3, OAuth 2.0, WebSocket, QUIC, TCP, DNS)
- Интеграция в интерактивную читалку (перевод по клику, авто-захват контекста, SRS карточки)
- Unit-тесты парсера и API (31/31 passed) и обновление Service Worker кэша
This commit is contained in:
2026-10-06 21:46:07 +03:00
parent e73d3470b3
commit 82b7a654ca
8 changed files with 877 additions and 66 deletions
+204
View File
@@ -3399,6 +3399,210 @@ body.drawer-open {
border-top: 1px solid var(--border-color);
}
/* Bookshelf Divider between General Books & RFC Specifications */
.bookshelf-divider {
margin: 48px 0 36px;
border: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--border-color) 15%, var(--border-color) 85%, transparent);
}
/* RFC Specifications Section */
.rfc-section {
margin-top: 8px;
}
.rfc-header {
margin-bottom: 20px;
}
.rfc-title-row {
display: flex;
align-items: center;
gap: 14px;
}
.rfc-icon-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
font-size: 24px;
background: rgba(14, 165, 233, 0.12);
border: 1px solid rgba(14, 165, 233, 0.28);
border-radius: var(--radius-md);
flex-shrink: 0;
}
.rfc-heading {
font-size: 20px;
font-weight: 700;
color: var(--text-main);
margin: 0;
}
.rfc-fetch-box {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px 24px;
margin-bottom: 24px;
box-shadow: var(--shadow-sm);
}
.rfc-fetch-form {
margin-bottom: 16px;
}
.rfc-input-wrap {
display: flex;
align-items: stretch;
gap: 10px;
}
.rfc-input-prefix {
display: inline-flex;
align-items: center;
padding: 0 14px;
font-weight: 700;
font-size: 13px;
letter-spacing: 0.05em;
color: #38bdf8;
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.25);
border-radius: var(--radius-md);
user-select: none;
}
.rfc-input {
flex: 1;
min-width: 0;
}
.rfc-fetch-btn {
white-space: nowrap;
padding: 0 20px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.rfc-fetch-status {
margin-top: 10px;
padding: 8px 14px;
font-size: 13px;
border-radius: var(--radius-md);
transition: all 0.2s ease;
}
.rfc-status-loading {
background: rgba(14, 165, 233, 0.1);
color: #38bdf8;
border: 1px solid rgba(14, 165, 233, 0.25);
}
.rfc-status-success {
background: rgba(34, 197, 94, 0.12);
color: #4ade80;
border: 1px solid rgba(34, 197, 94, 0.25);
}
.rfc-status-error {
background: rgba(239, 68, 68, 0.12);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.25);
}
.rfc-presets-wrap {
display: flex;
flex-direction: column;
gap: 10px;
}
.rfc-presets-label {
font-size: 12px;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
color: var(--text-dim);
}
.rfc-presets-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.rfc-preset-chip {
background: var(--bg-input);
color: var(--text-main);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
padding: 6px 14px;
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
user-select: none;
display: inline-flex;
align-items: center;
gap: 5px;
}
.rfc-preset-chip:hover {
background: rgba(14, 165, 233, 0.15);
border-color: #38bdf8;
color: #38bdf8;
transform: translateY(-1px);
}
.rfc-empty-state {
padding: 36px 20px;
background: var(--bg-surface);
border: 1px dashed var(--border-color);
border-radius: var(--radius-lg);
margin-bottom: 24px;
}
.rfc-card {
border-color: rgba(56, 189, 248, 0.2);
}
.rfc-card:hover {
border-color: rgba(56, 189, 248, 0.5);
box-shadow: 0 8px 24px rgba(14, 165, 233, 0.12);
}
.rfc-cover-icon {
background: rgba(14, 165, 233, 0.1);
border-color: rgba(14, 165, 233, 0.25);
}
.badge-rfc {
background: rgba(14, 165, 233, 0.2);
color: #38bdf8;
border: 1px solid rgba(14, 165, 233, 0.35);
}
.rfc-progress-fill {
background: linear-gradient(90deg, #0284c7, #38bdf8);
}
@media (max-width: 640px) {
.rfc-input-wrap {
flex-direction: column;
}
.rfc-input-prefix {
display: none;
}
.rfc-fetch-btn {
width: 100%;
padding: 10px 16px;
}
}
/* Reader Interface */
.reader-interface {
background-color: var(--bg-surface);