Files

94 lines
4.3 KiB
JavaScript

import {actions, canonicalValue, defaults, mouseValues, typesForAction, wheelValues} from './mapping.js';
import {keyNames} from './keys.js';
export function validate(entries) {
const errors = [];
const warnings = [];
const bindings = [];
const seen = new Set();
if (!Array.isArray(entries)) return {errors: [{message: 'Configuration must be a list of bindings.'}], warnings, bindings};
entries.forEach((entry, index) => {
const action = canonicalValue(entry?.action, actions);
const type = canonicalValue(entry?.type, ['KEY', 'MOUSE', 'WHEEL']);
let message;
if (!action) message = 'Unknown or empty action.';
else if (!type) message = 'Input type must be KEY, MOUSE or WHEEL.';
else if (!typesForAction(action).includes(type)) message = `${action} cannot use ${type}.`;
const choices = type === 'KEY' ? keyNames : type === 'MOUSE' ? mouseValues : wheelValues;
const value = canonicalValue(entry?.value, choices);
if (!message && !value) message = `Unknown or empty ${type} value.`;
if (!message && /[#;\r\n]/u.test(value)) message = 'This SDL key name cannot be represented in the current INI comment syntax.';
if (message) {
errors.push({index, message: `${entry?.action || 'Binding'}: ${message}`});
return;
}
const binding = {action, type, value};
const identity = `${action}:${type}:${value}`;
if (seen.has(identity)) warnings.push({index, message: `${action}: duplicate ${type}:${value} will be omitted.`});
else bindings.push(binding);
seen.add(identity);
});
const configured = new Set(bindings.map(binding => binding.action));
const sources = new Map();
const effective = bindings.map(binding => ({action: binding.action, source: `${binding.type}:${binding.value}`, builtin: false}));
for (const action of actions) {
if (!configured.has(action)) {
for (const source of defaults[action]) effective.push({action, source, builtin: true});
}
}
for (const binding of effective) {
const owners = sources.get(binding.source) || [];
owners.push(binding);
sources.set(binding.source, owners);
}
for (const [source, owners] of sources) {
if (owners.length > 1 && owners.some(owner => !owner.builtin)) {
warnings.push({message: `${source} controls ${owners.map(owner => `${owner.action}${owner.builtin ? ' (built-in)' : ''}`).join(' and ')}.`});
}
}
return {errors, warnings, bindings};
}
export function serialize(entries) {
const result = validate(entries);
if (result.errors.length) throw new Error(result.errors.map(error => error.message).join('\n'));
const lines = [];
for (const action of actions) {
for (const binding of result.bindings.filter(binding => binding.action === action)) {
lines.push(`${action} = ${binding.type}:${binding.value}`);
}
}
return lines.length ? `${lines.join('\n')}\n` : '';
}
export function parseIni(text) {
if (typeof text !== 'string') throw new TypeError('INI content must be text.');
const entries = [];
const errors = [];
const warnings = [];
const identities = new Set();
text.replace(/^\uFEFF/u, '').split('\n').forEach((line, index) => {
const content = line.split(/[#;]/u, 1)[0].trim();
if (!content) return;
const separator = content.indexOf('=');
const source = content.slice(separator + 1);
const colon = source.indexOf(':');
if (separator === -1 || colon === -1) {
errors.push({line: index + 1, message: 'Expected Action = TYPE:VALUE.'});
return;
}
const entry = {action: content.slice(0, separator), type: source.slice(0, colon), value: source.slice(colon + 1)};
const result = validate([entry]);
if (result.errors.length) {
errors.push(...result.errors.map(error => ({line: index + 1, message: error.message})));
return;
}
const binding = result.bindings[0];
const identity = JSON.stringify(binding);
if (identities.has(identity)) warnings.push({line: index + 1, message: 'Duplicate binding ignored.'});
else entries.push(binding);
identities.add(identity);
});
return {entries, errors, warnings};
}