Change regex parsing to avoid runaway matches

This commit is contained in:
Pieter Vander Vennet 2022-10-11 01:01:24 +02:00
parent bdcedae003
commit d562e7fd7c
10 changed files with 255 additions and 223 deletions

View file

@ -89,7 +89,8 @@ Regex equals
------------
A tag can also be tested against a regex with `key~regex`. Note that this regex __must match__ the entire value. If the
value is allowed to appear anywhere as substring, use `key~.*regex.*`
value is allowed to appear anywhere as substring, use `key~.*regex.*`.
The regex is put within braces as to prevent runaway values.
Regexes will match the newline character with `.` too - the `s`-flag is enabled by default. To enable case invariant
matching, use `key~i~regex`

View file

@ -40,6 +40,7 @@ export class RegexTag extends TagsFilter {
*
* // A wildcard regextag should only give the key
* new RegexTag("a", /^..*$/).asOverpass() // => [ `["a"]` ]
* new RegexTag("a", /.+/).asOverpass() // => [ `["a"]` ]
*
* // A regextag with a regex key should give correct output
* new RegexTag(/a.*x/, /^..*$/).asOverpass() // => [ `[~"a.*x"~\"^..*$\"]` ]
@ -56,7 +57,7 @@ export class RegexTag extends TagsFilter {
if (this.value instanceof RegExp) {
const src = this.value.source
if (src === "^..*$") {
if (src === "^..*$" || src === ".+") {
// anything goes
return [`[${inv}"${this.key}"]`]
}
@ -251,7 +252,7 @@ export class RegexTag extends TagsFilter {
if (typeof this.value === "string") {
return [{ k: this.key, v: this.value }]
}
if (this.value.toString() != "/^..*$/") {
if (this.value.toString() != "/^..*$/" || this.value.toString() != ".+") {
console.warn("Regex value in tag; using wildcard:", this.key, this.value)
}
return [{ k: this.key, v: undefined }]

View file

@ -1,13 +1,13 @@
import { Tag } from "./Tag"
import { TagsFilter } from "./TagsFilter"
import { And } from "./And"
import { Utils } from "../../Utils"
import {Tag} from "./Tag"
import {TagsFilter} from "./TagsFilter"
import {And} from "./And"
import {Utils} from "../../Utils"
import ComparingTag from "./ComparingTag"
import { RegexTag } from "./RegexTag"
import {RegexTag} from "./RegexTag"
import SubstitutingTag from "./SubstitutingTag"
import { Or } from "./Or"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { isRegExp } from "util"
import {Or} from "./Or"
import {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"
import {isRegExp} from "util"
import * as key_counts from "../../assets/key_totals.json"
type Tags = Record<string, string>
@ -239,23 +239,25 @@ export class TagUtils {
}
/**
* Parses a tag configuration (a json) into a TagsFilter
* Parses a tag configuration (a json) into a TagsFilter.
*
* Note that regexes must match the entire value
*
* TagUtils.Tag("key=value") // => new Tag("key", "value")
* TagUtils.Tag("key=") // => new Tag("key", "")
* TagUtils.Tag("key!=") // => new RegexTag("key", /^..*$/s)
* TagUtils.Tag("key~*") // => new RegexTag("key", /^..*$/s)
* TagUtils.Tag("name~i~somename") // => new RegexTag("name", /^somename$/si)
* TagUtils.Tag("key!=") // => new RegexTag("key", /.+/si)
* TagUtils.Tag("key~*") // => new RegexTag("key", /.+/si)
* TagUtils.Tag("name~i~somename") // => new RegexTag("name", /^(somename)$/si)
* TagUtils.Tag("key!=value") // => new RegexTag("key", "value", true)
* TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^.*bicycle_tube.*$/s)
* TagUtils.Tag("x!~y") // => new RegexTag("x", /^y$/s, true)
* TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^(.*bicycle_tube.*)$/s)
* TagUtils.Tag("x!~y") // => new RegexTag("x", /^(y)$/s, true)
* TagUtils.Tag({"and": ["key=value", "x=y"]}) // => new And([new Tag("key","value"), new Tag("x","y")])
* TagUtils.Tag("name~[sS]peelbos.*") // => new RegexTag("name", /^[sS]peelbos.*$/s)
* TagUtils.Tag("name~[sS]peelbos.*") // => new RegexTag("name", /^([sS]peelbos.*)$/s)
* TagUtils.Tag("survey:date:={_date:now}") // => new SubstitutingTag("survey:date", "{_date:now}")
* TagUtils.Tag("xyz!~\\[\\]") // => new RegexTag("xyz", /^\[\]$/s, true)
* TagUtils.Tag("tags~(.*;)?amenity=public_bookcase(;.*)?") // => new RegexTag("tags", /^(.*;)?amenity=public_bookcase(;.*)?$/s)
* TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^service:bicycle:.*$/, /^..*$/s)
* TagUtils.Tag("_first_comment~.*{search}.*") // => new RegexTag('_first_comment', /^.*{search}.*$/s)
* TagUtils.Tag("xyz!~\\[\\]") // => new RegexTag("xyz", /^(\[\])$/s, true)
* TagUtils.Tag("tags~(.*;)?amenity=public_bookcase(;.*)?") // => new RegexTag("tags", /^((.*;)?amenity=public_bookcase(;.*)?)$/s)
* TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^(service:bicycle:.*)$/, /.+/si)
* TagUtils.Tag("_first_comment~.*{search}.*") // => new RegexTag('_first_comment', /^(.*{search}.*)$/s)
*
* TagUtils.Tag("xyz<5").matchesProperties({xyz: 4}) // => true
* TagUtils.Tag("xyz<5").matchesProperties({xyz: 5}) // => false
@ -266,7 +268,19 @@ export class TagUtils {
*
* // Must match case insensitive
* TagUtils.Tag("name~i~somename").matchesProperties({name: "SoMeName"}) // => true
*
* // Must match the entire value
* TagUtils.Tag("key~value").matchesProperties({key: "valueandsome"}) // => false
* TagUtils.Tag("key~value").matchesProperties({key: "value"}) // => true
* TagUtils.Tag("key~x|y") // => new RegexTag("key", /^(x|y)$/s)
* TagUtils.Tag("maxspeed~[1-9]0|1[0-4]0").matchesProperties({maxspeed: "50 mph"}) // => false
*
* // Must match entire value: with mph
* const regex = TagUtils.Tag("maxspeed~([1-9]0|1[0-4]0) mph")
* regex // => new RegexTag("maxspeed", /^(([1-9]0|1[0-4]0) mph)$/s)
* regex.matchesProperties({maxspeed: "50 mph"}) // => true
*/
public static Tag(json: TagConfigJson, context: string = ""): TagsFilter {
try {
return this.ParseTagUnsafe(json, context)
@ -359,187 +373,9 @@ export class TagUtils {
return null
}
const [_, key, invert, modifier, value] = match
return { key, value, invert: invert == "!", modifier: modifier == "i~" ? "i" : "" }
return {key, value, invert: invert == "!", modifier: modifier == "i~" ? "i" : ""}
}
private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter {
if (json === undefined) {
throw new Error(
`Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`
)
}
if (typeof json != "string") {
if (json["and"] !== undefined && json["or"] !== undefined) {
throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined`
}
if (json["and"] !== undefined) {
return new And(json["and"].map((t) => TagUtils.Tag(t, context)))
}
if (json["or"] !== undefined) {
return new Or(json["or"].map((t) => TagUtils.Tag(t, context)))
}
throw `At ${context}: unrecognized tag: ${JSON.stringify(json)}`
}
const tag = json as string
for (const [operator, comparator] of TagUtils.comparators) {
if (tag.indexOf(operator) >= 0) {
const split = Utils.SplitFirst(tag, operator)
let val = Number(split[1].trim())
if (isNaN(val)) {
val = new Date(split[1].trim()).getTime()
}
const f = (value: string | number | undefined) => {
if (value === undefined) {
return false
}
let b: number
if (typeof value === "number") {
b = value
} else if (typeof b === "string") {
b = Number(value?.trim())
} else {
b = Number(value)
}
if (isNaN(b) && typeof value === "string") {
b = Utils.ParseDate(value).getTime()
if (isNaN(b)) {
return false
}
}
return comparator(b, val)
}
return new ComparingTag(split[0], f, operator + val)
}
}
if (tag.indexOf("~~") >= 0) {
const split = Utils.SplitFirst(tag, "~~")
if (split[1] === "*") {
split[1] = "..*"
}
return new RegexTag(
new RegExp("^" + split[0] + "$"),
new RegExp("^" + split[1] + "$", "s")
)
}
const withRegex = TagUtils.parseRegexOperator(tag)
if (withRegex != null) {
if (withRegex.value === "*" && withRegex.invert) {
throw `Don't use 'key!~*' - use 'key=' instead (empty string as value (in the tag ${tag} while parsing ${context})`
}
if (withRegex.value === "") {
throw (
"Detected a regextag with an empty regex; this is not allowed. Use '" +
withRegex.key +
"='instead (at " +
context +
")"
)
}
let value: string | RegExp = withRegex.value
if (value === "*") {
value = "..*"
}
return new RegexTag(
withRegex.key,
new RegExp("^" + value + "$", "s" + withRegex.modifier),
withRegex.invert
)
}
if (tag.indexOf("!:=") >= 0) {
const split = Utils.SplitFirst(tag, "!:=")
return new SubstitutingTag(split[0], split[1], true)
}
if (tag.indexOf(":=") >= 0) {
const split = Utils.SplitFirst(tag, ":=")
return new SubstitutingTag(split[0], split[1])
}
if (tag.indexOf("!=") >= 0) {
const split = Utils.SplitFirst(tag, "!=")
if (split[1] === "*") {
throw (
"At " +
context +
": invalid tag " +
tag +
". To indicate a missing tag, use '" +
split[0] +
"!=' instead"
)
}
if (split[1] === "") {
split[1] = "..*"
return new RegexTag(split[0], /^..*$/s)
}
return new RegexTag(split[0], split[1], true)
}
if (tag.indexOf("=") >= 0) {
const split = Utils.SplitFirst(tag, "=")
if (split[1] == "*") {
throw `Error while parsing tag '${tag}' in ${context}: detected a wildcard on a normal value. Use a regex pattern instead`
}
return new Tag(split[0], split[1])
}
throw `Error while parsing tag '${tag}' in ${context}: no key part and value part were found`
}
private static GetCount(key: string, value?: string) {
if (key === undefined) {
return undefined
}
const tag = TagUtils.keyCounts.tags[key]
if (tag !== undefined && tag[value] !== undefined) {
return tag[value]
}
return TagUtils.keyCounts.keys[key]
}
private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number {
const rta = a instanceof RegexTag
const rtb = b instanceof RegexTag
if (rta !== rtb) {
// Regex tags should always go at the end: these use a lot of computation at the overpass side, avoiding it is better
if (rta) {
return 1 // b < a
} else {
return -1
}
}
if (a["key"] !== undefined && b["key"] !== undefined) {
if (usePopularity) {
const countA = TagUtils.GetCount(a["key"], a["value"])
const countB = TagUtils.GetCount(b["key"], b["value"])
if (countA !== undefined && countB !== undefined) {
return countA - countB
}
}
if (a["key"] === b["key"]) {
return 0
}
if (a["key"] < b["key"]) {
return -1
}
return 1
}
return 0
}
private static joinL(tfs: TagsFilter[], seperator: string, toplevel: boolean) {
const joined = tfs.map((e) => TagUtils.toString(e, false)).join(seperator)
if (toplevel) {
return joined
}
return " (" + joined + ") "
}
/**
* Returns 'true' is opposite tags are detected.
* Note that this method will never work perfectly
@ -665,4 +501,195 @@ export class TagUtils {
)
return Utils.NoNull(spec)
}
private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter {
if (json === undefined) {
throw new Error(
`Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`
)
}
if (typeof json != "string") {
if (json["and"] !== undefined && json["or"] !== undefined) {
throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined`
}
if (json["and"] !== undefined) {
return new And(json["and"].map((t) => TagUtils.Tag(t, context)))
}
if (json["or"] !== undefined) {
return new Or(json["or"].map((t) => TagUtils.Tag(t, context)))
}
throw `At ${context}: unrecognized tag: ${JSON.stringify(json)}`
}
const tag = json as string
for (const [operator, comparator] of TagUtils.comparators) {
if (tag.indexOf(operator) >= 0) {
const split = Utils.SplitFirst(tag, operator)
let val = Number(split[1].trim())
if (isNaN(val)) {
val = new Date(split[1].trim()).getTime()
}
const f = (value: string | number | undefined) => {
if (value === undefined) {
return false
}
let b: number
if (typeof value === "number") {
b = value
} else if (typeof b === "string") {
b = Number(value?.trim())
} else {
b = Number(value)
}
if (isNaN(b) && typeof value === "string") {
b = Utils.ParseDate(value).getTime()
if (isNaN(b)) {
return false
}
}
return comparator(b, val)
}
return new ComparingTag(split[0], f, operator + val)
}
}
if (tag.indexOf("~~") >= 0) {
const split = Utils.SplitFirst(tag, "~~")
let keyRegex: RegExp;
if (split[0] === "*") {
keyRegex = new RegExp(".+","i")
} else {
keyRegex = new RegExp("^(" + split[0] + ")$")
}
let valueRegex: RegExp
if (split[1] === "*") {
valueRegex = new RegExp(".+", "si")
} else {
valueRegex = new RegExp("^(" + split[1] + ")$", "s")
}
return new RegexTag(
keyRegex,
valueRegex
)
}
const withRegex = TagUtils.parseRegexOperator(tag)
if (withRegex != null) {
if (withRegex.value === "*" && withRegex.invert) {
throw `Don't use 'key!~*' - use 'key=' instead (empty string as value (in the tag ${tag} while parsing ${context})`
}
if (withRegex.value === "") {
throw (
"Detected a regextag with an empty regex; this is not allowed. Use '" +
withRegex.key +
"='instead (at " +
context +
")"
)
}
let value: string | RegExp = withRegex.value
if (value === "*") {
return new RegexTag(
withRegex.key,
new RegExp(".+", "si" + withRegex.modifier),
withRegex.invert
)
}
return new RegexTag(
withRegex.key,
new RegExp("^(" + value + ")$", "s" + withRegex.modifier),
withRegex.invert
)
}
if (tag.indexOf("!:=") >= 0) {
const split = Utils.SplitFirst(tag, "!:=")
return new SubstitutingTag(split[0], split[1], true)
}
if (tag.indexOf(":=") >= 0) {
const split = Utils.SplitFirst(tag, ":=")
return new SubstitutingTag(split[0], split[1])
}
if (tag.indexOf("!=") >= 0) {
const split = Utils.SplitFirst(tag, "!=")
if (split[1] === "*") {
throw (
"At " +
context +
": invalid tag " +
tag +
". To indicate a missing tag, use '" +
split[0] +
"!=' instead"
)
}
if (split[1] === "") {
return new RegexTag(split[0], /.+/si)
}
return new RegexTag(split[0], split[1], true)
}
if (tag.indexOf("=") >= 0) {
const split = Utils.SplitFirst(tag, "=")
if (split[1] == "*") {
throw `Error while parsing tag '${tag}' in ${context}: detected a wildcard on a normal value. Use a regex pattern instead`
}
return new Tag(split[0], split[1])
}
throw `Error while parsing tag '${tag}' in ${context}: no key part and value part were found`
}
private static GetCount(key: string, value?: string) {
if (key === undefined) {
return undefined
}
const tag = TagUtils.keyCounts.tags[key]
if (tag !== undefined && tag[value] !== undefined) {
return tag[value]
}
return TagUtils.keyCounts.keys[key]
}
private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number {
const rta = a instanceof RegexTag
const rtb = b instanceof RegexTag
if (rta !== rtb) {
// Regex tags should always go at the end: these use a lot of computation at the overpass side, avoiding it is better
if (rta) {
return 1 // b < a
} else {
return -1
}
}
if (a["key"] !== undefined && b["key"] !== undefined) {
if (usePopularity) {
const countA = TagUtils.GetCount(a["key"], a["value"])
const countB = TagUtils.GetCount(b["key"], b["value"])
if (countA !== undefined && countB !== undefined) {
return countA - countB
}
}
if (a["key"] === b["key"]) {
return 0
}
if (a["key"] < b["key"]) {
return -1
}
return 1
}
return 0
}
private static joinL(tfs: TagsFilter[], seperator: string, toplevel: boolean) {
const joined = tfs.map((e) => TagUtils.toString(e, false)).join(seperator)
if (toplevel) {
return joined
}
return " (" + joined + ") "
}
}

View file

@ -1,17 +1,15 @@
import { Translation } from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import {Translation} from "../../UI/i18n/Translation"
import {TagsFilter} from "../../Logic/Tags/TagsFilter"
import FilterConfigJson from "./Json/FilterConfigJson"
import Translations from "../../UI/i18n/Translations"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import {TagUtils} from "../../Logic/Tags/TagUtils"
import ValidatedTextField from "../../UI/Input/ValidatedTextField"
import { TagConfigJson } from "./Json/TagConfigJson"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import { FilterState } from "../FilteredLayer"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import { Utils } from "../../Utils"
import { RegexTag } from "../../Logic/Tags/RegexTag"
import BaseUIElement from "../../UI/BaseUIElement"
import { InputElement } from "../../UI/Input/InputElement"
import {TagConfigJson} from "./Json/TagConfigJson"
import {UIEventSource} from "../../Logic/UIEventSource"
import {FilterState} from "../FilteredLayer"
import {QueryParameters} from "../../Logic/Web/QueryParameters"
import {Utils} from "../../Utils"
import {RegexTag} from "../../Logic/Tags/RegexTag"
export default class FilterConfig {
public readonly id: string
@ -133,6 +131,7 @@ export default class FilterConfig {
if (
t.value.source == "^..*$" ||
t.value.source == ".+" ||
t.value.source == "^[\\s\\S][\\s\\S]*$" /*Compiled regex with 'm'*/
) {
return

View file

@ -397,6 +397,10 @@
{
"if": "theme=waste_basket",
"then": "./assets/themes/waste_basket/waste_basket.svg"
},
{
"if": "theme=width",
"then": "./assets/themes/width/icon.svg"
}
]
},

View file

@ -159,7 +159,7 @@ describe("Tag optimalization", () => {
"leisure=picnic_table",
"planned:amenity=charging_station",
"tower:type=observation",
"(amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~^..*$|rental~^.*bicycle.*$) &bicycle_rental!=docking_station",
"(amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~.+|rental~^(.*bicycle.*)$) &bicycle_rental!=docking_station",
"leisure=playground&playground!=forest",
]

View file

@ -7594,7 +7594,7 @@ describe("GenerateCache", () => {
}
mkdirSync(dir + "np-cache")
initDownloads(
"(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*foot.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*hiking.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*bycicle.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*horse.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3Bnwr%5B%22drinking_water%22%3D%22yes%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B"
"(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*foot.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*hiking.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*bycicle.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*horse.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3Bnwr%5B%22drinking_water%22%3D%22yes%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B"
)
await main([
"natuurpunt",

View file

@ -26,7 +26,7 @@ export const mochaHooks = {
JSON.stringify(url),
", \n",
" ",
JSON.stringify(data),
// JSON.stringify(data),
"\n )\n------------------\n\n"
)
throw new Error(