cloudtube/utils/matcher.js

121 lines
3.9 KiB
JavaScript

const {Parser} = require("./parser")
const constants = require("./constants")
class PatternCompileError extends Error {
constructor(position, message) {
super(message)
this.position = position
}
}
class PatternRuntimeError extends Error {
}
class Matcher {
constructor(pattern) {
this.pattern = pattern
this.compiled = null
this.anchors = null
}
compilePattern() {
// Calculate anchors (starts or ends with -- to allow more text)
this.anchors = {start: true, end: true}
if (this.pattern.startsWith("--")) {
this.anchors.start = false
this.pattern = this.pattern.slice(2)
}
if (this.pattern.endsWith("--")) {
this.anchors.end = false
this.pattern = this.pattern.slice(0, -2)
}
this.compiled = []
// Check if the pattern is a regular expression, only if regexp filters are enabled by administrator
if (this.pattern.match(/^\/.*\/$/) && constants.server_setup.allow_regexp_filters) {
this.compiled.push({
type: "regexp",
expr: new RegExp(this.pattern.slice(1, -1), "i")
})
return // do not proceed to step-by-step
}
// Step-by-step pattern compilation
const patternParser = new Parser(this.pattern.toLowerCase())
while (patternParser.hasRemaining()) {
if (patternParser.swallow("[") > 0) { // there is a special command
let index = patternParser.seek("]")
if (index === -1) {
throw new PatternCompileError(patternParser.cursor, "Command is missing closing square bracket")
}
let command = patternParser.get({split: "]"})
let args = command.split("|")
if (args[0] === "digits") {
this.compiled.push({type: "regexp", expr: /\d+/})
} else if (args[0] === "choose") {
this.compiled.push({type: "choose", choices: args.slice(1).sort((a, b) => (b.length - a.length))})
} else {
throw new PatternCompileError(patternParser.cursor - command.length - 1 + args[0].length, `Unknown command name: \`${args[0]}\``)
}
} else { // no special command
let next = patternParser.get({split: "["})
this.compiled.push({type: "text", text: next})
if (patternParser.hasRemaining()) patternParser.cursor-- // rewind to before the [
}
}
}
match(string) {
if (this.compiled === null) {
throw new Error("Pattern was not compiled before matching. Compiling must be done explicitly.")
}
const stringParser = new Parser(string.toLowerCase())
let flexibleStart = !this.anchors.start
for (const fragment of this.compiled) {
if (fragment.type === "text") {
let index = stringParser.seek(fragment.text, {moveToMatch: true}) // index, and move to, start of match
if (index === -1) return false
if (index !== 0 && !flexibleStart) return false // allow matching anywhere if flexible start
stringParser.cursor += fragment.text.length // move to end of match.
}
else if (fragment.type === "regexp") {
const match = stringParser.remaining().match(fragment.expr)
if (!match) return false
if (match.index !== 0 && !flexibleStart) return false // allow matching anywhere if flexible start
stringParser.cursor += match.index + match[0].length
}
else if (fragment.type === "choose") {
const ok = fragment.choices.some(choice => {
let index = stringParser.seek(choice)
if (index === -1) return false // try next choice
if (index !== 0 && !flexibleStart) return false // try next choice
// otherwise, good enough for us! /shrug
stringParser.cursor += index + choice.length
return true
})
if (!ok) return false
}
else {
throw new PatternRuntimeError(`Unknown fragment type ${fragment.type}`)
}
flexibleStart = false // all further sequences must be anchored to the end of the last one.
}
if (stringParser.hasRemaining() && this.anchors.end) {
return false // pattern did not end when expected
}
return true
}
}
module.exports.Matcher = Matcher
module.exports.PatternCompileError = PatternCompileError
module.exports.PatternRuntimeError = PatternRuntimeError