diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index 649258cddb..2eb5fdd474 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -31,6 +31,7 @@ import { TagUtils } from "../src/Logic/Tags/TagUtils" import Script from "./Script" import { Changes } from "../src/Logic/Osm/Changes" import TableOfContents from "../src/UI/Base/TableOfContents" +import MarkdownUtils from "../src/Utils/MarkdownUtils" /** * Converts a markdown-file into a .json file, which a walkthrough/slideshow element can use @@ -56,15 +57,15 @@ class ToSlideshowJson { sections.push(currentSection) currentSection = [] } - line = line.replace('src="../../public/', 'src="./') - line = line.replace('src="../../', 'src="./') + line = line.replace("src=\"../../public/", "src=\"./") + line = line.replace("src=\"../../", "src=\"./") currentSection.push(line) } sections.push(currentSection) writeFileSync( this._target, JSON.stringify({ - sections: sections.map((s) => s.join("\n")).filter((s) => s.length > 0), + sections: sections.map((s) => s.join("\n")).filter((s) => s.length > 0) }) ) } @@ -83,7 +84,7 @@ class WikiPageGenerator { generate() { let wikiPage = - '{|class="wikitable sortable"\n' + + "{|class=\"wikitable sortable\"\n" + "! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" + "|-" @@ -140,8 +141,8 @@ export class GenerateDocs extends Script { mkdirSync("./Docs/Themes") } - this.WriteFile("./Docs/Tags_format.md", TagUtils.generateDocs(), [ - "src/Logic/Tags/TagUtils.ts", + this.WriteMarkdownFile("./Docs/Tags_format.md", TagUtils.generateDocs(), [ + "src/Logic/Tags/TagUtils.ts" ]) new ToSlideshowJson( @@ -166,58 +167,30 @@ export class GenerateDocs extends Script { }) this.WriteMarkdownFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), [ - "src/UI/SpecialVisualizations.ts", + "src/UI/SpecialVisualizations.ts" ]) - this.WriteFile( + this.WriteMarkdownFile( "./Docs/CalculatedTags.md", - new Combine([ - new Title("Metatags", 1), + [ + "# Metatags", SimpleMetaTaggers.HelpText(), - ExtraFunctions.HelpText(), - ]).SetClass("flex-col"), + ExtraFunctions.HelpText() + ].join("\n"), ["src/Logic/SimpleMetaTagger.ts", "src/Logic/ExtraFunctions.ts"] ) - this.WriteFile("./Docs/SpecialInputElements.md", Validators.HelpText(), [ - "src/UI/InputElement/Validators.ts", + this.WriteMarkdownFile("./Docs/SpecialInputElements.md", Validators.HelpText(), [ + "src/UI/InputElement/Validators.ts" ]) - this.WriteFile("./Docs/ChangesetMeta.md", Changes.getDocs(), [ + this.WriteMarkdownFile("./Docs/ChangesetMeta.md", Changes.getDocs(), [ "src/Logic/Osm/Changes.ts", - "src/Logic/Osm/ChangesetHandler.ts", + "src/Logic/Osm/ChangesetHandler.ts" ]) new WikiPageGenerator().generate() console.log("Generated docs") } - /** - * @deprecated - */ - private WriteFile( - filename, - html: string | BaseUIElement, - autogenSource: string[], - options?: { - noTableOfContents: boolean - } - ): void { - if (!html) { - return - } - - let md = new Combine([ - Translations.W(html), - "\n\nThis document is autogenerated from " + - autogenSource - .map( - (file) => - `[${file}](https://github.com/pietervdvn/MapComplete/blob/develop/${file})` - ) - .join(", "), - ]).AsMarkdown() - this.WriteMarkdownFile(filename, md, autogenSource, options) - } - private WriteMarkdownFile( filename: string, markdown: string, @@ -254,22 +227,30 @@ export class GenerateDocs extends Script { const warnAutomated = "[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)\n\n" - writeFileSync(filename, warnAutomated + md) + const generatedFrom = + [ + + "This document is autogenerated from", + autogenSource.map(s => `[${s}](https://github.com/pietervdvn/MapComplete/blob/develop/${s})`).join(", ") + ].join(" ") + + + writeFileSync(filename, warnAutomated + md+"\n\n" +generatedFrom+"\n") } private generateHotkeyDocs() { new ThemeViewState(new LayoutConfig(bookcases), new Set()) - this.WriteFile("./Docs/Hotkeys.md", Hotkeys.generateDocumentation(), []) + this.WriteMarkdownFile("./Docs/Hotkeys.md", Hotkeys.generateDocumentation(), ["src/UI/Base/Hotkeys.ts"]) } private generateBuiltinUnits() { const layer = new LayerConfig(unit, "units", true) - const els: (BaseUIElement | string)[] = [new Title(layer.id, 2)] + const els: string[] = ["## " + layer.id] for (const unit of layer.units) { - els.push(new Title(unit.quantity)) + els.push("### " + unit.quantity) for (const denomination of unit.denominations) { - els.push(new Title(denomination.canonical, 4)) + els.push("#### " + denomination.canonical) if (denomination.useIfNoUnitGiven === true) { els.push("*Default denomination*") } else if ( @@ -277,7 +258,7 @@ export class GenerateDocs extends Script { denomination.useIfNoUnitGiven.length > 0 ) { els.push("Default denomination in the following countries:") - els.push(new List(denomination.useIfNoUnitGiven)) + els.push(MarkdownUtils.list(denomination.useIfNoUnitGiven)) } if (denomination.prefix) { els.push("Prefixed") @@ -285,14 +266,14 @@ export class GenerateDocs extends Script { if (denomination.alternativeDenominations.length > 0) { els.push( "Alternative denominations:", - new List(denomination.alternativeDenominations) + MarkdownUtils.list(denomination.alternativeDenominations) ) } } } - this.WriteFile("./Docs/builtin_units.md", new Combine([new Title("Units", 1), ...els]), [ - `assets/layers/unit/unit.json`, + this.WriteMarkdownFile("./Docs/builtin_units.md", ["# Units", ...els].join("\n\n"), [ + `assets/layers/unit/unit.json` ]) } @@ -373,7 +354,7 @@ export class GenerateDocs extends Script { if (inlineSource !== undefined) { source = `assets/themes/${inlineSource}/${inlineSource}.json` } - this.WriteFile("./Docs/Layers/" + layer.id + ".md", element, [source]) + this.WriteMarkdownFile("./Docs/Layers/" + layer.id + ".md", element, [source]) }) } @@ -442,7 +423,7 @@ export class GenerateDocs extends Script { "The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'" ) - this.WriteFile( + this.WriteMarkdownFile( "./Docs/URL_Parameters.md", QueryParameterDocumentation.GenerateQueryParameterDocs(), ["src/Logic/Web/QueryParameters.ts", "src/UI/QueryParameterDocumentation.ts"] @@ -451,7 +432,7 @@ export class GenerateDocs extends Script { private generateBuiltinQuestions() { const qLayer = new LayerConfig(questions, "questions.json", true) - this.WriteFile( + this.WriteMarkdownFile( "./Docs/BuiltinQuestions.md", qLayer.GenerateDocumentation([], new Map(), []), ["assets/layers/questions/questions.json"] @@ -459,27 +440,25 @@ export class GenerateDocs extends Script { } private generateForTheme(theme: LayoutConfig): void { - const el = new Combine([ - new Title( - new Combine([ - theme.title, - "(", - new Link(theme.id, "https://mapcomplete.org/" + theme.id), - ")", - ]), - 2 - ), - theme.description, + const el = [ + ["##", + theme.title, + "(", + `[${theme.id}](https://mapcomplete.org/${theme.id})`, + ")" + ].join(" "), + + theme.description.txt, "This theme contains the following layers:", - new List( + MarkdownUtils.list( theme.layers .filter((l) => !l.id.startsWith("note_import_")) - .map((l) => new Link(l.id, "../Layers/" + l.id + ".md")) + .map((l) => (`[${l.id}](../Layers/${l.id}.md)`)) ), "Available languages:", - new List(theme.language.filter((ln) => ln !== "_context")), - ]).SetClass("flex flex-col") - this.WriteFile( + MarkdownUtils.list(theme.language.filter((ln) => ln !== "_context")) + ].join("\n") + this.WriteMarkdownFile( "./Docs/Themes/" + theme.id + ".md", el, [`assets/themes/${theme.id}/${theme.id}.json`], @@ -533,11 +512,11 @@ export class GenerateDocs extends Script { } } - const el = new Combine([ - new Title("Special and other useful layers", 1), + const el = [ + "# Special and other useful layers", "MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.", - new Title("Priviliged layers", 1), - new List(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")), + "# Priviliged layers", + MarkdownUtils.list(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")), ...Utils.NoNull( Constants.priviliged_layers.map((id) => AllSharedLayers.sharedLayers.get(id)) ).map((l) => @@ -549,15 +528,15 @@ export class GenerateDocs extends Script { Constants.no_include.indexOf(l.id) < 0 ) ), - new Title("Normal layers", 1), + "# Normal layers", "The following layers are included in MapComplete:", - new List( + MarkdownUtils.list( Array.from(AllSharedLayers.sharedLayers.keys()).map( - (id) => new Link(id, "./Layers/" + id + ".md") + (id) => `[${id}](./Layers/${id}.md)` ) - ), - ]) - this.WriteFile("./Docs/BuiltinLayers.md", el, ["src/Customizations/AllKnownLayouts.ts"]) + ) + ].join("\n\n") + this.WriteMarkdownFile("./Docs/BuiltinLayers.md", el, ["src/Customizations/AllKnownLayouts.ts"]) } } diff --git a/src/Logic/ExtraFunctions.ts b/src/Logic/ExtraFunctions.ts index aad9cc4042..20c6723d9b 100644 --- a/src/Logic/ExtraFunctions.ts +++ b/src/Logic/ExtraFunctions.ts @@ -5,6 +5,7 @@ import List from "../UI/Base/List" import Title from "../UI/Base/Title" import { BBox } from "./BBox" import { Feature, Geometry, MultiPolygon, Polygon } from "geojson" +import MarkdownUtils from "../Utils/MarkdownUtils" export interface ExtraFuncParams { /** @@ -517,16 +518,16 @@ export class ExtraFunctions { return record } - public static HelpText(): BaseUIElement { - const elems = [] + public static HelpText(): string { + const elems: string[] = [] for (const func of ExtraFunctions.allFuncs) { - elems.push(new Title(func._name, 3), func._doc, new List(func._args ?? [], true)) + elems.push("### "+func._name, func._doc, MarkdownUtils.list(func._args)) } - return new Combine([ + return [ ExtraFunctions.intro, - new List(ExtraFunctions.allFuncs.map((func) => `[${func._name}](#${func._name})`)), + MarkdownUtils.list(ExtraFunctions.allFuncs.map((func) => `[${func._name}](#${func._name})`)), ...elems, - ]) + ].join("\n") } } diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts index 2ca478825a..f53a9c2391 100644 --- a/src/Logic/Osm/Changes.ts +++ b/src/Logic/Osm/Changes.ts @@ -21,6 +21,7 @@ import ChangeLocationAction from "./Actions/ChangeLocationAction" import ChangeTagAction from "./Actions/ChangeTagAction" import FeatureSwitchState from "../State/FeatureSwitchState" import DeleteAction from "./Actions/DeleteAction" +import MarkdownUtils from "../../Utils/MarkdownUtils" /** * Handles all changes made to OSM. @@ -116,7 +117,7 @@ export class Changes { return changes } - public static getDocs(): BaseUIElement { + public static getDocs(): string { function addSource(items: any[], src: string) { items.forEach((i) => { i["source"] = src @@ -188,24 +189,24 @@ export class Changes { ...ReplaceGeometryAction.metatags, ...SplitAction.metatags,*/ ] - return new Combine([ - new Title("Metatags on a changeset", 1), + return [ + "# Metatags on a changeset", "You might encounter the following metatags on a changeset:", - new Table( + MarkdownUtils.table( ["key", "value", "explanation", "source"], metatagsDocs.map(({ key, value, docs, source, changeType, specialMotivation }) => [ key ?? changeType?.join(", ") ?? "", value, - new Combine([ + [ docs, specialMotivation ? "This might give a reason per modified node or way" : "", - ]), + ].join("\n"), source, ]), ), - ]) + ].join("\n\n") } private static GetNeededIds(changes: ChangeDescription[]) { diff --git a/src/Logic/SimpleMetaTagger.ts b/src/Logic/SimpleMetaTagger.ts index ec7b838b7a..84d4358d8d 100644 --- a/src/Logic/SimpleMetaTagger.ts +++ b/src/Logic/SimpleMetaTagger.ts @@ -766,29 +766,27 @@ export default class SimpleMetaTaggers { return somethingChanged } - public static HelpText(): BaseUIElement { - const subElements: (string | BaseUIElement)[] = [ - new Combine([ + public static HelpText(): string { + const subElements: string[] = [ + [ "Metatags are extra tags available, in order to display more data or to give better questions.", "They are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.", "**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object", - ]).SetClass("flex-col"), + ].join("\n"), ] - subElements.push(new Title("Metatags calculated by MapComplete", 2)) + subElements.push("## Metatags calculated by MapComplete") subElements.push( - new FixedUiElement( "The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme" - ) ) for (const metatag of SimpleMetaTaggers.metatags) { subElements.push( - new Title(metatag.keys.join(", "), 3), + "### "+metatag.keys.join(", "), metatag.doc, metatag.isLazy ? "This is a lazy metatag and is only calculated when needed" : "" ) } - return new Combine(subElements).SetClass("flex-col") + return subElements.join("\n\n") } } diff --git a/src/Models/ThemeConfig/FilterConfig.ts b/src/Models/ThemeConfig/FilterConfig.ts index f23e9943ca..c09c2d9d3c 100644 --- a/src/Models/ThemeConfig/FilterConfig.ts +++ b/src/Models/ThemeConfig/FilterConfig.ts @@ -11,6 +11,7 @@ import { RegexTag } from "../../Logic/Tags/RegexTag" import BaseUIElement from "../../UI/BaseUIElement" import Table from "../../UI/Base/Table" import Combine from "../../UI/Base/Combine" +import MarkdownUtils from "../../Utils/MarkdownUtils" export type FilterConfigOption = { question: Translation osmTags: TagsFilter | undefined @@ -199,20 +200,20 @@ export default class FilterConfig { ) } - public GenerateDocs(): BaseUIElement { + public GenerateDocs(): string { const hasField = this.options.some((opt) => opt.fields?.length > 0) - return new Table( + return MarkdownUtils.table( Utils.NoNull(["id", "question", "osmTags", hasField ? "fields" : undefined]), this.options.map((opt, i) => { const isDefault = this.options.length > 1 && (this.defaultSelection ?? 0) == i - return Utils.NoNull([ + return Utils.NoNull([ this.id + "." + i, isDefault - ? new Combine([opt.question.SetClass("font-bold"), "(default)"]) + ? `*${opt.question.txt}* (default)` : opt.question, - opt.osmTags?.asHumanString(false, false, {}) ?? "", + opt.osmTags?.asHumanString() ?? "", opt.fields?.length > 0 - ? new Combine(opt.fields.map((f) => f.name + " (" + f.type + ")")) + ? (opt.fields.map((f) => f.name + " (" + f.type + ")")).join(" ") : undefined, ]) }) diff --git a/src/Models/ThemeConfig/LayerConfig.ts b/src/Models/ThemeConfig/LayerConfig.ts index 3d7d1a14c8..4e88dc5b9c 100644 --- a/src/Models/ThemeConfig/LayerConfig.ts +++ b/src/Models/ThemeConfig/LayerConfig.ts @@ -14,13 +14,9 @@ import WithContextLoader from "./WithContextLoader" import LineRenderingConfig from "./LineRenderingConfig" import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" import BaseUIElement from "../../UI/BaseUIElement" -import Combine from "../../UI/Base/Combine" -import Title from "../../UI/Base/Title" -import List from "../../UI/Base/List" import Link from "../../UI/Base/Link" import { Utils } from "../../Utils" import { TagsFilter } from "../../Logic/Tags/TagsFilter" -import Table from "../../UI/Base/Table" import FilterConfigJson from "./Json/FilterConfigJson" import { Overpass } from "../../Logic/Osm/Overpass" import { FixedUiElement } from "../../UI/Base/FixedUiElement" @@ -28,6 +24,7 @@ import { ImmutableStore } from "../../Logic/UIEventSource" import { OsmTags } from "../OsmFeature" import Constants from "../Constants" import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson" +import MarkdownUtils from "../../Utils/MarkdownUtils" export default class LayerConfig extends WithContextLoader { public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const @@ -90,7 +87,7 @@ export default class LayerConfig extends WithContextLoader { overpassScript: json.source["overpassScript"], isOsmCache: json.source["isOsmCache"], mercatorCrs: json.source["mercatorCrs"], - idKey: json.source["idKey"], + idKey: json.source["idKey"] }, json.id ) @@ -159,7 +156,7 @@ export default class LayerConfig extends WithContextLoader { let preciseInput: PreciseInput = { preferredBackground: ["photo"], snapToLayers: undefined, - maxSnapDistance: undefined, + maxSnapDistance: undefined } if (pr["preciseInput"] !== undefined) { throw ( @@ -172,7 +169,7 @@ export default class LayerConfig extends WithContextLoader { let snapToLayers = pr.snapToLayer preciseInput = { snapToLayers, - maxSnapDistance: pr.maxSnapDistance ?? 10, + maxSnapDistance: pr.maxSnapDistance ?? 10 } } @@ -184,7 +181,7 @@ export default class LayerConfig extends WithContextLoader { `${translationContext}.presets.${i}.description` ), preciseInput: preciseInput, - exampleImages: pr.exampleImages, + exampleImages: pr.exampleImages } return config }) @@ -306,7 +303,7 @@ export default class LayerConfig extends WithContextLoader { } this.titleIcons = this.ParseTagRenderings(json.titleIcons ?? [], { - readOnlyMode: true, + readOnlyMode: true }) this.title = this.tr("title", undefined, translationContext) @@ -366,8 +363,8 @@ export default class LayerConfig extends WithContextLoader { }[] = [], addedByDefault = false, canBeIncluded = true - ): BaseUIElement { - const extraProps: (string | BaseUIElement)[] = [] + ): string { + const extraProps: string[] = [] extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher") if (canBeIncluded) { @@ -404,13 +401,11 @@ export default class LayerConfig extends WithContextLoader { if (this.source?.geojsonSource !== undefined) { extraProps.push( - new Combine([ - Utils.runningFromConsole - ? "" - : undefined, + [ + "", "This layer is loaded from an external source, namely ", - new FixedUiElement(this.source.geojsonSource).SetClass("code"), - ]) + "`" + this.source.geojsonSource + "`" + ].join("\n\n") ) } } else { @@ -419,44 +414,44 @@ export default class LayerConfig extends WithContextLoader { ) } - let usingLayer: BaseUIElement[] = [] + let usingLayer: string[] = [] if (!addedByDefault) { if (usedInThemes?.length > 0) { usingLayer = [ - new Title("Themes using this layer", 2), - new List( + "## Themes using this layer", + MarkdownUtils.list( (usedInThemes ?? []).map( - (id) => new Link(id, "https://mapcomplete.org/" + id) + (id) => (`[${id}](https://mapcomplete.org/${id})`) ) - ), + ) ] } else if (this.source !== null) { - usingLayer = [new FixedUiElement("No themes use this layer")] + usingLayer = ["No themes use this layer"] } } for (const dep of dependencies) { extraProps.push( - new Combine([ + [ "This layer will automatically load ", - new Link(dep.neededLayer, "./" + dep.neededLayer + ".md"), + (`[${dep.neededLayer}](./${dep.neededLayer}.md)`), " into the layout as it depends on it: ", dep.reason, - "(" + dep.context + ")", - ]) + "(" + dep.context + ")" + ].join(" ") ) } for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) { extraProps.push( - new Combine([ + [ "This layer is needed as dependency for layer", - new Link(revDep, "#" + revDep), - ]) + (`[${revDep}](#${revDep})`) + ].join(" ") ) } - const tableRows = Utils.NoNull( + const tableRows: string[][] = Utils.NoNull( this.tagRenderings .map((tr) => tr.FreeformValues()) .map((values) => { @@ -467,32 +462,28 @@ export default class LayerConfig extends WithContextLoader { Link.OsmWiki(values.key, v, true).SetClass("mr-2") ) ?? ["_no preset options defined, or no values in them_"] return [ - new Combine([ - new Link( - "", - "https://taginfo.openstreetmap.org/keys/" + values.key + "#values", - true - ), - Link.OsmWiki(values.key), - ]).SetClass("flex"), + [ + `]`, + Link.OsmWiki(values.key) + ].join(" "), values.type === undefined ? "Multiple choice" - : new Link(values.type, "../SpecialInputElements.md#" + values.type), - new Combine(embedded).SetClass("flex"), + : `[${values.type}](../SpecialInputElements.md#${values.type})`, + embedded.join(" ") ] }) ) - let quickOverview: BaseUIElement = undefined + let quickOverview: string[] = [] if (tableRows.length > 0) { - quickOverview = new Combine([ - new FixedUiElement("Warning: ").SetClass("bold"), + quickOverview = [ + ("**Warning:**"), "this quick overview is incomplete", - new Table( + MarkdownUtils.table( ["attribute", "type", "values which are supported by this layer"], tableRows - ).SetClass("zebra-table"), - ]).SetClass("flex-col flex") + ) + ] } let iconImg: BaseUIElement = new FixedUiElement("") @@ -503,35 +494,36 @@ export default class LayerConfig extends WithContextLoader { .map( (mr) => mr.RenderIcon(new ImmutableStore({ id: "node/-1" }), { - includeBadges: false, + includeBadges: false }).html ) .find((i) => i !== undefined) } - let overpassLink: BaseUIElement = undefined + let overpassLink: string = undefined if (this.source !== undefined) { try { - overpassLink = new Link( - "Execute on overpass", + overpassLink = ( + "[Execute on overpass](" + Overpass.AsOverpassTurboLink(this.source.osmTags.optimize()) .replaceAll("(", "%28") .replaceAll(")", "%29") + + ")" ) } catch (e) { console.error("Could not generate overpasslink for " + this.id) } } - const filterDocs: (string | BaseUIElement)[] = [] + const filterDocs: (string)[] = [] if (this.filters.length > 0) { - filterDocs.push(new Title("Filters", 4)) + filterDocs.push("#### Filters") filterDocs.push(...this.filters.map((filter) => filter.GenerateDocs())) } - const tagsDescription = [] + const tagsDescription: string[] = [] if (this.source !== null) { - tagsDescription.push(new Title("Basic tags for this layer", 2)) + tagsDescription.push("## Basic tags for this layer") const neededTags = this.source.osmTags.optimize() if (neededTags["and"]) { @@ -549,8 +541,8 @@ export default class LayerConfig extends WithContextLoader { } else { tagsDescription.push( "Elements must match the expression **" + - neededTags.asHumanString(true, false, {}) + - "**" + neededTags.asHumanString(true, false, {}) + + "**" ) } @@ -559,20 +551,19 @@ export default class LayerConfig extends WithContextLoader { tagsDescription.push("This is a special layer - data is not sourced from OpenStreetMap") } - return new Combine([ - new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass( - "flex flex-col" - ), - new List(extraProps), + return [ + [ + "# " + this.id+"\n", + iconImg, + this.description, "\n"].join("\n\n"), + MarkdownUtils.list(extraProps), ...usingLayer, ...tagsDescription, - new Title("Supported attributes", 2), + "## Supported attributes", quickOverview, ...this.tagRenderings.map((tr) => tr.GenerateDocumentation()), - ...filterDocs, - ]) - .SetClass("flex-col") - .SetClass("link-underline") + ...filterDocs + ] .join("\n\n") } public CustomCodeSnippets(): string[] { diff --git a/src/UI/Base/Hotkeys.ts b/src/UI/Base/Hotkeys.ts index e00f9a09f3..f43d1d5ba2 100644 --- a/src/UI/Base/Hotkeys.ts +++ b/src/UI/Base/Hotkeys.ts @@ -3,11 +3,13 @@ import Combine from "./Combine" import BaseUIElement from "../BaseUIElement" import Title from "./Title" import Table from "./Table" -import { UIEventSource } from "../../Logic/UIEventSource" +import { Store, UIEventSource } from "../../Logic/UIEventSource" import { VariableUiElement } from "./VariableUIElement" import { Translation } from "../i18n/Translation" import { FixedUiElement } from "./FixedUiElement" import Translations from "../i18n/Translations" +import MarkdownUtils from "../../Utils/MarkdownUtils" +import Locale from "../i18n/Locale" export default class Hotkeys { public static readonly _docs: UIEventSource< @@ -28,18 +30,18 @@ export default class Hotkeys { public static RegisterHotkey( key: ( | { - ctrl: string - } + ctrl: string + } | { - shift: string - } + shift: string + } | { - alt: string - } + alt: string + } | { - nomod: string - } - ) & { + nomod: string + } + ) & { onUp?: boolean }, documentation: string | Translation, @@ -61,7 +63,7 @@ export default class Hotkeys { return } if (key["ctrl"] !== undefined) { - document.addEventListener("keydown", function (event) { + document.addEventListener("keydown", function(event) { if (event.ctrlKey && event.key === keycode) { if (action() !== false) { event.preventDefault() @@ -69,7 +71,7 @@ export default class Hotkeys { } }) } else if (key["shift"] !== undefined) { - document.addEventListener(type, function (event) { + document.addEventListener(type, function(event) { if (Hotkeys.textElementSelected(event)) { // A text element is selected, we don't do anything special return @@ -81,7 +83,7 @@ export default class Hotkeys { } }) } else if (key["alt"] !== undefined) { - document.addEventListener(type, function (event) { + document.addEventListener(type, function(event) { if (event.altKey && event.key === keycode) { if (action() !== false) { event.preventDefault() @@ -89,7 +91,7 @@ export default class Hotkeys { } }) } else if (key["nomod"] !== undefined) { - document.addEventListener(type, function (event) { + document.addEventListener(type, function(event) { if (Hotkeys.textElementSelected(event) && keycode !== "Escape") { // A text element is selected, we don't do anything special return @@ -104,61 +106,71 @@ export default class Hotkeys { } } - static generateDocumentation(): BaseUIElement { - return new VariableUiElement( - Hotkeys._docs.mapD((docs) => { - let byKey: [string, string | Translation, Translation[] | undefined][] = docs - .map(({ key, documentation, alsoTriggeredBy }) => { - const modifiers = Object.keys(key).filter( - (k) => k !== "nomod" && k !== "onUp" - ) - let keycode: string = - key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] - if (keycode.length == 1) { - keycode = keycode.toUpperCase() - } - if (keycode === " ") { - keycode = "Spacebar" - } - modifiers.push(keycode) - return <[string, string | Translation, Translation[] | undefined]>[ - modifiers.join("+"), - documentation, - alsoTriggeredBy, - ] - }) - .sort() - byKey = Utils.NoNull(byKey) - for (let i = byKey.length - 1; i > 0; i--) { - if (byKey[i - 1][0] === byKey[i][0]) { - byKey.splice(i, 1) - } + static prepareDocumentation(docs: { + key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } + documentation: string | Translation + alsoTriggeredBy: Translation[] + }[]){ + let byKey: [string, string | Translation, Translation[] | undefined][] = docs + .map(({ key, documentation, alsoTriggeredBy }) => { + const modifiers = Object.keys(key).filter( + (k) => k !== "nomod" && k !== "onUp" + ) + let keycode: string = + key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] + if (keycode.length == 1) { + keycode = keycode.toUpperCase() } - const t = Translations.t.hotkeyDocumentation - return new Combine([ - new Title(t.title, 1), - t.intro, - new Table( - [t.key, t.action], - byKey.map(([key, doc, alsoTriggeredBy]) => { - let keyEl: BaseUIElement = new FixedUiElement(key).SetClass( - "literal-code w-fit h-fit" - ) - if (alsoTriggeredBy?.length > 0) { - keyEl = new Combine([keyEl, ...alsoTriggeredBy]).SetClass( - "flex gap-x-4 items-center" - ) - } - return [keyEl, doc] - }) - ), - ]) + if (keycode === " ") { + keycode = "Spacebar" + } + modifiers.push(keycode) + return <[string, string | Translation, Translation[] | undefined]>[ + modifiers.join("+"), + documentation, + alsoTriggeredBy + ] }) - ) + .sort() + byKey = Utils.NoNull(byKey) + for (let i = byKey.length - 1; i > 0; i--) { + if (byKey[i - 1][0] === byKey[i][0]) { + byKey.splice(i, 1) + } + } + return byKey } - static generateDocumentationDynamic(): BaseUIElement { - return new VariableUiElement(Hotkeys._docs.map((_) => Hotkeys.generateDocumentation())) + static generateDocumentationFor(docs: { + key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } + documentation: string | Translation + alsoTriggeredBy: Translation[] + }[], language: string): string { + + const tr = Translations.t.hotkeyDocumentation + function t(t: Translation | string){ + if(typeof t === "string"){ + return t + } + return t.textFor(language) + } + const contents: string[][] = this.prepareDocumentation(docs) + .map(([key, doc, alsoTriggeredBy]) => { + let keyEl: string = [key, ...(alsoTriggeredBy??[])].map(k => "`"+t(k)+"`").join(" ") + return [keyEl, t(doc)] + }) + return [ + "# "+t(tr.title), + t(tr.intro), + MarkdownUtils.table( + [t(tr.key), t(tr.action)], + contents + ) + ].join("\n") + } + + public static generateDocumentation(language?: string){ + return Hotkeys.generateDocumentationFor(Hotkeys._docs.data, language?? Locale.language.data) } private static textElementSelected(event: KeyboardEvent): boolean { diff --git a/src/UI/Base/TableOfContents.ts b/src/UI/Base/TableOfContents.ts index 5a362f83c6..4854023a35 100644 --- a/src/UI/Base/TableOfContents.ts +++ b/src/UI/Base/TableOfContents.ts @@ -98,7 +98,7 @@ export default class TableOfContents { const intro = md.substring(0, firstTitleIndex) const splitPoint = intro.lastIndexOf("\n") - return md.substring(0, splitPoint) + toc + md.substring(splitPoint) + return md.substring(0, splitPoint) +"\n" toc + md.substring(splitPoint) } public static generateStructure( diff --git a/src/UI/BaseUIElement.ts b/src/UI/BaseUIElement.ts index 9a213b79f9..c07d7b3687 100644 --- a/src/UI/BaseUIElement.ts +++ b/src/UI/BaseUIElement.ts @@ -5,6 +5,8 @@ */ import { Utils } from "../Utils" +/* @deprecated + */ export default abstract class BaseUIElement { protected _constructedHtmlElement: HTMLElement protected isDestroyed = false diff --git a/src/UI/BigComponents/AboutMapComplete.svelte b/src/UI/BigComponents/AboutMapComplete.svelte index 06f663a85a..d6ee3caab8 100644 --- a/src/UI/BigComponents/AboutMapComplete.svelte +++ b/src/UI/BigComponents/AboutMapComplete.svelte @@ -1,15 +1,12 @@ + + +
+ +
+ + + + + + + + + {#each byKey as [key, doc, alsoTriggeredBy] } + + + + + + {/each} +
+
+
+ {#if alsoTriggeredBy} +
+ +
{key}
+
{alsoTriggeredBy}
+ +
+ + {:else} +
{key}
+ {/if} +
+
+
diff --git a/src/UI/BigComponents/SelectedElementView.svelte b/src/UI/BigComponents/SelectedElementView.svelte index 128c37a144..f3842b778e 100644 --- a/src/UI/BigComponents/SelectedElementView.svelte +++ b/src/UI/BigComponents/SelectedElementView.svelte @@ -22,7 +22,7 @@ selectedElement.properties.id ) - let isAddNew = tags.mapD(t => t.id.startsWith(LastClickFeatureSource.newPointElementId)) + let isAddNew = tags.mapD(t => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false) function getLayer(properties: Record) { if (properties.id === "settings") { diff --git a/src/UI/Image/Attribution.ts b/src/UI/Image/Attribution.ts deleted file mode 100644 index aa5a017d3e..0000000000 --- a/src/UI/Image/Attribution.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Combine from "../Base/Combine" -import Translations from "../i18n/Translations" -import BaseUIElement from "../BaseUIElement" -import { VariableUiElement } from "../Base/VariableUIElement" -import { Store } from "../../Logic/UIEventSource" -import { LicenseInfo } from "../../Logic/ImageProviders/LicenseInfo" -import { FixedUiElement } from "../Base/FixedUiElement" -import Link from "../Base/Link" - -/** - * Small box in the bottom left of an image, e.g. the image in a popup - */ -export default class Attribution extends VariableUiElement { - constructor(license: Store, icon: BaseUIElement, date?: Date) { - if (license === undefined) { - throw "No license source given in the attribution element" - } - super( - license.map((license: LicenseInfo) => { - if (license === undefined) { - return undefined - } - - let title = undefined - if (license?.title) { - title = Translations.W(license?.title).SetClass("block") - if (license.informationLocation) { - title = new Link(title, license.informationLocation.href, true) - } - } - - return new Combine([ - icon - ?.SetClass("block left") - .SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), - - new Combine([ - title, - Translations.W(license?.artist ?? "").SetClass("block font-bold"), - Translations.W(license?.license ?? license?.licenseShortName), - date === undefined - ? undefined - : new FixedUiElement(date.toLocaleDateString()), - ]).SetClass("flex flex-col"), - ]).SetClass( - "flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg no-images" - ) - }) - ) - } -} diff --git a/src/UI/InputElement/Validators.ts b/src/UI/InputElement/Validators.ts index a7751a80a2..c3a3f332f7 100644 --- a/src/UI/InputElement/Validators.ts +++ b/src/UI/InputElement/Validators.ts @@ -99,15 +99,15 @@ export default class Validators { private static _byType = Validators._byTypeConstructor() - public static HelpText(): BaseUIElement { - const explanations: BaseUIElement[] = Validators.AllValidators.map((type) => - new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col") + public static HelpText(): string { + const explanations: string[] = Validators.AllValidators.flatMap((type) => + ["### "+type.name, type.explanation] ) - return new Combine([ - new Title("Available types for text fields", 1), + return [ + "# Available types for text fields", "The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them", ...explanations, - ]).SetClass("flex flex-col") + ].join("\n") } private static _byTypeConstructor(): Map { diff --git a/src/UI/InputElement/Validators/WikidataValidator.ts b/src/UI/InputElement/Validators/WikidataValidator.ts index d94fe646a9..cdd4fb0241 100644 --- a/src/UI/InputElement/Validators/WikidataValidator.ts +++ b/src/UI/InputElement/Validators/WikidataValidator.ts @@ -1,11 +1,11 @@ import Combine from "../../Base/Combine" import Wikidata, { WikidataResponse } from "../../../Logic/Web/Wikidata" -import WikidataSearchBox from "../../Wikipedia/WikidataSearchBox" import { Validator } from "../Validator" import { Translation } from "../../i18n/Translation" import Translations from "../../i18n/Translations" import Title from "../../Base/Title" import Table from "../../Base/Table" +import MarkdownUtils from "../../../Utils/MarkdownUtils" export default class WikidataValidator extends Validator { public static readonly _searchCache = new Map>() @@ -23,7 +23,7 @@ export default class WikidataValidator extends Validator { "options", new Combine([ "A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.", - new Table( + MarkdownUtils.table( ["subarg", "doc"], [ [ diff --git a/src/UI/QueryParameterDocumentation.ts b/src/UI/QueryParameterDocumentation.ts index 0bb2e115be..fb26a29a51 100644 --- a/src/UI/QueryParameterDocumentation.ts +++ b/src/UI/QueryParameterDocumentation.ts @@ -7,28 +7,29 @@ import { QueryParameters } from "../Logic/Web/QueryParameters" import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" +import MarkdownUtils from "../Utils/MarkdownUtils" export default class QueryParameterDocumentation { - private static QueryParamDocsIntro = [ - new Title("URL-parameters and URL-hash", 1), + private static QueryParamDocsIntro: string[] = [ + "# URL-parameters and URL-hash", "This document gives an overview of which URL-parameters can be used to influence MapComplete.", - new Title("What is a URL parameter?", 2), + "## What is a URL parameter?", '"URL-parameters are extra parts of the URL used to set the state.', "For example, if the url is `https://mapcomplete.org/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, " + "the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely: ", - new List( + MarkdownUtils.list( [ "The url-parameter `lat` is `51.0` in this instance", "The url-parameter `lon` is `4.3` in this instance", "The url-parameter `z` is `5` in this instance", "The url-parameter `test` is `true` in this instance", - ].map((s) => Translations.W(s)) + ] ), "Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.", ] public static UrlParamDocs(): Map { - const dummyLayout = new LayoutConfig({ + const dummyLayout = new LayoutConfig({ id: ">theme<", title: { en: "" }, description: "A theme to generate docs with", @@ -59,26 +60,26 @@ export default class QueryParameterDocumentation { QueryParameters.GetQueryParameter( "layer-<layer-id>", "true", - "Wether or not the layer with id is shown" + "Whether the layer with id is shown" ) return QueryParameters.documentation } - public static GenerateQueryParameterDocs(): BaseUIElement { - const docs: (string | BaseUIElement)[] = [ + public static GenerateQueryParameterDocs(): string { + const docs: string[] = [ ...QueryParameterDocumentation.QueryParamDocsIntro, ...ThemeViewStateHashActor.documentation, ] this.UrlParamDocs().forEach((value, key) => { - const c = new Combine([ - new Title(key, 2), + const c = [ + "## "+key, value, QueryParameters.defaults[key] === undefined ? "No default value set" : `The default value is _${QueryParameters.defaults[key]}_`, - ]) + ].join("\n\n") docs.push(c) }) - return new Combine(docs).SetClass("flex flex-col") + return docs.join("\n\n") } } diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index c93f40e5cb..70a32da8fd 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -74,6 +74,7 @@ import AboutMapComplete from "./BigComponents/AboutMapComplete.svelte" import IfNot from "./Base/IfNot.svelte" import Hotkeys from "./Base/Hotkeys" + import HotkeyTable from "./BigComponents/HotkeyTable.svelte" export let state: ThemeViewState let layout = state.layout @@ -575,7 +576,7 @@
- +