Files
lms-sb/src/lib/tools/_shared/yaml.ts
T
admins b6d555ab3b Add Obsidian Toolbox — 4 syntax generators under /tools
Inside-LMS toolbox for registered students: callout-CSS, YAML-frontmatter,
theme CSS-variables, Style Settings generators. Auth inherited from (student)
route group; no public surface, no email gate.

- Pure generators in src/lib/tools/* with Vitest (33 tests)
- Shared YAML-safe scalar quoter (_shared/yaml.ts) used by frontmatter +
  style-settings: unquoted user input was silently breaking YAML (tags '#x'
  -> null, color default '#7C3AED' -> null, 'a: b' -> parse error)
- hex validation in callout (no NaN), date input constrained, clipboard +
  analytics calls guarded
- Prisma ToolUsage model + migration; logToolUsage Server Action (auth-first,
  10-min dedup window per user/tool)
- Tool index /tools + ToolCard (explicit lucide map, no import *) + header link

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:45:52 +05:00

36 lines
2.1 KiB
TypeScript

// Безопасная сериализация YAML-скаляра. Общая для генераторов frontmatter и
// style-settings: пользовательский ввод не должен ломать/тихо терять данные в YAML.
// YAML plain-scalar indicator chars: значение, начинающееся с любого из них,
// нужно квотить, иначе парсер прочитает его как seq/map/anchor/tag/comment/число.
const LEADING_INDICATORS = new Set([
"-", "?", ":", ",", "[", "]", "{", "}", "#", "&", "*", "!", "|", ">", "'", '"', "%", "@", "`",
]);
/**
* True, когда значение сломает или коэрцитнёт тип, если вывести его как plain-scalar.
* Покрывает: пустое, окружающие пробелы, ведущие индикаторы, "двоеточие+пробел",
* inline-комментарий "#", и значения, выглядящие как число/булево/null/дата/время.
*/
export function yamlScalarNeedsQuotes(v: string): boolean {
if (v === "") return true;
if (v !== v.trim()) return true;
if (LEADING_INDICATORS.has(v[0])) return true;
if (v.includes(": ") || v.endsWith(":")) return true;
if (v.includes(" #")) return true;
if (/[\n\t]/.test(v)) return true;
if (/^(true|false|yes|no|on|off|null|~)$/i.test(v)) return true; // bool / null
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(v)) return true; // number
if (/^0x[0-9a-fA-F]+$/.test(v)) return true; // hex int
if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(v)) return true; // time HH:MM(:SS) — YAML читает как base-60
if (/^\d{4}-\d{2}-\d{2}([Tt ].*)?$/.test(v)) return true; // date / datetime-looking
return false;
}
/** Вывести значение как безопасный YAML-скаляр — в кавычках только когда нужно. */
export function yamlScalar(v: string): string {
return yamlScalarNeedsQuotes(v)
? '"' + v.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'
: v;
}