From d562e7fd7ccb750323e084b6839fb8743aa54b60 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 11 Oct 2022 01:01:24 +0200 Subject: [PATCH] Change regex parsing to avoid runaway matches --- Docs/Tags_format.md | 3 +- Logic/Tags/RegexTag.ts | 5 +- Logic/Tags/TagUtils.ts | 423 ++++++++++-------- Models/ThemeConfig/FilterConfig.ts | 21 +- .../osm_community_index/license_info.json | 2 +- .../mapcomplete-changes.json | 6 +- .../osm_community_index/license_info.json | 2 +- test/Logic/Tags/OptimizeTags.spec.ts | 12 +- test/scripts/GenerateCache.spec.ts | 2 +- test/testhooks.ts | 2 +- 10 files changed, 255 insertions(+), 223 deletions(-) diff --git a/Docs/Tags_format.md b/Docs/Tags_format.md index 681b243a3..0e9d9b0aa 100644 --- a/Docs/Tags_format.md +++ b/Docs/Tags_format.md @@ -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` diff --git a/Logic/Tags/RegexTag.ts b/Logic/Tags/RegexTag.ts index e68e89b09..988f9987c 100644 --- a/Logic/Tags/RegexTag.ts +++ b/Logic/Tags/RegexTag.ts @@ -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 }] diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index c4bebaa18..60bb1e0c2 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -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 @@ -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 + ") " + } } diff --git a/Models/ThemeConfig/FilterConfig.ts b/Models/ThemeConfig/FilterConfig.ts index 8c8efa85f..e67cba117 100644 --- a/Models/ThemeConfig/FilterConfig.ts +++ b/Models/ThemeConfig/FilterConfig.ts @@ -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 diff --git a/assets/layers/osm_community_index/license_info.json b/assets/layers/osm_community_index/license_info.json index 822410b4c..0dac25a16 100644 --- a/assets/layers/osm_community_index/license_info.json +++ b/assets/layers/osm_community_index/license_info.json @@ -9,4 +9,4 @@ "https://github.com/osmlab/osm-community-index/blob/main/dist/img/osm.svg" ] } -] +] \ No newline at end of file diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index 52d6257bf..c661ab2b9 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -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" } ] }, @@ -557,4 +561,4 @@ } } ] -} +} \ No newline at end of file diff --git a/assets/themes/osm_community_index/license_info.json b/assets/themes/osm_community_index/license_info.json index 822410b4c..0dac25a16 100644 --- a/assets/themes/osm_community_index/license_info.json +++ b/assets/themes/osm_community_index/license_info.json @@ -9,4 +9,4 @@ "https://github.com/osmlab/osm-community-index/blob/main/dist/img/osm.svg" ] } -] +] \ No newline at end of file diff --git a/test/Logic/Tags/OptimizeTags.spec.ts b/test/Logic/Tags/OptimizeTags.spec.ts index 928b813d9..1e864ecd5 100644 --- a/test/Logic/Tags/OptimizeTags.spec.ts +++ b/test/Logic/Tags/OptimizeTags.spec.ts @@ -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", ] @@ -237,8 +237,8 @@ describe("Tag optimalization", () => { /* > When the first OR is written out, this becomes - club=climbing - OR + club=climbing + OR (sport=climbing&(office~* | club~*)) OR (sport=climbing & leisure=sports_centre) @@ -248,9 +248,9 @@ describe("Tag optimalization", () => { /* > We can join the 'sport=climbing' in the last 3 phrases - club=climbing - OR - (sport=climbing AND + club=climbing + OR + (sport=climbing AND (office~* | club~*)) OR (leisure=sports_centre) diff --git a/test/scripts/GenerateCache.spec.ts b/test/scripts/GenerateCache.spec.ts index 8dd702d4e..7ee670c44 100644 --- a/test/scripts/GenerateCache.spec.ts +++ b/test/scripts/GenerateCache.spec.ts @@ -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", diff --git a/test/testhooks.ts b/test/testhooks.ts index 178964476..c36defeec 100644 --- a/test/testhooks.ts +++ b/test/testhooks.ts @@ -26,7 +26,7 @@ export const mochaHooks = { JSON.stringify(url), ", \n", " ", - JSON.stringify(data), + // JSON.stringify(data), "\n )\n------------------\n\n" ) throw new Error(