Skip to content

Instantly share code, notes, and snippets.

@f-space
Created July 3, 2020 08:42
Show Gist options
  • Save f-space/d8bb4649a6543b405394c78ecb936bbc to your computer and use it in GitHub Desktop.
Save f-space/d8bb4649a6543b405394c78ecb936bbc to your computer and use it in GitHub Desktop.
Parser combinators for plugin commands of RPG Maker MV.
const Parser = (() => {
const isOk = result => result.error === undefined;
const succeed = value => (_, i) => ({ value, position: i });
const fail = error => () => ({ error });
const value = (context, parse) => (args, i) => {
if (i < args.length) {
const result = parse(args[i]);
if (result !== undefined) {
const { value } = result;
const position = i + 1;
return { value, position };
} else {
const type = 'format';
const start = i;
const end = i + 1;
return { error: { type, context, start, end } };
}
} else {
const type = 'eol';
const start = i;
return { error: { type, context, start } };
}
};
const end = () => (args, i) =>
i === args.length
? { value: null, position: i }
: { error: { type: 'excess', start: i } };
const not = parser => (args, i) => {
const result = parser(args, i);
return isOk(result)
? { error: { type: 'not', value: result.value, start: i, end: result.position } }
: { value: result.error, position: i };
};
const andThen = (parser, fn) => (args, i) => {
const result = parser(args, i);
return isOk(result) ? fn(result.value)(args, result.position) : result;
};
const orElse = (parser, fn) => (args, i) => {
const result = parser(args, i);
return isOk(result) ? result : fn(result.error)(args, i);
};
const map = (parser, fn) => andThen(parser, value => succeed(fn(value)));
const mapError = (parser, fn) => orElse(parser, error => fail(fn(error)));
const string = () => value("string", value => ({ value }));
const keyword = s => value(`'${s}'`, x => x === s ? { value: s } : undefined);
const number = () => value("number", x => {
const value = Number(x);
return !Number.isNaN(value) ? { value } : undefined;
});
const integer = () => value("integer", x => {
const RE = /^(?:-?[1-9][0-9]*|0|0b[0-1]+|0o[0-7]+|0x[0-9a-f]+)$/i;
return RE.test(x) ? { value: Number(x) } : undefined;
});
const decimal = () =>
value("decimal", x => /^(?:-?[1-9][0-9]*|0)$/.test(x) ? { value: Number.parseInt(x, 10) } : undefined);
const natural = () =>
value("natural", x => /^(?:[1-9][0-9]*|0)$/.test(x) ? { value: Number.parseInt(x, 10) } : undefined);
const boolean = () => value("boolean", x => {
return x === 'true' ? { value: true } : x === 'false' ? { value: false } : undefined;
});
const re = (context, pattern, fn) => value(context, x => {
const match = x.match(pattern);
return match !== null ? { value: fn(match) } : undefined;
});
const subdivide = (context, splitter, parser) => value(context, s => {
const result = parser(splitter(s), 0);
return isOk(result) ? { value: result.value } : undefined;
});
const array = parser => {
const merger = value => map(parser, x => [...value, x]);
const scanner = value => orElse(andThen(merger(value), scanner), () => succeed(value));
return scanner([]);
};
const tuple = parsers => {
const merger = parser => value => map(parser, x => [...value, x]);
const reducer = (merged, parser) => andThen(merged, merger(parser));
return parsers.reduce(reducer, succeed([]));
};
const oneOf = parsers => {
const order = error => error.start !== undefined ? error.start : -Infinity;
const select = (a, b) => order(a) >= order(b) ? a : b;
const merger = parser => error => mapError(parser, x => select(error, x));
const reducer = (merged, parser) => orElse(merged, merger(parser));
return parsers.reduce(reducer, none());
};
const none = () => (_, i) => ({ error: { type: 'none', start: i } });
const where = (parser, condition, predicate) =>
andThen(parser, value => predicate(value)
? succeed(value)
: (_, i) => ({ error: { type: 'condition', condition, value, end: i } })
);
const terminate = parser => andThen(parser, value => map(end(), () => value));
const commands = definitions =>
oneOf(definitions.map(({ name, parser }) =>
andThen(keyword(name), () => map(parser, value => {
const args = typeof value === 'object' && value !== null && !Array.isArray(value) ? value : { value };
return { ...args, command: name };
}))
));
const options = definitions =>
map(
array(oneOf(definitions.map(({ name, alias, parser }) => {
const aliases = Array.isArray(alias) ? alias : alias !== undefined ? [alias] : [];
const keywords = [`--${name}`, ...aliases];
return andThen(oneOf(keywords.map(keyword)), () => map(parser, value => ({ [name]: value })));
}))),
list => Object.assign(
{},
...definitions.map(({ name, default: value }) => ({ [name]: value })),
...list,
)
);
const arguments_ = definitions =>
map(
tuple(definitions.map(({ name, optional, parser }) => {
const opt = Boolean(optional);
const def = typeof optional === 'object' ? optional.default : undefined;
return map(
orElse(parser, error => opt ? succeed(def) : fail(error)),
value => ({ [name]: value })
);
})),
list => Object.assign({}, ...list)
);
const build = (parser, formatter = defaultErrorFormatter) => (command, args) => {
const result = terminate(parser)(args, 0);
return isOk(result) ? { value: result.value } : { error: formatter(result.error, command, args) };
};
const defaultErrorFormatter = (error, command, args) => {
const { start, end } = error;
const print = value => {
const MAX_LENGTH = 32;
const s = typeof value === 'object' ? JSON.stringify(value) : String(value);
return s.length > MAX_LENGTH ? `${s.slice(0, MAX_LENGTH)}...` : s;
};
const message = () => {
switch (error.type) {
case 'format': return `failed to parse ${error.context} value`;
case 'eol': return `reached end of command line while parsing ${error.context} value`;
case 'excess': return `detected excess arguments`;
case 'not': return `cannot accept "${print(error.value)}"`;
case 'none': return `didn't match any patterns`;
case 'condition': return `"${print(error.value)}" unsatisfy condition ${print(error.condition)}`;
default: return "unexpected error occurred";
}
};
const decorate = args => {
if (start !== undefined && end !== undefined) {
return start !== end
? [...args.slice(0, start), "***", ...args.slice(start, end), "***", ...args.slice(end)]
: [...args.slice(0, start), "<*>", ...args.slice(end)];
} else if (start !== undefined) {
return [...args.slice(0, start), "|>", ...args.slice(start)];
} else if (end !== undefined) {
return [...args.slice(0, end), "<|", ...args.slice(end)];
} else {
return args;
}
};
const format = () => [command, ...decorate(args)].join(" ");
return `${message()}\n${format()}`;
};
return {
succeed,
fail,
value,
end,
not,
andThen,
orElse,
map,
mapError,
string,
keyword,
number,
integer,
decimal,
natural,
boolean,
re,
subdivide,
array,
tuple,
oneOf,
none,
where,
terminate,
commands,
options,
arguments: arguments_,
build,
defaultErrorFormatter,
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment