Merge master

This commit is contained in:
Pieter Vander Vennet 2023-07-28 00:29:21 +02:00
commit 80168f5d0d
919 changed files with 95585 additions and 8504 deletions

View file

@ -0,0 +1,7 @@
import { Validator } from "../Validator"
export default class ColorValidator extends Validator {
constructor() {
super("color", "Shows a color picker")
}
}

View file

@ -0,0 +1,28 @@
import { Validator } from "../Validator"
export default class DateValidator extends Validator {
constructor() {
super("date", "A date with date picker")
}
isValid(str: string): boolean {
return !isNaN(new Date(str).getTime())
}
reformat(str: string) {
console.log("Reformatting", str)
if (!this.isValid(str)) {
// The date is invalid - we return the string as is
return str
}
const d = new Date(str)
let month = "" + (d.getMonth() + 1)
let day = "" + d.getDate()
const year = d.getFullYear()
if (month.length < 2) month = "0" + month
if (day.length < 2) day = "0" + day
return [year, month, day].join("-")
}
}

View file

@ -0,0 +1,29 @@
import IntValidator from "./IntValidator"
export default class DirectionValidator extends IntValidator {
constructor() {
super(
"direction",
[
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl).",
"### Input helper",
"This element has an input helper showing a map and 'viewport' indicating the direction. By default, this map is zoomed to zoomlevel 17, but this can be changed with the first argument",
].join("\n\n")
)
}
isValid(str): boolean {
if (str.endsWith("°")) {
str = str.substring(0, str.length - 1)
}
return super.isValid(str)
}
reformat(str): string {
if (str.endsWith("°")) {
str = str.substring(0, str.length - 1)
}
const n = Number(str) % 360
return "" + n
}
}

View file

@ -0,0 +1,40 @@
import { Translation } from "../../i18n/Translation.js"
import Translations from "../../i18n/Translations.js"
import * as emailValidatorLibrary from "email-validator"
import { Validator } from "../Validator"
export default class EmailValidator extends Validator {
constructor() {
super("email", "An email adress", "email")
}
isValid = (str) => {
if (str === undefined) {
return false
}
str = str.trim()
if (str.startsWith("mailto:")) {
str = str.substring("mailto:".length)
}
return emailValidatorLibrary.validate(str)
}
reformat = (str) => {
if (str === undefined) {
return undefined
}
str = str.trim()
if (str.startsWith("mailto:")) {
str = str.substring("mailto:".length)
}
return str
}
getFeedback(s: string): Translation {
if (s.indexOf("@") < 0) {
return Translations.t.validation.email.noAt
}
return super.getFeedback(s)
}
}

View file

@ -0,0 +1,27 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../Validator"
export default class FloatValidator extends Validator {
inputmode = "decimal"
constructor(name?: string, explanation?: string) {
super(name ?? "float", explanation ?? "A decimal number", "decimal")
}
isValid(str) {
return !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",")
}
reformat(str): string {
return "" + Number(str)
}
getFeedback(s: string): Translation {
if (isNaN(Number(s))) {
return Translations.t.validation.nat.notANumber
}
return undefined
}
}

View file

@ -0,0 +1,39 @@
import UrlValidator from "./UrlValidator"
import { Translation } from "../../i18n/Translation"
export default class ImageUrlValidator extends UrlValidator {
private static readonly allowedExtensions = ["jpg", "jpeg", "svg", "png"]
constructor() {
super(
"image",
"Same as the URL-parameter, except that it checks that the URL ends with `.jpg`, `.png` or some other typical image format"
)
}
private static hasValidExternsion(str: string): boolean {
str = str.toLowerCase()
return ImageUrlValidator.allowedExtensions.some((ext) => str.endsWith(ext))
}
getFeedback(s: string, _?: () => string): Translation | undefined {
const superF = super.getFeedback(s, _)
if (superF) {
return superF
}
if (!ImageUrlValidator.hasValidExternsion(s)) {
return new Translation(
"This URL does not end with one of the allowed extensions. These are: " +
ImageUrlValidator.allowedExtensions.join(", ")
)
}
return undefined
}
isValid(str: string): boolean {
if (!super.isValid(str)) {
return false
}
return ImageUrlValidator.hasValidExternsion(str)
}
}

View file

@ -0,0 +1,29 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../Validator"
export default class IntValidator extends Validator {
constructor(name?: string, explanation?: string) {
super(
name ?? "int",
explanation ?? "A whole number, either positive, negative or zero",
"numeric"
)
}
isValid(str): boolean {
str = "" + str
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))
}
getFeedback(s: string): Translation {
const n = Number(s)
if (isNaN(n)) {
return Translations.t.validation.nat.notANumber
}
if (Math.floor(n) !== n) {
return Translations.t.validation.nat.mustBeWhole
}
return undefined
}
}

