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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | 3x 45x 3x 3x 3x 3x 34x 5x 5x 29x 29x 29x 29x 435x 29x 29x 29x 83x 23x 23x 23x 60x 60x 35x 35x 25x 25x 2x 2x 23x 20x 20x 20x 20x 20x 20x 83x 83x 60x 23x 23x 23x 37x 35x 35x 2x 20x 20x | /**
* Parser for Android (Kotlin) color token files.
* Handles direct Color(0x...) hex values, palette token class naming,
* and reference assignments between token classes.
*/
import { normalizeHex } from './color-utils.js';
import {
classNameToMode,
resolvePaletteTokenName,
storeDirectToken,
resolveReferences,
} from './mobile-parser-utils.js';
/**
* Pre-compiled regex patterns used to insert hyphens before compound words
* that aren't separated by camelCase in Android's lowercased token names.
* e.g. 'accentcontainer' → 'accent-container'
*/
const COMPOUND_WORD_PATTERNS = [
'container',
'inverse',
'disabled',
'surface',
'accent',
'background',
'selected',
'hover',
'active',
'focus',
'border',
'error',
'warning',
'success',
'info',
].map((word) => ({ word, regex: new RegExp(`([a-z])${word}`, 'g') }));
const KOTLIN_PALETTE_CLASS_RE = /Palette(.+?)(?:Light|Dark)$/;
const KOTLIN_OBJECT_RE = /object\s+(\w+)/;
const KOTLIN_DIRECT_COLOR_RE = /val\s+(\w+)(?:\s+get\(\))?\s*=\s*Color\(0x([A-Fa-f0-9]+)\)/;
const KOTLIN_REF_RE = /val\s+(\w+)(?:\s+get\(\))?\s*=\s*(\w+)\.(\w+)/;
/**
* Convert an Android Kotlin token name to the canonical CSS token name format.
* Android uses lowercase concatenated names (e.g. 'accentcontainer1', 'errorbase10')
* while CSS uses kebab-case (e.g. 'color-accent-container-1', 'color-error-base-10').
* @param {string} tokenName
* @returns {string}
*/
export function normalizeAndroidTokenName(tokenName) {
if (tokenName.startsWith('palette')) {
const paletteStr = tokenName.substring('palette'.length);
return (
'color-palette-' +
paletteStr
.replaceAll(/([a-z])([A-Z])/g, '$1-$2')
.replaceAll(/([a-z])(\d)/g, '$1-$2')
.replaceAll(/([A-Z])(\d)/g, '$1-$2')
.toLowerCase()
);
}
let result = tokenName
.replaceAll(/([a-z])([A-Z])/g, '$1-$2')
.replaceAll(/([a-zA-Z])(\d)/g, '$1-$2')
.toLowerCase();
if (result.startsWith('color')) result = result.replace(/^color/, '');
if (result.startsWith('on')) result = 'on-' + result.substring(2);
for (const { word, regex } of COMPOUND_WORD_PATTERNS) {
result = result.replace(regex, `$1-${word}`);
}
result = result.replaceAll(/([a-z])base(\d|$)/g, '$1-base-$2');
result = result.replaceAll(/--+/g, '-').replaceAll(/-+$/g, '');
return 'color-' + result;
}
/**
* Parse a single Kotlin source line into a typed result object.
* Returns null for lines that don't match any known pattern.
* @param {string} trimmed
* @param {string|null} currentClass
* @param {string|null} currentMode
* @returns {{ type: 'class', className: string, mode: string|null }
* | { type: 'direct', fullTokenName: string, hexValue: string, mode: string|null }
* | { type: 'ref', tokenName: string, refClass: string, refProperty: string, mode: string|null, refMode: string|null }
* | null}
*/
function parseKotlinLine(trimmed, currentClass, currentMode) {
if (trimmed.includes('object')) {
const m = KOTLIN_OBJECT_RE.exec(trimmed);
Iif (!m) return null;
return { type: 'class', className: m[1], mode: classNameToMode(m[1]) };
}
const directMatch = KOTLIN_DIRECT_COLOR_RE.exec(trimmed);
if (directMatch) {
const fullTokenName = resolvePaletteTokenName(directMatch[1], currentClass, KOTLIN_PALETTE_CLASS_RE);
return { type: 'direct', fullTokenName, hexValue: normalizeHex(directMatch[2]), mode: currentMode };
}
const refMatch = KOTLIN_REF_RE.exec(trimmed);
if (refMatch) {
const refClass = refMatch[2];
return {
type: 'ref',
tokenName: refMatch[1],
refClass,
refProperty: refMatch[3],
mode: currentMode,
refMode: classNameToMode(refClass),
};
}
return null;
}
/**
* Parse all color tokens from a Kotlin source file.
* @param {string} kotlinContent
* @returns {Map<string, string>} token name → hex (e.g. 'color-brand-base-50-light' → 'aabbcc')
*/
export function parseColorsFromKotlin(kotlinContent) {
const colors = new Map();
const directColors = new Map();
const referenceAssignments = [];
let currentMode = null;
let currentClass = null;
for (const line of kotlinContent.split('\n')) {
const result = parseKotlinLine(line.trim(), currentClass, currentMode);
if (!result) continue;
if (result.type === 'class') {
currentClass = result.className;
currentMode = result.mode;
continue;
}
if (result.type === 'direct') {
storeDirectToken(
directColors,
colors,
result.fullTokenName,
result.hexValue,
result.mode,
normalizeAndroidTokenName,
);
continue;
}
Eif (result.type === 'ref') referenceAssignments.push(result);
}
resolveReferences(referenceAssignments, directColors, colors, normalizeAndroidTokenName);
return colors;
}
|