2023-07-17 22:04:35 +02:00
import Combine from "../src/UI/Base/Combine"
import BaseUIElement from "../src/UI/BaseUIElement"
2023-12-12 03:46:51 +01:00
import { existsSync , mkdirSync , readFileSync , writeFileSync } from "fs"
2023-07-17 22:04:35 +02:00
import { AllKnownLayouts } from "../src/Customizations/AllKnownLayouts"
import TableOfContents from "../src/UI/Base/TableOfContents"
import SimpleMetaTaggers from "../src/Logic/SimpleMetaTagger"
import SpecialVisualizations from "../src/UI/SpecialVisualizations"
import { ExtraFunctions } from "../src/Logic/ExtraFunctions"
import Title from "../src/UI/Base/Title"
import QueryParameterDocumentation from "../src/UI/QueryParameterDocumentation"
2022-01-29 02:45:59 +01:00
import ScriptUtils from "./ScriptUtils"
2023-07-17 22:04:35 +02:00
import List from "../src/UI/Base/List"
import Translations from "../src/UI/i18n/Translations"
2023-07-20 13:28:38 +02:00
import themeOverview from "../src/assets/generated/theme_overview.json"
2023-07-17 22:04:35 +02:00
import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"
2023-07-20 13:28:38 +02:00
import bookcases from "../src/assets/generated/themes/bookcases.json"
2023-02-08 01:14:21 +01:00
import fakedom from "fake-dom"
2023-12-12 03:46:51 +01:00
import unit from "../src/assets/generated/layers/unit.json"
2023-07-17 22:04:35 +02:00
import Hotkeys from "../src/UI/Base/Hotkeys"
import { QueryParameters } from "../src/Logic/Web/QueryParameters"
import Link from "../src/UI/Base/Link"
import Constants from "../src/Models/Constants"
import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import DependencyCalculator from "../src/Models/ThemeConfig/DependencyCalculator"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
import ThemeViewState from "../src/Models/ThemeViewState"
import Validators from "../src/UI/InputElement/Validators"
2023-07-20 13:28:38 +02:00
import questions from "../src/assets/generated/layers/questions.json"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
2023-07-28 00:29:21 +02:00
import { Utils } from "../src/Utils"
import { TagUtils } from "../src/Logic/Tags/TagUtils"
2023-12-12 03:46:51 +01:00
import Script from "./Script"
2023-03-02 05:20:53 +01:00
/ * *
2023-12-12 03:46:51 +01:00
* Converts a markdown - file into a . json file , which a walkthrough / slideshow element can use
*
* These are used in the studio
2023-03-02 05:20:53 +01:00
* /
2023-12-12 03:46:51 +01:00
class ToSlideshowJson {
private readonly _source : string
private readonly _target : string
2023-03-02 05:20:53 +01:00
2023-12-12 03:46:51 +01:00
constructor ( source : string , target : string ) {
this . _source = source
this . _target = target
2023-03-02 05:20:53 +01:00
}
2023-12-12 03:46:51 +01:00
public convert() {
const lines = readFileSync ( this . _source , "utf8" ) . split ( "\n" )
2023-03-02 05:20:53 +01:00
2023-12-12 03:46:51 +01:00
const sections : string [ ] [ ] = [ ]
let currentSection : string [ ] = [ ]
for ( let line of lines ) {
if ( line . trim ( ) . startsWith ( "# " ) ) {
sections . push ( currentSection )
currentSection = [ ]
2023-03-02 05:20:53 +01:00
}
2023-12-12 03:46:51 +01:00
line = line . replace ( 'src="../../public/' , 'src="./' )
line = line . replace ( 'src="../../' , 'src="./' )
currentSection . push ( line )
2023-03-02 05:20:53 +01:00
}
2023-12-12 03:46:51 +01:00
sections . push ( currentSection )
writeFileSync (
this . _target ,
JSON . stringify ( {
sections : sections.map ( ( s ) = > s . join ( "\n" ) ) . filter ( ( s ) = > s . length > 0 ) ,
} )
)
2023-03-02 05:20:53 +01:00
}
}
/ * *
2023-12-12 03:46:51 +01:00
* Generates a wiki page with the theme overview
* The wikitable should be updated regularly as some tools show an overview of apps based on the wiki .
2023-03-02 05:20:53 +01:00
* /
2023-12-12 03:46:51 +01:00
class WikiPageGenerator {
private readonly _target : string
2023-03-02 05:20:53 +01:00
2023-12-12 03:46:51 +01:00
constructor ( target : string = "Docs/wikiIndex.txt" ) {
this . _target = target
2023-03-02 05:20:53 +01:00
}
2023-12-12 03:46:51 +01:00
generate() {
let wikiPage =
'{|class="wikitable sortable"\n' +
"! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" +
"|-"
2023-03-02 05:20:53 +01:00
2023-12-12 03:46:51 +01:00
for ( const layout of themeOverview ) {
if ( layout . hideFromOverview ) {
2023-03-02 05:20:53 +01:00
continue
}
2023-12-12 03:46:51 +01:00
wikiPage += "\n" + this . generateWikiEntryFor ( layout )
2023-03-02 05:20:53 +01:00
}
2023-12-12 03:46:51 +01:00
wikiPage += "\n|}"
2023-03-02 05:20:53 +01:00
2023-12-12 03:46:51 +01:00
writeFileSync ( this . _target , wikiPage )
2023-03-02 05:20:53 +01:00
}
2023-12-12 03:46:51 +01:00
private generateWikiEntryFor ( layout : {
2022-11-02 13:47:34 +01:00
hideFromOverview : boolean
id : string
shortDescription : any
2023-12-12 03:46:51 +01:00
} ) : string {
2022-10-27 22:03:41 +02:00
if ( layout . hideFromOverview ) {
2022-11-02 13:47:34 +01:00
return ""
2022-10-27 22:03:41 +02:00
}
2023-01-11 01:32:37 +01:00
const languagesInDescr = Array . from ( Object . keys ( layout . shortDescription ) ) . filter (
( k ) = > k !== "_context"
)
2022-11-02 13:47:34 +01:00
const languages = languagesInDescr . map ( ( ln ) = > ` {{#language: ${ ln } |en}} ` ) . join ( ", " )
let auth = "Yes"
2022-10-27 22:03:41 +02:00
return ` {{service_item
2023-08-23 18:33:30 +02:00
| name = [ https : //mapcomplete.org/${layout.id} ${layout.id}]
2022-10-27 22:03:41 +02:00
| region = Worldwide
| lang = $ { languages }
| descr = A MapComplete theme : $ { Translations . T ( layout . shortDescription )
. textFor ( "en" )
. replace ( "<a href='" , "[[" )
2022-11-02 13:47:34 +01:00
. replace ( /'>.*<\/a>/ , "]]" ) }
2023-08-23 18:33:30 +02:00
| material = { { yes | [ https : //mapcomplete.org/ ${auth}]}}
2022-10-27 22:03:41 +02:00
| image = MapComplete_Screenshot . png
| genre = POI , editor , $ { layout . id }
} } `
}
2023-12-12 03:46:51 +01:00
}
2022-10-27 22:03:41 +02:00
2023-12-12 03:46:51 +01:00
export class GenerateDocs extends Script {
constructor ( ) {
super ( "Generates various documentation files" )
}
2022-10-27 22:03:41 +02:00
2023-12-12 03:46:51 +01:00
async main ( args : string [ ] ) {
console . log ( "Starting documentation generation..." )
ScriptUtils . fixUtils ( )
if ( ! existsSync ( "./Docs/Themes" ) ) {
mkdirSync ( "./Docs/Themes" )
2022-10-27 22:03:41 +02:00
}
2023-12-12 03:46:51 +01:00
this . WriteFile ( "./Docs/Tags_format.md" , TagUtils . generateDocs ( ) , [
"src/Logic/Tags/TagUtils.ts" ,
] )
new ToSlideshowJson (
"./Docs/Studio/Introduction.md" ,
"./src/assets/studio_introduction.json"
) . convert ( )
new ToSlideshowJson (
"./Docs/Studio/TagRenderingIntro.md" ,
"./src/assets/studio_tagrenderings_intro.json"
) . convert ( )
this . generateHotkeyDocs ( )
this . generateBuiltinIndex ( )
this . generateQueryParameterDocs ( )
this . generateBuiltinQuestions ( )
this . generateOverviewsForAllSingleLayer ( )
this . generateLayerOverviewText ( )
this . generateBuiltinUnits ( )
Array . from ( AllKnownLayouts . allKnownLayouts . values ( ) ) . map ( ( theme ) = > {
this . generateForTheme ( theme )
} )
this . WriteFile ( "./Docs/SpecialRenderings.md" , SpecialVisualizations . HelpMessage ( ) , [
"src/UI/SpecialVisualizations.ts" ,
] )
this . WriteFile (
"./Docs/CalculatedTags.md" ,
new Combine ( [
new Title ( "Metatags" , 1 ) ,
SimpleMetaTaggers . HelpText ( ) ,
ExtraFunctions . HelpText ( ) ,
] ) . SetClass ( "flex-col" ) ,
[ "src/Logic/SimpleMetaTagger.ts" , "src/Logic/ExtraFunctions.ts" ]
)
this . WriteFile ( "./Docs/SpecialInputElements.md" , Validators . HelpText ( ) , [
"src/UI/InputElement/Validators.ts" ,
] )
new WikiPageGenerator ( ) . generate ( )
console . log ( "Generated docs" )
}
2022-10-27 22:03:41 +02:00
2023-12-12 03:46:51 +01:00
private WriteFile (
filename ,
html : string | BaseUIElement ,
autogenSource : string [ ] ,
options ? : {
noTableOfContents : boolean
}
) : void {
if ( ! html ) {
return
}
for ( const source of autogenSource ) {
if ( source . indexOf ( "*" ) > 0 ) {
continue
}
if ( ! existsSync ( source ) ) {
throw (
"While creating a documentation file and checking that the generation sources are properly linked: source file " +
source +
" was not found. Typo?"
)
}
2022-10-27 22:03:41 +02:00
}
2023-12-12 03:46:51 +01:00
if ( html instanceof Combine && ! options ? . noTableOfContents ) {
const toc = new TableOfContents ( html )
const els = html . getElements ( )
html = new Combine ( [ els . shift ( ) , toc , . . . els ] ) . SetClass ( "flex flex-col" )
}
2023-10-24 21:41:04 +02:00
2023-12-12 03:46:51 +01:00
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 ( )
md . replace ( /\n\n\n+/g , "\n\n" )
if ( ! md . endsWith ( "\n" ) ) {
md += "\n"
2023-10-24 21:41:04 +02:00
}
2023-12-12 03:46:51 +01:00
const warnAutomated =
"[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)"
2023-11-07 18:51:50 +01:00
2023-12-12 03:46:51 +01:00
writeFileSync ( filename , warnAutomated + md )
2022-04-06 16:12:01 +02:00
}
2023-12-12 03:46:51 +01:00
private generateHotkeyDocs() {
new ThemeViewState ( new LayoutConfig ( < any > bookcases ) )
this . WriteFile ( "./Docs/Hotkeys.md" , Hotkeys . generateDocumentation ( ) , [ ] )
2022-04-06 16:12:01 +02:00
}
2022-11-30 21:38:50 +01:00
2023-12-12 03:46:51 +01:00
private generateBuiltinUnits() {
const layer = new LayerConfig ( < LayerConfigJson > unit , "units" , true )
const els : ( BaseUIElement | string ) [ ] = [ new Title ( layer . id , 2 ) ]
for ( const unit of layer . units ) {
els . push ( new Title ( unit . quantity ) )
for ( const denomination of unit . denominations ) {
els . push ( new Title ( denomination . canonical , 4 ) )
if ( denomination . useIfNoUnitGiven === true ) {
els . push ( "*Default denomination*" )
} else if (
denomination . useIfNoUnitGiven &&
denomination . useIfNoUnitGiven . length > 0
) {
els . push ( "Default denomination in the following countries:" )
els . push ( new List ( denomination . useIfNoUnitGiven ) )
}
if ( denomination . prefix ) {
els . push ( "Prefixed" )
}
if ( denomination . alternativeDenominations . length > 0 ) {
els . push (
"Alternative denominations:" ,
new List ( denomination . alternativeDenominations )
)
}
}
}
this . WriteFile ( "./Docs/builtin_units.md" , new Combine ( [ new Title ( "Units" , 1 ) , . . . els ] ) , [
` assets/layers/unit/unit.json ` ,
] )
2023-04-15 03:15:17 +02:00
}
2023-12-12 03:46:51 +01:00
/ * *
* Generates documentation for the all the individual layers .
* Inline layers are included ( if the theme is public )
* /
private generateOverviewsForAllSingleLayer ( ) : void {
const allLayers : LayerConfig [ ] = Array . from ( AllSharedLayers . sharedLayers . values ( ) ) . filter (
( layer ) = > layer [ "source" ] !== null
)
const builtinLayerIds : Set < string > = new Set < string > ( )
allLayers . forEach ( ( l ) = > builtinLayerIds . add ( l . id ) )
const inlineLayers = new Map < string , string > ( )
for ( const layout of Array . from ( AllKnownLayouts . allKnownLayouts . values ( ) ) ) {
if ( layout . hideFromOverview ) {
continue
}
for ( const layer of layout . layers ) {
if ( layer . source === null ) {
continue
}
if ( builtinLayerIds . has ( layer . id ) ) {
continue
}
if ( layer . source . geojsonSource !== undefined ) {
// Not an OSM-source
continue
}
allLayers . push ( layer )
builtinLayerIds . add ( layer . id )
inlineLayers . set ( layer . id , layout . id )
}
2022-01-29 02:45:59 +01:00
}
2023-12-12 03:46:51 +01:00
const themesPerLayer = new Map < string , string [ ] > ( )
for ( const layout of Array . from ( AllKnownLayouts . allKnownLayouts . values ( ) ) ) {
if ( layout . hideFromOverview ) {
2022-01-29 02:45:59 +01:00
continue
}
2023-12-12 03:46:51 +01:00
for ( const layer of layout . layers ) {
if ( ! builtinLayerIds . has ( layer . id ) ) {
// This is an inline layer
continue
}
if ( ! themesPerLayer . has ( layer . id ) ) {
themesPerLayer . set ( layer . id , [ ] )
2022-01-29 02:45:59 +01:00
}
2023-12-12 03:46:51 +01:00
themesPerLayer . get ( layer . id ) . push ( layout . id )
2022-01-29 02:45:59 +01:00
}
}
2023-12-12 03:46:51 +01:00
// Determine the cross-dependencies
const layerIsNeededBy : Map < string , string [ ] > = new Map < string , string [ ] > ( )
for ( const layer of allLayers ) {
for ( const dep of DependencyCalculator . getLayerDependencies ( layer ) ) {
const dependency = dep . neededLayer
if ( ! layerIsNeededBy . has ( dependency ) ) {
layerIsNeededBy . set ( dependency , [ ] )
}
layerIsNeededBy . get ( dependency ) . push ( layer . id )
2022-01-29 02:45:59 +01:00
}
}
2022-09-08 21:40:48 +02:00
2023-12-12 03:46:51 +01:00
allLayers . forEach ( ( layer ) = > {
const element = layer . GenerateDocumentation (
themesPerLayer . get ( layer . id ) ,
layerIsNeededBy ,
DependencyCalculator . getLayerDependencies ( layer )
)
const inlineSource = inlineLayers . get ( layer . id )
ScriptUtils . erasableLog ( "Exporting layer documentation for" , layer . id )
if ( ! existsSync ( "./Docs/Layers" ) ) {
mkdirSync ( "./Docs/Layers" )
}
let source : string = ` assets/layers/ ${ layer . id } / ${ layer . id } .json `
if ( inlineSource !== undefined ) {
source = ` assets/themes/ ${ inlineSource } / ${ inlineSource } .json `
}
this . WriteFile ( "./Docs/Layers/" + layer . id + ".md" , element , [ source ] , {
noTableOfContents : true ,
} )
} )
2022-01-29 02:45:59 +01:00
}
2022-09-08 21:40:48 +02:00
2023-12-12 03:46:51 +01:00
/ * *
* Generate the builtinIndex which shows interlayer dependencies
* @private
* /
private generateBuiltinIndex() {
const layers = ScriptUtils . getLayerFiles ( ) . map ( ( f ) = > f . parsed )
const builtinsPerLayer = new Map < string , string [ ] > ( )
const layersUsingBuiltin = new Map < string /* Builtin */ , string [ ] > ( )
for ( const layer of layers ) {
if ( layer . tagRenderings === undefined ) {
continue
}
const usedBuiltins : string [ ] = [ ]
for ( const tagRendering of layer . tagRenderings ) {
if ( typeof tagRendering === "string" ) {
usedBuiltins . push ( tagRendering )
continue
}
if ( tagRendering [ "builtin" ] !== undefined ) {
const builtins = tagRendering [ "builtin" ]
if ( typeof builtins === "string" ) {
usedBuiltins . push ( builtins )
} else {
usedBuiltins . push ( . . . builtins )
}
}
}
for ( const usedBuiltin of usedBuiltins ) {
const usingLayers = layersUsingBuiltin . get ( usedBuiltin )
if ( usingLayers === undefined ) {
layersUsingBuiltin . set ( usedBuiltin , [ layer . id ] )
} else {
usingLayers . push ( layer . id )
}
}
2022-01-14 19:34:00 +01:00
2023-12-12 03:46:51 +01:00
builtinsPerLayer . set ( layer . id , usedBuiltins )
}
const docs = new Combine ( [
new Title ( "Index of builtin TagRendering" , 1 ) ,
new Title ( "Existing builtin tagrenderings" , 2 ) ,
. . . Array . from ( layersUsingBuiltin . entries ( ) ) . map ( ( [ builtin , usedByLayers ] ) = >
new Combine ( [ new Title ( builtin ) , new List ( usedByLayers ) ] ) . SetClass ( "flex flex-col" )
) ,
] ) . SetClass ( "flex flex-col" )
this . WriteFile ( "./Docs/BuiltinIndex.md" , docs , [ "assets/layers/*.json" ] )
}
private generateQueryParameterDocs() {
if ( fakedom === undefined ) {
throw "FakeDom not initialized"
}
QueryParameters . GetQueryParameter (
"mode" ,
"map" ,
"The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'"
)
this . WriteFile (
"./Docs/URL_Parameters.md" ,
QueryParameterDocumentation . GenerateQueryParameterDocs ( ) ,
[ "src/Logic/Web/QueryParameters.ts" , "src/UI/QueryParameterDocumentation.ts" ]
)
}
private generateBuiltinQuestions() {
const qLayer = new LayerConfig ( < LayerConfigJson > questions , "questions.json" , true )
this . WriteFile (
"./Docs/BuiltinQuestions.md" ,
qLayer . GenerateDocumentation ( [ ] , new Map ( ) , [ ] ) ,
[ "assets/layers/questions/questions.json" ]
)
}
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 ,
"This theme contains the following layers:" ,
new List (
theme . layers
. filter ( ( l ) = > ! l . id . startsWith ( "note_import_" ) )
. map ( ( l ) = > new Link ( l . id , "../Layers/" + l . id + ".md" ) )
) ,
"Available languages:" ,
new List ( theme . language . filter ( ( ln ) = > ln !== "_context" ) ) ,
] ) . SetClass ( "flex flex-col" )
this . WriteFile (
"./Docs/Themes/" + theme . id + ".md" ,
el ,
[ ` assets/themes/ ${ theme . id } / ${ theme . id } .json ` ] ,
{ noTableOfContents : true }
)
}
/ * *
* Generates the documentation for the layers overview page
* @constructor
* /
private generateLayerOverviewText ( ) : BaseUIElement {
for ( const id of Constants . priviliged_layers ) {
if ( ! AllSharedLayers . sharedLayers . has ( id ) ) {
console . error ( "Priviliged layer definition not found: " + id )
return undefined
}
}
const allLayers : LayerConfig [ ] = Array . from ( AllSharedLayers . sharedLayers . values ( ) ) . filter (
( layer ) = > layer [ "source" ] === null
)
const builtinLayerIds : Set < string > = new Set < string > ( )
allLayers . forEach ( ( l ) = > builtinLayerIds . add ( l . id ) )
const themesPerLayer = new Map < string , string [ ] > ( )
for ( const layout of Array . from ( AllKnownLayouts . allKnownLayouts . values ( ) ) ) {
for ( const layer of layout . layers ) {
if ( ! builtinLayerIds . has ( layer . id ) ) {
continue
}
if ( ! themesPerLayer . has ( layer . id ) ) {
themesPerLayer . set ( layer . id , [ ] )
}
themesPerLayer . get ( layer . id ) . push ( layout . id )
}
}
// Determine the cross-dependencies
const layerIsNeededBy : Map < string , string [ ] > = new Map < string , string [ ] > ( )
for ( const layer of allLayers ) {
for ( const dep of DependencyCalculator . getLayerDependencies ( layer ) ) {
const dependency = dep . neededLayer
if ( ! layerIsNeededBy . has ( dependency ) ) {
layerIsNeededBy . set ( dependency , [ ] )
}
layerIsNeededBy . get ( dependency ) . push ( layer . id )
}
}
const el = new Combine ( [
new Title ( "Special and other useful layers" , 1 ) ,
"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 + ")" ) ) ,
. . . Utils . NoNull (
Constants . priviliged_layers . map ( ( id ) = > AllSharedLayers . sharedLayers . get ( id ) )
) . map ( ( l ) = >
l . GenerateDocumentation (
themesPerLayer . get ( l . id ) ,
layerIsNeededBy ,
DependencyCalculator . getLayerDependencies ( l ) ,
Constants . added_by_default . indexOf ( < any > l . id ) >= 0 ,
Constants . no_include . indexOf ( < any > l . id ) < 0
)
) ,
new Title ( "Normal layers" , 1 ) ,
"The following layers are included in MapComplete:" ,
new List (
Array . from ( AllSharedLayers . sharedLayers . keys ( ) ) . map (
( id ) = > new Link ( id , "./Layers/" + id + ".md" )
)
) ,
] )
this . WriteFile ( "./Docs/BuiltinLayers.md" , el , [ "src/Customizations/AllKnownLayouts.ts" ] )
}
2023-04-15 03:15:17 +02:00
}
2023-10-24 21:41:04 +02:00
2023-12-12 03:46:51 +01:00
new GenerateDocs ( ) . run ( )