Created
July 3, 2020 08:42
-
-
Save f-space/d8bb4649a6543b405394c78ecb936bbc to your computer and use it in GitHub Desktop.
Parser combinators for plugin commands of RPG Maker MV.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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