Refactoring: port doc generation to generate markdown directly without UIElements

This commit is contained in:
Pieter Vander Vennet 2024-07-12 03:17:15 +02:00
parent 7a7439b161
commit 8e9c03e258
17 changed files with 309 additions and 320 deletions

View file

@ -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(<any>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(<LayerConfigJson>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(<LayerConfigJson>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(<any>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"])
}
}

View file

@ -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")
}
}

View file

@ -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[]) {

View file

@ -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")
}
}

View file

@ -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 <string[]> 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,
])
})

View file

@ -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(<TagRenderingConfigJson[]>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
? "<img src='../warning.svg' height='1rem'/>"
: undefined,
[
"<img src='../warning.svg' height='1rem'/>",
"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(
"<img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'>",
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values",
true
),
Link.OsmWiki(values.key),
]).SetClass("flex"),
[
`<a target="_blank" href='https://taginfo.openstreetmap.org/keys/${ values.key}#values'><img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'></a>]`,
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<OsmTags>({ 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(<TagsFilter>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 = <TagsFilter>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[] {

View file

@ -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 {

View file

@ -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(

View file

@ -5,6 +5,8 @@
*/
import { Utils } from "../Utils"
/* @deprecated
*/
export default abstract class BaseUIElement {
protected _constructedHtmlElement: HTMLElement
protected isDestroyed = false

View file

@ -1,15 +1,12 @@
<script lang="ts">
import Translations from "../i18n/Translations"
import { Utils } from "../../Utils"
import Hotkeys from "../Base/Hotkeys"
import Constants from "../../Models/Constants"
import Tr from "../Base/Tr.svelte"
import Add from "../../assets/svg/Add.svelte"
import Github from "../../assets/svg/Github.svelte"
import DocumentChartBar from "@babeard/svelte-heroicons/outline/DocumentChartBar"
import Mastodon from "../../assets/svg/Mastodon.svelte"
import Liberapay from "../../assets/svg/Liberapay.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import { EyeIcon } from "@rgossiaux/svelte-heroicons/solid"
import MapillaryLink from "./MapillaryLink.svelte"
import OpenJosm from "../Base/OpenJosm.svelte"
@ -18,6 +15,7 @@
import Community from "../../assets/svg/Community.svelte"
import Bug from "../../assets/svg/Bug.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import DocumentChartBar from "@babeard/svelte-heroicons/outline/DocumentChartBar"
export let state: ThemeViewState

View file

@ -0,0 +1,55 @@
<script lang="ts">
import Hotkeys from "../Base/Hotkeys"
import { Translation } from "../i18n/Translation"
import { Utils } from "../../Utils"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
let keys = Hotkeys._docs
const t = Translations.t.hotkeyDocumentation
let byKey = Hotkeys.prepareDocumentation($keys)
$: {
byKey = Hotkeys.prepareDocumentation($keys)
}
</script>
<AccordionSingle>
<div slot="header">
<Tr t={t.title} />
</div>
<Tr t={t.intro} />
<table>
<tr>
<th>
<Tr t={t.key}></Tr>
</th>
<th>
<Tr t={t.action} />
</th>
</tr>
{#each byKey as [key, doc, alsoTriggeredBy] }
<tr>
<td class="flex items-center justify-center">
{#if alsoTriggeredBy}
<div class="flex items-center justify-center gap-x-1">
<div class="literal-code w-fit h-fit">{key}</div>
<div class="literal-code w-fit h-fit">{alsoTriggeredBy}</div>
</div>
{:else}
<div class="literal-code w-fit h-fit flex items-center w-full">{key}</div>
{/if}
</td>
<td>
<Tr t={doc} />
</td>
</tr>
{/each}
</table>
</AccordionSingle>

View file

@ -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<string, string>) {
if (properties.id === "settings") {

View file

@ -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<LicenseInfo>, 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"
)
})
)
}
}

View file

@ -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<ValidatorType, Validator> {

View file

@ -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<string, Promise<WikidataResponse[]>>()
@ -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"],
[
[

View file

@ -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<string, string> {
const dummyLayout = new LayoutConfig({
const dummyLayout = new LayoutConfig(<any>{
id: "&gt;theme&lt;",
title: { en: "<theme>" },
description: "A theme to generate docs with",
@ -59,26 +60,26 @@ export default class QueryParameterDocumentation {
QueryParameters.GetQueryParameter(
"layer-&lt;layer-id&gt;",
"true",
"Wether or not the layer with id <layer-id> is shown"
"Whether the layer with id <layer-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")
}
}

View file

@ -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 @@
<div slot="content0" class="flex flex-col">
<AboutMapComplete {state} />
<div class="m-2 flex flex-col">
<ToSvelte construct={Hotkeys.generateDocumentationDynamic} />
<HotkeyTable/>
</div>
</div>