View file

@ -0,0 +1,16 @@
import { Validator } from "../Validator"
export default class LengthValidator extends Validator {
constructor() {
super(
"distance",
'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]',
"decimal"
)
}
isValid = (str) => {
const t = Number(str)
return !isNaN(t)
}
}

View file

@ -0,0 +1,30 @@
import IntValidator from "./IntValidator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class NatValidator extends IntValidator {
constructor(name?: string, explanation?: string) {
super(name ?? "nat", explanation ?? "A whole, positive number or zero")
}
isValid(str): boolean {
if (str === undefined) {
return false
}
str = "" + str
return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
}
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
const n = Number(s)
if (n < 0) {
return Translations.t.validation.nat.mustBePositive
}
return undefined
}
}

View file

@ -0,0 +1,54 @@
import Combine from "../../Base/Combine"
import Title from "../../Base/Title"
import Table from "../../Base/Table"
import { Validator } from "../Validator"
export default class OpeningHoursValidator extends Validator {
constructor() {
super(
"opening_hours",
new Combine([
"Has extra elements to easily input when a POI is opened.",
new Title("Helper arguments"),
new Table(
["name", "doc"],
[
[
"options",
new Combine([
"A JSON-object of type `{ prefix: string, postfix: string }`. ",
new Table(
["subarg", "doc"],
[
[
"prefix",
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.",
],
[
"postfix",
"Piece of text that will always be added to the end of the generated opening hours",
],
]
),
]),
],
]
),
new Title("Example usage"),
"To add a conditional (based on time) access restriction:\n\n```\n" +
`
"freeform": {
"key": "access:conditional",
"type": "opening_hours",
"helperArgs": [
{
"prefix":"no @ (",
"postfix":")"
}
]
}` +
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`",
])
)
}
}

View file

@ -0,0 +1,23 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../Validator"
export default class PFloatValidator extends Validator {
constructor() {
super("pfloat", "A positive decimal number or zero")
}
isValid = (str) =>
!isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",")
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
if (Number(s) < 0) {
return Translations.t.validation.nat.mustBePositive
}
return undefined
}
}

View file

@ -0,0 +1,27 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import NatValidator from "./NatValidator"
export default class PNatValidator extends NatValidator {
constructor() {
super("pnat", "A strict positive number")
}
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
if (Number(s) === 0) {
return Translations.t.validation.pnat.noZero
}
return undefined
}
isValid = (str) => {
if (!super.isValid(str)) {
return false
}
return Number(str) > 0
}
}

View file

@ -0,0 +1,54 @@
import { parsePhoneNumberFromString } from "libphonenumber-js"
import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class PhoneValidator extends Validator {
constructor() {
super("phone", "A phone number", "tel")
}
getFeedback(s: string, requestCountry?: () => string): Translation {
if (this.isValid(s, requestCountry)) {
return undefined
}
const tr = Translations.t.validation.phone
const generic = tr.feedback
if (requestCountry) {
const country = requestCountry()
if (country) {
return tr.feedbackCountry.Subs({ country })
}
}
return generic
}
public isValid(str, country: () => string): boolean {
if (str === undefined) {
return false
}
if (str.startsWith("tel:")) {
str = str.substring("tel:".length)
}
let countryCode = undefined
if (country !== undefined) {
countryCode = country()?.toUpperCase()
}
return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false
}
public reformat(str, country: () => string) {
if (str.startsWith("tel:")) {
str = str.substring("tel:".length)
}
let countryCode = undefined
if (country) {
countryCode = country()
}
return parsePhoneNumberFromString(
str,
countryCode?.toUpperCase() as any
)?.formatInternational()
}
}

View file

@ -0,0 +1,51 @@
import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import TagKeyValidator from "./TagKeyValidator"
/**
* Checks that the input conforms `key=value`, where `key` and `value` don't have too much weird characters
*/
export default class SimpleTagValidator extends Validator {
private static readonly KeyValidator = new TagKeyValidator()
constructor() {
super(
"simple_tag",
"A simple tag of the format `key=value` where `key` conforms to a normal key `"
)
}
getFeedback(tag: string, _): Translation | undefined {
const parts = tag.split("=")
if (parts.length < 2) {
return Translations.T("A tag should contain a = to separate the 'key' and 'value'")
}
if (parts.length > 2) {
return Translations.T(
"A tag should contain precisely one `=` to separate the 'key' and 'value', but " +
(parts.length - 1) +
" equal signs were found"
)
}
const [key, value] = parts
const keyFeedback = SimpleTagValidator.KeyValidator.getFeedback(key, _)
if (keyFeedback) {
return keyFeedback
}
if (value.length > 255) {
return Translations.T("A `value should be at most 255 characters")
}
if (value.length == 0) {
return Translations.T("A `value should not be empty")
}
return undefined
}
isValid(tag: string, _): boolean {
return this.getFeedback(tag, _) === undefined
}
}

View file

@ -0,0 +1,7 @@
import { Validator } from "../Validator"
export default class StringValidator extends Validator {
constructor() {
super("string", "A simple piece of text")
}
}

View file

@ -0,0 +1,30 @@
import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class TagKeyValidator extends Validator {
constructor() {
super("key", "Validates a key, mostly that no weird characters are used")
}
getFeedback(key: string, _?: () => string): Translation | undefined {
if (key.length > 255) {
return Translations.T("A `key` should be at most 255 characters")
}
if (key.length == 0) {
return Translations.T("A `key` should not be empty")
}
const keyRegex = /[a-zA-Z0-9:_]+/
if (!key.match(keyRegex)) {
return Translations.T(
"A `key` should only have the characters `a-zA-Z0-9`, `:` or `_`"
)
}
return undefined
}
isValid(key: string, getCountry?: () => string): boolean {
return this.getFeedback(key, getCountry) === undefined
}
}

View file

@ -0,0 +1,12 @@
import { Validator } from "../Validator"
export default class TextValidator extends Validator {
constructor() {
super(
"text",
"A longer piece of text. Uses an textArea instead of a textField",
"text",
true
)
}
}

View file

@ -0,0 +1,16 @@
import { Validator } from "../Validator"
export default class TranslationValidator extends Validator {
constructor() {
super("translation", "Makes sure the the string is of format `Record<string, string>` ")
}
isValid(value: string, getCountry?: () => string): boolean {
try {
JSON.parse(value)
return true
} catch (e) {
return false
}
}
}

View file

@ -0,0 +1,71 @@
import { Validator } from "../Validator"
export default class UrlValidator extends Validator {
constructor(name?: string, explanation?: string) {
super(
name ??"url",
explanation?? "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed",
"url"
)
}
reformat(str: string): string {
try {
let url: URL
// str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763
if (
!str.startsWith("http://") &&
!str.startsWith("https://") &&
!str.startsWith("http:")
) {
url = new URL("https://" + str)
} else {
url = new URL(str)
}
const blacklistedTrackingParams = [
"fbclid", // Oh god, how I hate the fbclid. Let it burn, burn in hell!
"gclid",
"cmpid",
"agid",
"utm",
"utm_source",
"utm_medium",
"campaignid",
"campaign",
"AdGroupId",
"AdGroup",
"TargetId",
"msclkid",
]
for (const dontLike of blacklistedTrackingParams) {
url.searchParams.delete(dontLike.toLowerCase())
}
let cleaned = url.toString()
if (cleaned.endsWith("/") && !str.endsWith("/")) {
// Do not add a trailing '/' if it wasn't typed originally
cleaned = cleaned.substr(0, cleaned.length - 1)
}
return cleaned
} catch (e) {
console.error(e)
return undefined
}
}
isValid(str: string): boolean {
try {
if (
!str.startsWith("http://") &&
!str.startsWith("https://") &&
!str.startsWith("http:")
) {
str = "https://" + str
}
const url = new URL(str)
const dotIndex = url.host.indexOf(".")
return dotIndex > 0 && url.host[url.host.length - 1] !== "."
} catch (e) {
return false
}
}
}

View file

@ -0,0 +1,34 @@
import Combine from "../../Base/Combine"
import Wikidata from "../../../Logic/Web/Wikidata"
import WikidataSearchBox from "../../Wikipedia/WikidataSearchBox"
import { Validator } from "../Validator"
export default class WikidataValidator extends Validator {
constructor() {
super("wikidata", new Combine(["A wikidata identifier, e.g. Q42.", WikidataSearchBox.docs]))
}
public isValid(str): boolean {
if (str === undefined) {
return false
}
if (str.length <= 2) {
return false
}
return !str.split(";").some((str) => Wikidata.ExtractKey(str) === undefined)
}
public reformat(str) {
if (str === undefined) {
return undefined
}
let out = str
.split(";")
.map((str) => Wikidata.ExtractKey(str))
.join("; ")
if (str.endsWith(";")) {
out = out + ";"
}
return out
}
}