All files / packages/adapters/home-assistant/src emitter.ts

100% Statements 14/14
100% Branches 0/0
100% Functions 7/7
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91                            1x                             134x 14070x           86x                       16x                                           2x   294x 2x 86x     2x 16x 16x                 2x    
// SPDX-License-Identifier: MIT
/**
 * YAML emitter for the Home Assistant adapter.
 *
 * Hand-rolls a deterministic Home Assistant `themes.yaml` document from the Turbo
 * theme registry. js-yaml is intentionally not used at runtime so the adapter has
 * no serialization dependency; tests parse the output with js-yaml to validate it.
 */
 
import { flavors } from '@lgtm-hq/turbo-themes-core';
import { mapTokensToHomeAssistant } from './mapping.js';
import { AUTO_THEME_PAIRINGS, assertPairingsValid, resolveAutoTheme } from './pairings.js';
import { renderKey, renderValue } from './yaml.js';
 
const HEADER = [
  '# Generated by @lgtm-hq/turbo-themes/adapters/home-assistant',
  '# Do not edit by hand.',
].join('\n');
 
/**
 * Render a variable mapping as indented `key: "value"` lines.
 *
 * The mapping preserves {@link REQUIRED_KEYS} order (produced by
 * {@link mapTokensToHomeAssistant}), so iterating its entries is deterministic.
 *
 * @param mapping - Variable name → value record.
 * @param indent - Leading indent applied to every line.
 */
function renderMapping(mapping: Record<string, string>, indent: string): string[] {
  return Object.entries(mapping).map(
    ([key, value]) => `${indent}${renderKey(key)}: ${renderValue(value)}`,
  );
}
 
/** Emit a single flat theme block (top-level keys only). */
function emitFlatTheme(name: string, mapping: Record<string, string>): string {
  return [`${renderKey(name)}:`, ...renderMapping(mapping, '  ')].join('\n');
}
 
/**
 * Emit an auto theme block: the dark mapping at the top level, plus a `modes:`
 * block carrying the full dark and light mappings.
 */
function emitAutoTheme(
  name: string,
  darkMapping: Record<string, string>,
  lightMapping: Record<string, string>,
): string {
  return [
    `${renderKey(name)}:`,
    ...renderMapping(darkMapping, '  '),
    '  modes:',
    '    dark:',
    ...renderMapping(darkMapping, '      '),
    '    light:',
    ...renderMapping(lightMapping, '      '),
  ].join('\n');
}
 
/**
 * Generate the Home Assistant themes YAML document.
 *
 * Flat themes are emitted first, sorted alphabetically by theme id for
 * determinism, followed by the auto (dark/light paired) themes in
 * {@link AUTO_THEME_PAIRINGS} order.
 *
 * @returns The full YAML document as a string.
 * @throws If any auto-theme pairing is invalid (see {@link assertPairingsValid}).
 */
export function generateHomeAssistantThemes(): string {
  assertPairingsValid();
 
  const sorted = [...flavors].sort((a, b) => a.id.localeCompare(b.id, 'en'));
  const blocks = sorted.map((flavor) =>
    emitFlatTheme(flavor.label, mapTokensToHomeAssistant(flavor.tokens)),
  );
 
  for (const pairing of AUTO_THEME_PAIRINGS) {
    const { name, darkTokens, lightTokens } = resolveAutoTheme(pairing);
    blocks.push(
      emitAutoTheme(
        name,
        mapTokensToHomeAssistant(darkTokens),
        mapTokensToHomeAssistant(lightTokens),
      ),
    );
  }
 
  return `${HEADER}\n${blocks.join('\n')}\n`;
}