Function bodies 456 total
drawText function · javascript · L84-L91 (8 LOC)client/src/components/TinyScreen.jsx
function drawText(ctx, text, x, baselineY, font) {
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i)
if (code >= 32 && code <= 126) {
drawChar(ctx, code, x + i * font.width, baselineY, font)
}
}
}drawIcon function · javascript · L97-L123 (27 LOC)client/src/components/TinyScreen.jsx
function drawIcon(ctx, iconId, x, y, color = ICON_COLOR) {
const bytes = Icons[iconId]
if (!bytes) return
ctx.fillStyle = color
for (let row = 0; row < ICON_SIZE; row++) {
const b0 = bytes[row * 3]
const b1 = bytes[row * 3 + 1]
const b2 = bytes[row * 3 + 2]
for (let bit = 0; bit < ICON_SIZE; bit++) {
let byteIdx, bitIdx
if (bit < 8) {
byteIdx = b0
bitIdx = bit
} else if (bit < 16) {
byteIdx = b1
bitIdx = bit - 8
} else {
byteIdx = b2
bitIdx = bit - 16
}
if (byteIdx & (1 << bitIdx)) {
ctx.fillRect(x + bit, y + row, 1, 1)
}
}
}
}drawSelectionBar function · javascript · L128-L132 (5 LOC)client/src/components/TinyScreen.jsx
function drawSelectionBar(ctx, activeIndex, color) {
if (activeIndex < 0 || activeIndex > 2) return
ctx.fillStyle = color
ctx.fillRect(BAR_X, SLOT_Y[activeIndex], BAR_W, ICON_SLOT_H)
}operatorWordWrap function · javascript · L154-L174 (21 LOC)client/src/components/TinyScreen.jsx
function operatorWordWrap(sentence, previewWord) {
const combined = [sentence.trim(), previewWord].filter(Boolean).join(' ')
if (!combined) return ['', '', '']
const words = combined.split(/\s+/)
const rows = ['', '', '']
let row = 0
for (const w of words) {
if (row >= 3) break
const test = rows[row] ? rows[row] + ' ' + w : w
if (test.length <= OP_MAX_CHARS) {
rows[row] = test
} else if (row < 2) {
row++
rows[row] = w
}
}
return rows
}renderOperatorDisplay function · javascript · L180-L236 (57 LOC)client/src/components/TinyScreen.jsx
function renderOperatorDisplay(ctx, display, color, previewVisible = true) {
const sentence = display.line1?.left || ''
const previewWord = display.line2?.text || ''
const rows = operatorWordWrap(sentence, previewWord)
// Locate which row ends with the preview word (it's always the last word)
let previewRow = -1
let previewX = MARGIN_X
if (previewWord) {
for (let r = 2; r >= 0; r--) {
if (rows[r] && rows[r].endsWith(previewWord)) {
previewRow = r
const prefixLen = rows[r].length - previewWord.length
previewX = MARGIN_X + prefixLen * FONT_6x10.width
break
}
}
}
ctx.fillStyle = color
for (let r = 0; r < 3; r++) {
if (!rows[r]) continue
if (r === previewRow && previewWord) {
// Draw committed prefix on this row
const prefixLen = rows[r].length - previewWord.length
drawText(ctx, rows[r].substring(0, prefixLen), MARGIN_X, OPERATOR_LINE_Y[r], FONT_6x10)
// Blink: only draw previerenderDisplay function · javascript · L243-L328 (86 LOC)client/src/components/TinyScreen.jsx
function renderDisplay(ctx, display, color, line2Color = null, iconColors = null) {
// Operator sentence mode has completely different layout
if (display.line2?.style === 'operator') {
renderOperatorDisplay(ctx, display, color)
return
}
const { line1, line2, line3, icons } = display
const isLocked = line2.style === 'locked'
// Effective line 2 color: explicit override → locked bright → base color
const l2Color = line2Color !== null ? line2Color
: isLocked ? COLOR_BRIGHT
: color
// === ICON COLUMN ===
if (icons && icons.length === 3) {
for (let i = 0; i < 3; i++) {
const icon = icons[i]
if (icon && icon.id) {
const slotColor = (iconColors && iconColors[i] != null) ? iconColors[i] : ICON_COLOR
drawIcon(ctx, icon.id, ICON_COL_X, ICON_Y[i], slotColor)
}
}
// Draw selection bar next to the active icon slot (only if 2+ icons visible)
const visibleCount = icons.filter(ic => ic && ic.id && ic.id !== 'empty').TinyScreen function · javascript · L334-L526 (193 LOC)client/src/components/TinyScreen.jsx
export default function TinyScreen({ display, compact = false }) {
const canvasRef = useRef(null)
const animRef = useRef(null)
const criticalSeenRef = useRef(new Set()) // keys of critical content already flashed
const prevIconsRef = useRef(null) // previous icon state for change detection
const iconBlinkRef = useRef({}) // { slotIndex: startTimestamp } for active blinks
const renderFrame = useCallback((canvas, style, time) => {
if (!canvas || !display) return
const ctx = canvas.getContext('2d')
// Clear to OLED background
ctx.fillStyle = COLOR_BG
ctx.fillRect(0, 0, W, H)
// Determine draw color based on style
let color = COLOR_NORMAL
if (style === 'abstained') {
color = COLOR_DIM
} else if (style === 'waiting' && time !== undefined) {
// Pulse between dim and normal over 2s
const t = (Math.sin(time / 1000 * Math.PI) + 1) / 2 // 0->1->0 over 2s
const r = Math.round(0x80 + (0xff - 0x80) * t)
const g =Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
TutorialSlidesModal function · javascript · L7-L85 (79 LOC)client/src/components/TutorialSlidesModal.jsx
export default function TutorialSlidesModal({
isOpen,
onClose,
players,
onPushCompSlide,
onPushRoleTipSlide,
onPushItemTipSlide,
}) {
const [expanded, setExpanded] = useState(false);
// Roles currently in the game (pre-assigned in lobby, or active role in game)
const gameRoles = [...new Set(
(players || [])
.map(p => p.preAssignedRole || p.role)
.filter(Boolean)
)];
const rolesToShow = expanded ? AVAILABLE_ROLES : gameRoles;
return (
<Modal isOpen={isOpen} onClose={onClose} title="TUTORIAL SLIDES">
<div className={styles.section}>
<div className={styles.sectionHeader}>
<span className={styles.sectionLabel}>ROLES</span>
{!expanded && gameRoles.length > 0 && (
<span className={styles.sectionHint}>in game</span>
)}
<button
className={styles.expandBtn}
onClick={() => setExpanded(e => !e)}
>
{expanded ? '▲ in game' : '▼ all roles'}
GameProvider function · javascript · L13-L260 (248 LOC)client/src/context/GameContext.jsx
export function GameProvider({ children }) {
const [connected, setConnected] = useState(false);
const [gameState, setGameState] = useState(null);
const [playerState, setPlayerState] = useState(null);
const [slideQueue, setSlideQueue] = useState({ queue: [], currentIndex: -1, current: null });
const [currentSlide, setCurrentSlide] = useState(null);
const [log, setLog] = useState([]);
const [eventPrompt, setEventPrompt] = useState(null);
const [eventResult, setEventResult] = useState(null);
const [notifications, setNotifications] = useState([]);
const [eventTimers, setEventTimers] = useState({});
const [gamePresets, setGamePresets] = useState([]);
const [presetSettings, setPresetSettings] = useState(null);
const [hostSettings, setHostSettings] = useState(null);
const [operatorState, setOperatorState] = useState({ words: [], ready: false });
const [scores, setScores] = useState({});
const [calibrationState, setCalibrationState] = useState(null);
const wsuseGame function · javascript · L262-L268 (7 LOC)client/src/context/GameContext.jsx
export function useGame() {
const context = useContext(GameContext);
if (!context) {
throw new Error('useGame must be used within a GameProvider');
}
return context;
}DebugGrid function · javascript · L13-L199 (187 LOC)client/src/pages/DebugGrid.jsx
export default function DebugGrid() {
const [playerStates, setPlayerStates] = useState({});
const [gameStates, setGameStates] = useState({});
const [eventPrompts, setEventPrompts] = useState({});
const [eventResults, setEventResults] = useState({});
const [connections, setConnections] = useState({});
const wsRefs = useRef({});
const gameStatesRef = useRef({});
// Set page title
useEffect(() => {
document.title = 'Debug - MURDERHOUSE';
}, []);
// Connect all 9 players
useEffect(() => {
const newConnections = {};
for (let i = 1; i <= PLAYER_COUNT; i++) {
const playerId = String(i);
const ws = new WebSocket(WS_URL);
ws.onopen = () => {
console.log(`[Player ${playerId}] Connected`);
ws.send(
JSON.stringify({
type: ClientMsg.JOIN,
payload: { playerId },
})
);
newConnections[playerId] = true;
setConnections({ ...newConnections });
};
ws.onLanding function · javascript · L7-L82 (76 LOC)client/src/pages/Landing.jsx
export default function Landing() {
const { connected, gameState } = useGame();
const players = gameState?.players || [];
return (
<div className={styles.container}>
<div className={styles.content}>
<header className={styles.header}>
<h1>{getStr('landing', 'title')}</h1>
<p className={styles.tagline}>{getStr('landing', 'tagline')}</p>
</header>
<section className={styles.links}>
<div className={styles.linkGroup}>
<h2>Join as Player</h2>
<div className={styles.playerGrid}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => {
const taken = players.some((p) => p.id === String(num));
return (
<Link
key={num}
to={`/player/${num}`}
className={`${styles.playerLink} ${
taken ? styles.taken : ''
}`}
>
<span cbuildDisplay function · javascript · L27-L58 (32 LOC)client/src/pages/Operator.jsx
function buildDisplay(wordIndex, words, ready, cleared) {
const entry = FLAT_WORDS[wordIndex] || FLAT_WORDS[0];
if (cleared) {
return {
line1: { left: '', right: '' },
line2: { text: 'SENT!', style: 'critical' },
line3: { text: '', left: '', right: '', center: '' },
icons: [
{ id: 'empty', state: 'empty' },
{ id: 'empty', state: 'empty' },
{ id: 'empty', state: 'empty' },
],
idleScrollIndex: 0,
};
}
const previewWord = ready ? '' : entry.word;
const catIconId = ready ? 'empty' : entry.icon;
return {
line1: { left: words.join(' '), right: '' },
line2: { text: previewWord, style: 'operator' },
line3: { text: '', left: '', right: '', center: '' },
icons: [
{ id: catIconId, state: 'normal' },
{ id: 'empty', state: 'empty' },
{ id: ready ? 'op_tick' : 'empty', state: ready ? 'normal' : 'empty' },
],
idleScrollIndex: 0,
};
}Operator function · javascript · L62-L277 (216 LOC)client/src/pages/Operator.jsx
export default function Operator() {
const wsRef = useRef(null);
const readyRef = useRef(false); // mutable ref for stale-closure-safe access in onmessage
const yesTimerRef = useRef(null); // pointer long press timers
const noTimerRef = useRef(null);
const keyYesTimerRef = useRef(null); // keyboard long press timers
const keyNoTimerRef = useRef(null);
const [connected, setConnected] = useState(false);
// Server-sync state
const [words, setWords] = useState([]);
const [ready, setReady] = useState(false);
const [cleared, setCleared] = useState(false);
// Local dial state
const [wordIndex, setWordIndex] = useState(0);
const send = useCallback((type, payload = {}) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type, payload }));
}
}, []);
// WebSocket connection
useEffect(() => {
const connect = () => {
const ws = new WebSocket(WS_URL);
wsRef.current = ws;
handleDialUp function · javascript · L127-L130 (4 LOC)client/src/pages/Operator.jsx
function handleDialUp() {
if (ready || cleared) return;
setWordIndex(i => (i - 1 + FLAT_WORDS.length) % FLAT_WORDS.length);
}Repobility (the analyzer behind this table) · https://repobility.com
handleDialDown function · javascript · L132-L135 (4 LOC)client/src/pages/Operator.jsx
function handleDialDown() {
if (ready || cleared) return;
setWordIndex(i => (i + 1) % FLAT_WORDS.length);
}handleYes function · javascript · L137-L140 (4 LOC)client/src/pages/Operator.jsx
function handleYes() {
if (ready || cleared) return;
send(ClientMsg.OPERATOR_ADD, { word: entry.word });
}handleNo function · javascript · L142-L155 (14 LOC)client/src/pages/Operator.jsx
function handleNo() {
if (cleared) return;
if (ready) {
send(ClientMsg.OPERATOR_UNREADY);
return;
}
if (words.length > 0) {
// Jump dial to the position of the word being deleted
const lastWord = words[words.length - 1];
const idx = FLAT_WORDS.findIndex(w => w.word === lastWord);
if (idx !== -1) setWordIndex(idx);
send(ClientMsg.OPERATOR_DELETE);
}
}handleLongYes function · javascript · L157-L160 (4 LOC)client/src/pages/Operator.jsx
function handleLongYes() {
if (ready || cleared) return;
if (words.length > 0) send(ClientMsg.OPERATOR_READY);
}handleLongNo function · javascript · L162-L165 (4 LOC)client/src/pages/Operator.jsx
function handleLongNo() {
if (cleared) return;
if (words.length > 0 || ready) send(ClientMsg.OPERATOR_CLEAR);
}handleYesPointerDown function · javascript · L168-L171 (4 LOC)client/src/pages/Operator.jsx
function handleYesPointerDown() {
if (!yesEnabled) return;
yesTimerRef.current = setTimeout(() => { yesTimerRef.current = null; handleLongYes(); }, LONG_PRESS_MS);
}handleYesPointerUp function · javascript · L172-L174 (3 LOC)client/src/pages/Operator.jsx
function handleYesPointerUp() {
if (yesTimerRef.current) { clearTimeout(yesTimerRef.current); yesTimerRef.current = null; handleYes(); }
}handleYesPointerLeave function · javascript · L175-L177 (3 LOC)client/src/pages/Operator.jsx
function handleYesPointerLeave() {
clearTimeout(yesTimerRef.current); yesTimerRef.current = null;
}All rows scored by the Repobility analyzer (https://repobility.com)
handleNoPointerDown function · javascript · L180-L183 (4 LOC)client/src/pages/Operator.jsx
function handleNoPointerDown() {
if (!noEnabled) return;
noTimerRef.current = setTimeout(() => { noTimerRef.current = null; handleLongNo(); }, LONG_PRESS_MS);
}handleNoPointerUp function · javascript · L184-L186 (3 LOC)client/src/pages/Operator.jsx
function handleNoPointerUp() {
if (noTimerRef.current) { clearTimeout(noTimerRef.current); noTimerRef.current = null; handleNo(); }
}handleNoPointerLeave function · javascript · L187-L189 (3 LOC)client/src/pages/Operator.jsx
function handleNoPointerLeave() {
clearTimeout(noTimerRef.current); noTimerRef.current = null;
}Player function · javascript · L10-L178 (169 LOC)client/src/pages/Player.jsx
export default function Player() {
const { id } = useParams();
const playerId = id;
const {
connected,
gameState,
playerState,
eventPrompt,
eventResult,
notifications,
send,
joinAsPlayer,
rejoinAsPlayer,
} = useGame();
// Set page title
useEffect(() => {
document.title = `Player ${playerId} - MURDERHOUSE`;
}, [playerId]);
// Join game on mount
useEffect(() => {
if (connected && playerId) {
// JOIN handles both new players and reconnection
joinAsPlayer(playerId);
}
}, [connected, playerId, joinAsPlayer]);
// Auto-rejoin after game reset
useEffect(() => {
// If we're connected but have no playerState, and game is in LOBBY, rejoin
if (connected && playerId && !playerState && gameState?.phase === GamePhase.LOBBY) {
console.log('[Player] Detected reset, rejoining...');
joinAsPlayer(playerId);
}
}, [connected, playerId, playerState, gameState?.phase, joinAsPlayer]);
// Handle Screen function · javascript · L26-L153 (128 LOC)client/src/pages/Screen.jsx
export default function Screen() {
const {
connected,
gameState,
currentSlide,
slideQueue,
eventTimers,
connectAsScreen,
} = useGame()
const effectiveSlide = currentSlide || slideQueue?.current
useEffect(() => {
console.log('[Screen] State update:', {
connected,
phase: gameState?.phase,
playerCount: gameState?.players?.length,
currentSlide: currentSlide ? { type: currentSlide.type, id: currentSlide.id } : null,
slideQueueCurrent: slideQueue?.current ? { type: slideQueue.current.type, id: slideQueue.current.id } : null,
effectiveSlide: effectiveSlide ? { type: effectiveSlide.type, id: effectiveSlide.id } : null,
slideQueueLen: slideQueue?.queue?.length,
})
}, [connected, gameState, currentSlide, slideQueue, effectiveSlide])
useEffect(() => {
document.title = 'Screen - MURDERHOUSE'
}, [])
useEffect(() => {
if (connected) connectAsScreen()
}, [connected, connectAsScreen])
const rendeloadOverrides function · javascript · L34-L41 (8 LOC)client/src/pages/SlideEditor.jsx
function loadOverrides() {
try {
const raw = localStorage.getItem(LS_KEY)
return raw ? JSON.parse(raw) : {}
} catch {
return {}
}
}mergeStrings function · javascript · L43-L49 (7 LOC)client/src/pages/SlideEditor.jsx
function mergeStrings(overrides) {
const merged = {}
for (const group of Object.keys(SLIDE_STRINGS)) {
merged[group] = { ...SLIDE_STRINGS[group], ...(overrides[group] || {}) }
}
return merged
}renderSlideComponent function · javascript · L51-L88 (38 LOC)client/src/pages/SlideEditor.jsx
function renderSlideComponent(slideType, variantKey, slide, gameState, strings) {
const players = MOCK_PLAYERS
if (slideType === 'fallback') {
return <FallbackSlide gameState={gameState ?? MOCK_GAME_STATE_LOBBY} strings={strings.fallback} />
}
switch (slideType) {
case SlideType.TITLE:
return <TitleSlide slide={slide} players={players} />
case SlideType.PLAYER_REVEAL:
return <PlayerRevealSlide slide={slide} players={players} />
case SlideType.VOTE_TALLY:
return <VoteTallySlide slide={slide} players={players} />
case SlideType.GALLERY:
return <GallerySlide slide={slide} players={players} gameState={MOCK_GAME_STATE_DAY} strings={strings.gallery} />
case SlideType.COUNTDOWN:
return <CountdownSlide slide={slide} />
case SlideType.DEATH:
return <DeathSlide slide={slide} players={players} strings={strings.death} />
case SlideType.VICTORY:
return <VictorySlide slide={slide} />
case SlideType.COMPOSITION:
Repobility · open methodology · https://repobility.com/research/
useAutoScale function · javascript · L91-L103 (13 LOC)client/src/pages/SlideEditor.jsx
function useAutoScale(wrapperRef, deps) {
useLayoutEffect(() => {
const wrapper = wrapperRef.current
const slide = wrapper?.firstElementChild
if (!slide) return
slide.style.transform = ''
const availableH = wrapper.clientHeight
const naturalH = slide.offsetHeight
if (naturalH > availableH) {
slide.style.transform = `scale(${availableH / naturalH})`
}
}, deps) // eslint-disable-line react-hooks/exhaustive-deps
}SlideEditor function · javascript · L105-L229 (125 LOC)client/src/pages/SlideEditor.jsx
export default function SlideEditor() {
const [selectedType, setSelectedType] = useState(SLIDE_EDITOR_LIST[0].type)
const [selectedVariant, setSelectedVariant] = useState(0)
const [overrides, setOverrides] = useState(loadOverrides)
const [copyFeedback, setCopyFeedback] = useState(false)
const strings = mergeStrings(overrides)
const entryDef = SLIDE_EDITOR_LIST.find(e => e.type === selectedType) || SLIDE_EDITOR_LIST[0]
const variant = entryDef.variants[selectedVariant] || entryDef.variants[0]
const slide = MOCK_SLIDES[variant.key]
const gameState = variant.gameState ?? (selectedType === 'fallback' ? MOCK_GAME_STATE_LOBBY : MOCK_GAME_STATE_DAY)
const wrapperRef = useRef(null)
useAutoScale(wrapperRef, [selectedType, selectedVariant, strings])
// Which string groups to show
const stringGroupKeys = SLIDE_STRING_KEYS[selectedType] || []
function handleStringChange(group, key, value) {
setOverrides(prev => {
const next = { ...prhandleStringChange function · javascript · L124-L130 (7 LOC)client/src/pages/SlideEditor.jsx
function handleStringChange(group, key, value) {
setOverrides(prev => {
const next = { ...prev, [group]: { ...(prev[group] || {}), [key]: value } }
localStorage.setItem(LS_KEY, JSON.stringify(next))
return next
})
}handleReset function · javascript · L132-L135 (4 LOC)client/src/pages/SlideEditor.jsx
function handleReset() {
localStorage.removeItem(LS_KEY)
setOverrides({})
}handleCopyJson function · javascript · L137-L142 (6 LOC)client/src/pages/SlideEditor.jsx
function handleCopyJson() {
navigator.clipboard.writeText(JSON.stringify(strings, null, 2)).then(() => {
setCopyFeedback(true)
setTimeout(() => setCopyFeedback(false), 2000)
})
}handleSelectType function · javascript · L145-L148 (4 LOC)client/src/pages/SlideEditor.jsx
function handleSelectType(type) {
setSelectedType(type)
setSelectedVariant(0)
}loadOverrides function · javascript · L14-L21 (8 LOC)client/src/pages/StringSheets.jsx
function loadOverrides() {
try {
const raw = localStorage.getItem(LS_KEY)
return raw ? JSON.parse(raw) : {}
} catch {
return {}
}
}saveOverrides function · javascript · L23-L25 (3 LOC)client/src/pages/StringSheets.jsx
function saveOverrides(overrides) {
localStorage.setItem(LS_KEY, JSON.stringify(overrides))
}Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
loadTagOverrides function · javascript · L27-L34 (8 LOC)client/src/pages/StringSheets.jsx
function loadTagOverrides() {
try {
const raw = localStorage.getItem(LS_TAGS_KEY)
return raw ? JSON.parse(raw) : {}
} catch {
return {}
}
}saveTagOverrides function · javascript · L36-L38 (3 LOC)client/src/pages/StringSheets.jsx
function saveTagOverrides(tagOverrides) {
localStorage.setItem(LS_TAGS_KEY, JSON.stringify(tagOverrides))
}loadTokenExamples function · javascript · L40-L47 (8 LOC)client/src/pages/StringSheets.jsx
function loadTokenExamples() {
try {
const raw = localStorage.getItem(LS_TOKEN_KEY)
return raw ? JSON.parse(raw) : {}
} catch {
return {}
}
}saveTokenExamples function · javascript · L49-L51 (3 LOC)client/src/pages/StringSheets.jsx
function saveTokenExamples(examples) {
localStorage.setItem(LS_TOKEN_KEY, JSON.stringify(examples))
}renderWithTokens function · javascript · L53-L55 (3 LOC)client/src/pages/StringSheets.jsx
function renderWithTokens(template, tokenValues) {
return template.replace(/\{(\w+)\}/g, (_, key) => tokenValues[`{${key}}`] ?? `{${key}}`)
}escapeCsvField function · javascript · L59-L65 (7 LOC)client/src/pages/StringSheets.jsx
function escapeCsvField(val) {
const s = String(val ?? '')
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return `"${s.replace(/"/g, '""')}"`
}
return s
}buildCsv function · javascript · L67-L81 (15 LOC)client/src/pages/StringSheets.jsx
function buildCsv(catalog, overrides) {
const rows = [CSV_COLUMNS.map(escapeCsvField).join(',')]
for (const entry of catalog) {
const value = overrides[`${entry.cat}.${entry.key}`] ?? entry.default
rows.push([
entry.cat,
entry.key,
entry.default,
value,
(entry.tokens || []).join(' '),
entry.desc || '',
].map(escapeCsvField).join(','))
}
return rows.join('\n')
}parseCsv function · javascript · L83-L115 (33 LOC)client/src/pages/StringSheets.jsx
function parseCsv(text) {
// Returns array of objects keyed by first-row headers
const lines = text.trim().split(/\r?\n/)
if (lines.length < 2) return []
const parseRow = (line) => {
const fields = []
let current = ''
let inQuotes = false
for (let i = 0; i < line.length; i++) {
const ch = line[i]
if (inQuotes) {
if (ch === '"' && line[i + 1] === '"') { current += '"'; i++ }
else if (ch === '"') { inQuotes = false }
else { current += ch }
} else {
if (ch === '"') { inQuotes = true }
else if (ch === ',') { fields.push(current); current = '' }
else { current += ch }
}
}
fields.push(current)
return fields
}
const headers = parseRow(lines[0])
return lines.slice(1).map(line => {
const fields = parseRow(line)
const obj = {}
headers.forEach((h, i) => { obj[h.trim()] = fields[i] ?? '' })
return obj
})
}Repobility (the analyzer behind this table) · https://repobility.com
downloadFile function · javascript · L117-L125 (9 LOC)client/src/pages/StringSheets.jsx
function downloadFile(content, filename, mime) {
const blob = new Blob([content], { type: mime })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}buildJsFile function · javascript · L127-L147 (21 LOC)client/src/pages/StringSheets.jsx
function buildJsFile(catalog, overrides, tagOverrides) {
const lines = [
'// client/src/strings/gameStrings.js',
'// Generated by String Sheets — paste this file to update string defaults.',
'',
'export const STRING_CATALOG = [',
]
for (const entry of catalog) {
const value = overrides[`${entry.cat}.${entry.key}`] ?? entry.default
const tokens = entry.tokens ? `, tokens: ${JSON.stringify(entry.tokens)}` : ''
const effectiveTags = getEffectiveTags(entry, tagOverrides)
const tags = effectiveTags.length ? `, tags: ${JSON.stringify(effectiveTags)}` : ''
lines.push(
` { cat: ${JSON.stringify(entry.cat)}, key: ${JSON.stringify(entry.key)}, default: ${JSON.stringify(value)}${tokens}${tags}, desc: ${JSON.stringify(entry.desc || '')} },`
)
}
const catsJs = STRING_CATEGORIES.map(c => ` { id: ${JSON.stringify(c.id)}, label: ${JSON.stringify(c.label)} }`).join(',\n')
lines.push(']', '', `export const STRING_CATEGORIES = [\n${catsJs},\n]`,getEffectiveTags function · javascript · L150-L153 (4 LOC)client/src/pages/StringSheets.jsx
function getEffectiveTags(entry, tagOverrides) {
const id = `${entry.cat}.${entry.key}`
return tagOverrides[id] ?? entry.tags ?? []
}