From ec1c206f8411e57406e0eb501663d9b4d9a60e25 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 7 Jun 2022 03:35:55 +0200 Subject: [PATCH] Add 'visit'-functionality to TagsFilter, add case invariant regextag --- Docs/Tags_format.md | 15 +++-- Logic/Tags/And.ts | 5 ++ Logic/Tags/ComparingTag.ts | 8 +-- Logic/Tags/Or.ts | 5 ++ Logic/Tags/RegexTag.ts | 14 +++-- Logic/Tags/SubstitutingTag.ts | 8 +-- Logic/Tags/Tag.ts | 12 +++- Logic/Tags/TagUtils.ts | 114 +++++++++++++++++++++------------- Logic/Tags/TagsFilter.ts | 19 +++--- 9 files changed, 131 insertions(+), 69 deletions(-) diff --git a/Docs/Tags_format.md b/Docs/Tags_format.md index 83ff0c392c..9c2231c657 100644 --- a/Docs/Tags_format.md +++ b/Docs/Tags_format.md @@ -57,6 +57,9 @@ 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.*` +Regexes will match the newline character with `.` too - the `s`-flag is enabled by default. +To enable case invariant matching, use `key~i~regex` + Equivalently, `key!~regex` can be used if you _don't_ want to match the regex in order to appear. @@ -81,13 +84,15 @@ which we do not want. To mitigate this, use: ```json -"mappings": [ { - "if":"key:={some_other_key}" - "then": "...", - "hideInAnswer": "some_other_key=" + "mappings": [ + { + "if":"key:={some_other_key}" + "then": "...", + "hideInAnswer": "some_other_key=" + } + ] } -] ``` One can use `key!:=prefix-{other_key}-postfix` as well, to match if `key` is _not_ the same diff --git a/Logic/Tags/And.ts b/Logic/Tags/And.ts index 25799da1ce..e98e92e71c 100644 --- a/Logic/Tags/And.ts +++ b/Logic/Tags/And.ts @@ -5,6 +5,7 @@ import {Tag} from "./Tag"; import {RegexTag} from "./RegexTag"; export class And extends TagsFilter { + public and: TagsFilter[] constructor(and: TagsFilter[]) { @@ -373,5 +374,9 @@ export class And extends TagsFilter { return !this.and.some(t => !t.isNegative()); } + visit(f: (TagsFilter: any) => void) { + f(this) + this.and.forEach(sub => sub.visit(f)) + } } \ No newline at end of file diff --git a/Logic/Tags/ComparingTag.ts b/Logic/Tags/ComparingTag.ts index fa543350fe..8abbdbb161 100644 --- a/Logic/Tags/ComparingTag.ts +++ b/Logic/Tags/ComparingTag.ts @@ -52,10 +52,6 @@ export default class ComparingTag implements TagsFilter { return []; } - AsJson() { - return this.asHumanString(false, false, {}) - } - optimize(): TagsFilter | boolean { return this; } @@ -63,4 +59,8 @@ export default class ComparingTag implements TagsFilter { isNegative(): boolean { return true; } + + visit(f: (TagsFilter) => void) { + f(this) + } } \ No newline at end of file diff --git a/Logic/Tags/Or.ts b/Logic/Tags/Or.ts index b2aa7ec27c..d8b4feda79 100644 --- a/Logic/Tags/Or.ts +++ b/Logic/Tags/Or.ts @@ -261,6 +261,11 @@ export class Or extends TagsFilter { return this.or.some(t => t.isNegative()); } + visit(f: (TagsFilter: any) => void) { + f(this) + this.or.forEach(t => t.visit(f)) + } + } diff --git a/Logic/Tags/RegexTag.ts b/Logic/Tags/RegexTag.ts index a5db5a77f8..91dfb06a0f 100644 --- a/Logic/Tags/RegexTag.ts +++ b/Logic/Tags/RegexTag.ts @@ -43,6 +43,9 @@ export class RegexTag extends TagsFilter { * * // A regextag with a regex key should give correct output * new RegexTag(/a.*x/, /^..*$/).asOverpass() // => [ `[~"a.*x"~\"^..*$\"]` ] + * + * // A regextag with a case invariant flag should signal this to overpass + * new RegexTag("key", /^.*value.*$/i).asOverpass() // => [ `["key"~\"^.*value.*$\",i]` ] */ asOverpass(): string[] { const inv =this.invert ? "!" : "" @@ -57,7 +60,8 @@ export class RegexTag extends TagsFilter { // anything goes return [`[${inv}"${this.key}"]`] } - return [`["${this.key}"${inv}~"${src}"]`] + const modifier = this.value.ignoreCase ? ",i" : "" + return [`["${this.key}"${inv}~"${src}"${modifier}]`] }else{ // Normal key and normal value return [`["${this.key}"${inv}="${this.value}"]`]; @@ -256,10 +260,6 @@ export class RegexTag extends TagsFilter { return [] } - AsJson() { - return this.asHumanString() - } - optimize(): TagsFilter | boolean { return this; } @@ -267,4 +267,8 @@ export class RegexTag extends TagsFilter { isNegative(): boolean { return this.invert; } + + visit(f: (TagsFilter) => void) { + f(this) + } } \ No newline at end of file diff --git a/Logic/Tags/SubstitutingTag.ts b/Logic/Tags/SubstitutingTag.ts index 0cb941f140..ccc097956d 100644 --- a/Logic/Tags/SubstitutingTag.ts +++ b/Logic/Tags/SubstitutingTag.ts @@ -82,10 +82,6 @@ export default class SubstitutingTag implements TagsFilter { return [{k: this._key, v: v}]; } - AsJson() { - return this._key + (this._invert ? '!' : '') + "=" + this._value - } - optimize(): TagsFilter | boolean { return this; } @@ -93,4 +89,8 @@ export default class SubstitutingTag implements TagsFilter { isNegative(): boolean { return false; } + + visit(f: (TagsFilter: any) => void) { + f(this) + } } \ No newline at end of file diff --git a/Logic/Tags/Tag.ts b/Logic/Tags/Tag.ts index f8ee64aa00..92f470b4fa 100644 --- a/Logic/Tags/Tag.ts +++ b/Logic/Tags/Tag.ts @@ -1,11 +1,10 @@ import {Utils} from "../../Utils"; -import {RegexTag} from "./RegexTag"; import {TagsFilter} from "./TagsFilter"; + export class Tag extends TagsFilter { public key: string public value: string - constructor(key: string, value: string) { super() this.key = key @@ -23,6 +22,8 @@ export class Tag extends TagsFilter { /** + * imort + * * const tag = new Tag("key","value") * tag.matchesProperties({"key": "value"}) // => true * tag.matchesProperties({"key": "z"}) // => false @@ -89,6 +90,9 @@ export class Tag extends TagsFilter { } /** + * + * import {RegexTag} from "./RegexTag"; + * * // should handle advanced regexes * new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true * new Tag("key","value").shadows(new RegexTag("key", /^..*$/, true)) // => false @@ -129,4 +133,8 @@ export class Tag extends TagsFilter { isNegative(): boolean { return false; } + + visit(f: (TagsFilter) => void) { + f(this) + } } \ No newline at end of file diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index 064c9cab32..884e81e4ae 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -57,10 +57,10 @@ export class TagUtils { } /*** - * Creates a hash {key --> [values : string | Regex ]}, with all the values present in the tagsfilter + * Creates a hash {key --> [values : string | RegexTag ]}, with all the values present in the tagsfilter */ static SplitKeys(tagsFilters: TagsFilter[], allowRegex = false) { - const keyValues = {} // Map string -> string[] + const keyValues = {} // Map string -> (string | RegexTag)[] tagsFilters = [...tagsFilters] // copy all, use as queue while (tagsFilters.length > 0) { const tagsFilter = tagsFilters.shift(); @@ -200,20 +200,29 @@ export class TagUtils { * * TagUtils.Tag("key=value") // => new Tag("key", "value") * TagUtils.Tag("key=") // => new Tag("key", "") - * TagUtils.Tag("key!=") // => new RegexTag("key", /^..*$/) - * TagUtils.Tag("key~*") // => new RegexTag("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!=value") // => new RegexTag("key", "value", true) - * TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^.*bicycle_tube.*$/) - * TagUtils.Tag("x!~y") // => new RegexTag("x", /^y$/, 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.*$/) + * 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", /^\[\]$/, true) - * TagUtils.Tag("tags~(.*;)?amenity=public_bookcase(;.*)?") // => new RegexTag("tags", /^(.*;)?amenity=public_bookcase(;.*)?$/) - * TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^service:bicycle:.*$/, /^..*$/) + * 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<5").matchesProperties({xyz: 4}) // => true * TagUtils.Tag("xyz<5").matchesProperties({xyz: 5}) // => false + * + * // RegexTags must match values with newlines + * TagUtils.Tag("note~.*aed.*").matchesProperties({note: "Hier bevindt zich wss een defibrillator. \\n\\n De aed bevindt zich op de 5de verdieping"}) // => true + * TagUtils.Tag("note~i~.*aed.*").matchesProperties({note: "Hier bevindt zich wss een defibrillator. \\n\\n De AED bevindt zich op de 5de verdieping"}) // => true + * + * // Must match case insensitive + * TagUtils.Tag("name~i~somename").matchesProperties({name: "SoMeName"}) // => true */ public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter { try { @@ -247,6 +256,33 @@ export class TagUtils { return r } + /** + * Parses the various parts of a regex tag + * + * TagUtils.parseRegexOperator("key~value") // => {invert: false, key: "key", value: "value", modifier: ""} + * TagUtils.parseRegexOperator("key!~value") // => {invert: true, key: "key", value: "value", modifier: ""} + * TagUtils.parseRegexOperator("key~i~value") // => {invert: false, key: "key", value: "value", modifier: "i"} + * TagUtils.parseRegexOperator("key!~i~someweirdvalue~qsdf") // => {invert: true, key: "key", value: "someweirdvalue~qsdf", modifier: "i"} + * TagUtils.parseRegexOperator("_image:0~value") // => {invert: false, key: "_image:0", value: "value", modifier: ""} + * TagUtils.parseRegexOperator("key~*") // => {invert: false, key: "key", value: "*", modifier: ""} + * TagUtils.parseRegexOperator("Brugs volgnummer~*") // => {invert: false, key: "Brugs volgnummer", value: "*", modifier: ""} + * TagUtils.parseRegexOperator("socket:USB-A~*") // => {invert: false, key: "socket:USB-A", value: "*", modifier: ""} + * TagUtils.parseRegexOperator("tileId~*") // => {invert: false, key: "tileId", value: "*", modifier: ""} + */ + public static parseRegexOperator(tag: string): { + invert: boolean; + key: string; + value: string; + modifier: "i" | ""; + } | null { + const match = tag.match(/^([_a-zA-Z0-9: -]+)(!)?~([i]~)?(.*)$/); + if (match == null) { + return null; + } + const [_, key, invert, modifier, value] = match; + return {key, value, invert: invert == "!", modifier: (modifier == "i~" ? "i" : "")}; + } + private static TagUnsafe(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter { if (json === undefined) { @@ -299,18 +335,7 @@ export class TagUtils { return new ComparingTag(split[0], f, operator + val) } } - - if (tag.indexOf("!~") >= 0) { - const split = Utils.SplitFirst(tag, "!~"); - if (split[1] === "*") { - throw `Don't use 'key!~*' - use 'key=' instead (empty string as value (in the tag ${tag} while parsing ${context})` - } - return new RegexTag( - split[0], - new RegExp("^"+ split[1]+"$"), - true - ); - } + if (tag.indexOf("~~") >= 0) { const split = Utils.SplitFirst(tag, "~~"); if (split[1] === "*") { @@ -318,9 +343,30 @@ export class TagUtils { } return new RegexTag( new RegExp("^" + split[0] + "$"), - new RegExp("^" + split[1] + "$") + 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); @@ -337,7 +383,7 @@ export class TagUtils { } if (split[1] === "") { split[1] = "..*" - return new RegexTag(split[0], /^..*$/) + return new RegexTag(split[0], /^..*$/s) } return new RegexTag( split[0], @@ -345,22 +391,8 @@ export class TagUtils { true ); } - if (tag.indexOf("~") >= 0) { - const split = Utils.SplitFirst(tag, "~"); - let value : string | RegExp = split[1] - if (split[1] === "") { - throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")" - } - if (value === "*") { - value = /^..*$/ - }else { - value = new RegExp("^"+value+"$") - } - return new RegexTag( - split[0], - value - ); - } + + if (tag.indexOf("=") >= 0) { @@ -511,7 +543,5 @@ export class TagUtils { public static containsEquivalents( guards: TagsFilter[], listToFilter: TagsFilter[] ) : boolean { return listToFilter.some(tf => guards.some(guard => guard.shadows(tf))) } - - } \ No newline at end of file diff --git a/Logic/Tags/TagsFilter.ts b/Logic/Tags/TagsFilter.ts index a99251bb2a..e02739c7ba 100644 --- a/Logic/Tags/TagsFilter.ts +++ b/Logic/Tags/TagsFilter.ts @@ -20,7 +20,7 @@ export abstract class TagsFilter { * Returns all normal key/value pairs * Regex tags, substitutions, comparisons, ... are exempt */ - abstract usedTags(): {key: string, value: string}[]; + abstract usedTags(): { key: string, value: string }[]; /** * Converts the tagsFilter into a list of key-values that should be uploaded to OSM. @@ -34,22 +34,27 @@ export abstract class TagsFilter { * Returns an optimized version (or self) of this tagsFilter */ abstract optimize(): TagsFilter | boolean; - + /** * Returns 'true' if the tagsfilter might select all features (i.e. the filter will return everything from OSM, except a few entries). - * + * * A typical negative tagsfilter is 'key!=value' - * + * * import {RegexTag} from "./RegexTag"; * import {Tag} from "./Tag"; * import {And} from "./And"; - * import {Or} from "./Or"; - * + * import {Or} from "./Or"; + * * new Tag("key","value").isNegative() // => false * new And([new RegexTag("key","value", true)]).isNegative() // => true * new Or([new RegexTag("key","value", true), new Tag("x","y")]).isNegative() // => true * new And([new RegexTag("key","value", true), new Tag("x","y")]).isNegative() // => false */ abstract isNegative(): boolean - + + /** + * Walks the entire tree, every tagsFilter will be passed into the function once + */ + abstract visit(f: ((TagsFilter) => void)); + } \ No newline at end of file