Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
barsdeveloper committed Jan 21, 2024
2 parents b09238e + 7a33b38 commit 9e26061
Show file tree
Hide file tree
Showing 20 changed files with 1,612 additions and 586 deletions.
708 changes: 444 additions & 264 deletions dist/parsernostrum.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/parsernostrum.min.js

Large diffs are not rendered by default.

114 changes: 85 additions & 29 deletions src/Parsernostrum.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@ export default class Parsernostrum {
/** @type {(new (parser: Parser<any>) => Parsernostrum<typeof parser>) & typeof Parsernostrum} */
Self

static lineColumnFromOffset(string, offset) {
const lines = string.substring(0, offset).split('\n')
const line = lines.length
const column = lines[lines.length - 1].length + 1
return { line, column }
}
/** @param {[any, ...any]|RegExpExecArray} param0 */
static #firstElementGetter = ([v, _]) => v
/** @param {[any, any, ...any]|RegExpExecArray} param0 */
static #secondElementGetter = ([_, v]) => v
static #arrayFlatter = ([first, rest]) => [first, ...rest]
/** @param {any} v */
static #joiner = v =>
v instanceof Array
? v.join("")
: v
/**
* @template T
* @param {T} v
* @returns {T extends Array ? String : T}
*/
// @ts-expect-error
static #joiner = v => v instanceof Array ? v.join("") : v
static #createEscapeable = character => String.raw`[^${character}\\]*(?:\\.[^${character}\\]*)*`
static #numberRegex = /[-\+]?(?:\d*\.)?\d+/

Expand Down Expand Up @@ -89,8 +97,6 @@ export default class Parsernostrum {

/** @param {T} parser */
constructor(parser, optimized = false) {
// @ts-expect-error
this.Self = this.constructor
this.#parser = parser
}

Expand All @@ -103,18 +109,65 @@ export default class Parsernostrum {
* @returns {Result<ParserValue<T>>}
*/
run(input) {
const result = this.#parser.parse(Reply.makeContext(this, input), 0)
return result.status && result.position === input.length ? result : Reply.makeFailure(result.position)
const result = this.#parser.parse(Reply.makeContext(this, input), 0, Reply.makePathNode(this.#parser))
if (result.position !== input.length) {
result.status = false
}
return result
}

/**
* @param {String} input
* @throws when the parser fails to match
* @throws {Error} when the parser fails to match
*/
parse(input) {
const result = this.run(input)
if (!result.status) {
throw new Error(`Could not parse "${input.length > 20 ? input.substring(0, 17) + "..." : input}"`)
const chunkLength = 60
const chunkRange = /** @type {[Number, Number]} */(
[Math.ceil(chunkLength / 2), Math.floor(chunkLength / 2)]
)
const position = Parsernostrum.lineColumnFromOffset(input, result.bestPosition)
let bestPosition = result.bestPosition
const inlineInput = input.replaceAll(
/^(\s)+|\s{6,}|\s*?\n\s*/g,
(m, startingSpace, offset) => {
let replaced = startingSpace ? "..." : " ... "
if (offset <= result.bestPosition) {
if (result.bestPosition < offset + m.length) {
bestPosition -= result.bestPosition - offset
} else {
bestPosition -= m.length - replaced.length
}
}
return replaced
}
)
const string = inlineInput.substring(0, chunkLength).trimEnd()
const leadingWhitespaceLength = Math.min(
input.substring(result.bestPosition - chunkRange[0]).match(/^\s*/)[0].length,
chunkRange[0] - 1,
)
let offset = Math.min(bestPosition, chunkRange[0] - leadingWhitespaceLength)
chunkRange[0] = Math.max(0, bestPosition - chunkRange[0]) + leadingWhitespaceLength
chunkRange[1] = Math.min(input.length, chunkRange[0] + chunkLength)
let segment = inlineInput.substring(...chunkRange)
if (chunkRange[0] > 0) {
segment = "..." + segment
offset += 3
}
if (chunkRange[1] < inlineInput.length - 1) {
segment = segment + "..."
}
throw new Error(
`Could not parse: ${string}\n\n`
+ `Input: ${segment}\n`
+ " " + " ".repeat(offset)
+ `^ From here (line: ${position.line}, column: ${position.column}, offset: ${result.bestPosition})${result.bestPosition === input.length ? ", end of string" : ""}\n\n`
+ (result.bestParser ? "Last valid parser matched:" : "No parser matched:")
+ this.toString(1, true, result.bestParser)
+ "\n"
)
}
return result.value
}
Expand Down Expand Up @@ -187,13 +240,9 @@ export default class Parsernostrum {
return new this(new LazyParser(parser))
}

/**
* @param {Number} min
* @returns {Parsernostrum<TimesParser<T>>}
*/
/** @param {Number} min */
times(min, max = min) {
// @ts-expect-error
return new this.Self(new TimesParser(this.#parser, min, max))
return new Parsernostrum(new TimesParser(this.#parser, min, max))
}

many() {
Expand All @@ -213,24 +262,24 @@ export default class Parsernostrum {
/** @returns {Parsernostrum<T?>} */
opt() {
// @ts-expect-error
return this.Self.alt(this, this.Self.success())
return Parsernostrum.alt(this, Parsernostrum.success())
}

/**
* @template {Parsernostrum<Parser<any>>} P
* @param {P} separator
*/
sepBy(separator, allowTrailing = false) {
const results = this.Self.seq(
const results = Parsernostrum.seq(
this,
this.Self.seq(separator, this).map(Parsernostrum.#secondElementGetter).many()
Parsernostrum.seq(separator, this).map(Parsernostrum.#secondElementGetter).many()
)
.map(Parsernostrum.#arrayFlatter)
return results
}

skipSpace() {
return this.Self.seq(this, this.Self.whitespaceOpt).map(Parsernostrum.#firstElementGetter)
return Parsernostrum.seq(this, Parsernostrum.whitespaceOpt).map(Parsernostrum.#firstElementGetter)
}

/**
Expand All @@ -240,34 +289,41 @@ export default class Parsernostrum {
*/
map(fn) {
// @ts-expect-error
return new this.Self(new MapParser(this.#parser, fn))
return new Parsernostrum(new MapParser(this.#parser, fn))
}

/**
* @template {Parsernostrum<any>} P
* @param {(v: ParserValue<T>, input: String, position: Number) => P} fn
*/
chain(fn) {
return new this.Self(new ChainedParser(this.#parser, fn))
return new Parsernostrum(new ChainedParser(this.#parser, fn))
}

/**
* @param {(v: ParserValue<T>, input: String, position: Number) => boolean} fn
* @return {Parsernostrum<T>}
*/
assert(fn) {
return /** @type {Parsernostrum<T>} */(this.chain((v, input, position) => fn(v, input, position)
? this.Self.success().map(() => v)
: this.Self.failure()
))
// @ts-expect-error
return this.chain((v, input, position) => fn(v, input, position)
? Parsernostrum.success().map(() => v)
: Parsernostrum.failure()
)
}

join(value = "") {
return this.map(Parsernostrum.#joiner)
}

toString(indent = 0, newline = false) {
/** @param {Parsernostrum<Parser<any>> | Parser<any> | PathNode} highlight */
toString(indent = 0, newline = false, highlight = null) {
if (highlight instanceof Parsernostrum) {
highlight = highlight.getParser()
}
const context = Reply.makeContext(this, "")
context.highlighted = highlight
return (newline ? "\n" + Parser.indentation.repeat(indent) : "")
+ this.#parser.toString(Reply.makeContext(this, ""), indent)
+ this.#parser.toString(context, indent, Reply.makePathNode(this.#parser))
}
}
51 changes: 28 additions & 23 deletions src/Reply.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
/**
* @template Value
* @typedef {{
* status: Boolean,
* value: Value,
* position: Number,
* }} Result
*/

export default class Reply {

/**
* @template Value
* @template T
* @param {Number} position
* @param {Value} value
* @param {T} value
* @param {PathNode} bestPath
* @returns {Result<T>}
*/
static makeSuccess(position, value) {
return /** @type {Result<Value>} */({
static makeSuccess(position, value, bestPath = null, bestPosition = 0) {
return {
status: true,
value: value,
position: position,
})
bestParser: bestPath,
bestPosition: bestPosition,
}
}

/**
* @template Value
* @param {Number} position
* @param {PathNode} bestPath
* @returns {Result<null>}
*/
static makeFailure(position) {
return /** @type {Result<Value>} */({
static makeFailure(position = 0, bestPath = null, bestPosition = 0) {
return {
status: false,
value: null,
position: position,
})
position,
bestParser: bestPath,
bestPosition: bestPosition,
}
}

/** @param {Parsernostrum<Parser<any>>} parsernostrum */
static makeContext(parsernostrum = null, input = "") {
return /** @type {Context} */({
parsernostrum: parsernostrum,
input: input,
visited: new Map(),
parsernostrum,
input,
highlighted: null,
})
}

static makePathNode(parser, index = 0, previous = null) {
return /** @type {PathNode} */({
parent: previous,
parser,
index,
})
}
}
2 changes: 1 addition & 1 deletion src/grammars/MathGrammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default class MathGrammar {
static expressionFragment = P.alt(
P.seq(
MathGrammar.#termFragment,
MathGrammar.#opFragment
MathGrammar.#opFragment,
).map(([term, fragment]) => [...term, ...fragment]),
MathGrammar.#number.map(v => [v]),
)
Expand Down
70 changes: 43 additions & 27 deletions src/parser/AlternativeParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import SuccessParser from "./SuccessParser.js"
*/
export default class AlternativeParser extends Parser {

static highlightRegexp = new RegExp(
// Matches the beginning of a row containing Parser.highlight only when after the first row of an alternative
String.raw`(?<=[^\S\n]*\| .*\n)^(?=[^\S\n]*\^+ ${Parser.highlight}(?:\n|$))`,
"m"
)

#parsers
get parsers() {
return this.#parsers
Expand All @@ -20,55 +26,65 @@ export default class AlternativeParser extends Parser {
this.#parsers = parsers
}

unwrap() {
return [...this.#parsers]
}

/**
* @template {Parser<any>[]} T
* @param {T} parsers
* @returns {AlternativeParser<T>}
*/
wrap(...parsers) {
// @ts-expect-error
const result = /** @type {AlternativeParser<T>} */(new this.Self(...parsers))
return result
}

/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
*/
parse(context, position) {
let result
parse(context, position, path) {
const result = Reply.makeSuccess(0, /** @type {ParserValue<T>} */(""))
for (let i = 0; i < this.#parsers.length; ++i) {
result = this.#parsers[i].parse(context, position)
if (result.status) {
const outcome = this.#parsers[i].parse(
context,
position,
{ parent: path, parser: this.#parsers[i], index: i }
)
if (outcome.bestPosition > result.bestPosition) {
result.bestParser = outcome.bestParser
result.bestPosition = outcome.bestPosition
}
if (outcome.status) {
result.value = outcome.value
result.position = outcome.position
return result
}
}
return Reply.makeFailure(position)
result.status = false
result.value = null
return result
}

/**
* @protected
* @param {Context} context
* @param {Number} indent
* @param {PathNode} path
*/
doToString(context, indent = 0) {
doToString(context, indent, path) {
const indentation = Parser.indentation.repeat(indent)
const deeperIndentation = Parser.indentation.repeat(indent + 1)
if (this.#parsers.length === 2 && this.#parsers[1] instanceof SuccessParser) {
let result = this.#parsers[0].toString(context, indent)
if (!(this.#parsers[0] instanceof StringParser) && !context.visited.has(this.#parsers[0])) {
let result = this.#parsers[0].toString(
context,
indent,
{ parent: path, parser: this.#parsers[0], index: 0 }
)
if (!(this.#parsers[0] instanceof StringParser)) {
result = "<" + result + ">"
}
result += "?"
return result
}
return "ALT<\n"
+ deeperIndentation + this.#parsers
.map(p => p.toString(context, indent + 1))
.join("\n" + deeperIndentation + "| ")
let serialized = this.#parsers
.map((parser, index) => parser.toString(context, indent + 1, { parent: path, parser, index }))
.join("\n" + deeperIndentation + "| ")
if (context.highlighted) {
serialized = serialized.replace(AlternativeParser.highlightRegexp, " ")
}
let result = "ALT<\n"
+ (this.isHighlighted(context, path) ? `${indentation}^^^ ${Parser.highlight}\n` : "")
+ deeperIndentation + serialized
+ "\n" + indentation + ">"
return result
}
}
Loading

0 comments on commit 9e26061

Please sign in to comment.