Compare commits

...

4 Commits

Author SHA1 Message Date
Matthias@Dell
6a1243678a add example 2023-11-27 11:49:07 +01:00
Matthias@Dell
56f900e1d4 cleanup 2023-11-27 11:48:50 +01:00
ea964ae874 improve sidenav structure and generation 2023-11-25 03:26:52 +01:00
eea58fd25f rm failed css files 2023-11-24 18:31:15 +01:00
42 changed files with 2030 additions and 41 deletions

View File

@ -332,16 +332,19 @@ $(OUT_DIR)/%.html: $(PROJECT_DIR)/%.html | $(OUT_DIRS) $(_DEP_DIRS)
@#awk -i inplace '{FS="" sub(/<!--.*-->/,"")}1' $@
@#awk -i inplace '{if (NF != 0) print}' $@
# SASS
$(OUT_DIR)/%.css: $(PROJECT_DIR)/%.sass | $(OUT_DIRS) $(_DEP_DIRS)
@printf $(FMT_OUT_CSS) "$<" "$@";
@$(_SASS_CMD) --indented $< $@
@# generate a dependecy file from the source map and delete the map
@$(_SASS_CMD) --indented "$<" "$@" || { rm "$@"; exit 1; }
@depfile=$(patsubst $(OUT_DIR)/%,$(DEP_DIR)/%,$@).d; echo -n "$@: " > "$$depfile"; \
jq -r '.sources | @sh' $@.map | tr -d \' | sed 's|file://||g' >> "$$depfile"; \
rm $@.map
@# generate a dependecy file from the source map and delete the map
# SCSS
$(OUT_DIR)/%.css: $(PROJECT_DIR)/%.scss | $(OUT_DIRS) $(_DEP_DIRS)
@printf $(FMT_OUT_CSS) "$<" "$@";
@$(_SASS_CMD) --no-indented $< $@
@$(_SASS_CMD) --no-indented "$<" "$@" || { rm "$@"; exit 1; }
@# generate a dependecy file from the source map and delete the map
@depfile=$(patsubst $(OUT_DIR)/%,$(DEP_DIR)/%,$@).d; echo -n "$@: " > "$$depfile"; \
jq -r '.sources | @sh' $@.map | tr -d \' | sed 's|file://||g' >> "$$depfile"; \

View File

@ -0,0 +1 @@
build/de/de-only.html: src/include/head.html src/de/de-only.html

View File

@ -0,0 +1 @@
build/de/index.html: src/include/head.html src/include/index.de.md src/common/index.html

View File

@ -0,0 +1 @@
build/en/en-only.html: src/include/head.html src/en/en-only.html

View File

@ -0,0 +1 @@
build/en/index.html: src/include/head.html src/include/index.en.md src/common/index.html

View File

@ -0,0 +1 @@
build/style/main.css: /home/matth/Projekte/web/bUwUma/example/src/style/main.sass /home/matth/Projekte/web/bUwUma/example/src/include/style/images.sass /home/matth/Projekte/web/bUwUma/example/src/include/style/sidenav.sass

1
example/.sitemap.pkl Normal file
View File

@ -0,0 +1 @@
<EFBFBD>}<7D>.

375
example/Makefile Normal file
View File

@ -0,0 +1,375 @@
# ABOUT
# - In this Makefile, 'building a file' means:
# - if the file has a '.html' extension: run the html preprocessor on the file and place the output in the output directory
# - elif the file has a '.sass' or '.scss' extension: run the sass compiler on the file and place the output in the output directory
# - else: copy the file to the output directory
# - Folder structure from source directories will be preserved in the output directory
# - Abbreviations:
# - FLS: files
# - DIR: directory
# - SRC: source
# - LANG: language
# - PP: preprocessor
# - DEP: dependency
#
# NORMAL SETTINGS
# change these to fir your project
#
# root dir for the project, most other paths are relative to PROJECT_DIR
# [absolute or relative to current working directory]
PROJECT_DIR = src
# path where final website will be in
# [absolute or relative to current working directory]
OUT_DIR = build
# SOURCE FILES:
# all SRC_FLS and all files (recursively) in the SRC_DIRS will be built
# all files in PROJECT_DIR (not recursively) are source files
# [relative to PROJECT_DIR]
SRC_DIRS = de en script
SRC_FLS =
# CSS FILES:
# directories which may contain sass and scss to compile sass to a correspondig css in OUT_DIR/CSS_DIR (also css, it will simply be copied)
# [relative to PROJECT_DIR]
CSS_DIRS = style
CSS_FILES =
# RESOURCE FILES:
# all RESOURCE_FLS and all files in the RESOURCE_DIRS will be copied to OUT_DIR
# [relative to PROJECT_DIR]
RESOURCE_DIRS = resources
RESOURCE_FLS =
# MULTI-LANG SOURCE FILES:
# the files in COMMON_DIR will be built for all LANGS:
# for example:
# LANGS = de en
# PROJECT_DIR/COMMON_DIR/home.html
# -> OUT_DIR/de/home.html
# -> OUT_DIR/en/home.html
# foreach html-file in COMMON_DIR:
# foreach lang in LANGS:
# run HTML_PP_CMD with --var lang=lang on file and output to OUT_DIR without the COMMON_DIR prefix, so COMMON_DIR/subdir/file.html -> OUT_DIR/lang/subdir/file.html
# For all .html files, the proprocessor will make the variable `lang` available, for example lang=de
# All non-html files will handled the same way, but without the preprocessor being run on them. They are simply copied.
# leave COMMON_DIR blank to disable multi-lang feature
# [relative to PROJECT_DIR]
COMMON_DIR = common
LANGS = de en
# FAVICON
# image from which the favicons will be generated
# leave FAVICON_SRC blank to not generate favicons
# [relative to PROJECT_DIR]
FAVICON_SRC = resources/favicon.png
# directory where all genreated favicons will be placed
# [relative to OUT_DIR]
FAVICON_DIR = favicon
# in addition to the ones below, a favicon.ico containing the 16x16, 32x32 and 48x48will be generated
# all apple-touch-icon-XXxXX.png sizes
APPLE_ICON_SIZES = 180x180
# all mstile-XXxXX.png sizes
WINDOWS_ICON_SIZES = 150x150
# all android-chrome-XXxXX.png sizes
ANDROID_ICON_SIZES = 192x192 512x512
# all favicon-XXxXX.png sizes
FAVICON_ICON_SIZES = 16x16 32x32 48x48
# THUMBNAILS:
# thumbnails for all resource files having an extension in THUMB_FOR_TYPES will be generated and placed relative to THUMB_OUT_DIR
# leave THUMB_OUT_DIR blank to not generate thumbnails
# [relative to OUT_DIR]
THUMB_OUT_DIR =
# build thumbnails for these types: supported: mp3, flac, wav, pdf and all image formats that magick can handle
THUMB_FOR_TYPES = png gif jpg jpeg webp pdf mp4 mp3 flac wav
# filetype for the thumbnails. (pdfs will always have .jpg)
THUMB_TYPE = jpg
# size for the thumbnails (not respected by pdf)
THUMB_SIZE = 300
# SITEMAP
# leave SITEMAP blank to not generate a sitemap
# [relative to OUT_DIR]
SITEMAP = sitemap.xml
# base url of the website, without trailing /
WEBSITE_URL = https://example.com
# file required during build process for sitemap generation [absolute or relative to current working directory]
SITEMAP_TEMP_FILE = .sitemap.pkl
# comment to keep the file extension on sitemap entries
SITEMAP_REMOVE_EXT = 1
# PREPROCESSOR
# path to of the files that should be included
# [relative to PROJECT_DIR]
INCLUDE_DIR = include
# additional search paths passed to sass compiler
# [relative to PROJECT_DIR]
SASS_INCLUDE_DIRS = include/style
# ADVANCED
# the command to run the html preprocessor
HTML_PP_CMD = python3 html-preprocessor --exit-on light
# command to compile sass and scss files with
# --indented is added for sass and --no-indented for scss
# --source-maps-urls=absolute is appended for generating dependency files
SASS_CMD = sass --color
# [absolute or relative to current working directory]
DEP_DIR = .dependencies
#
# NOT SETTINGS ANYMORE
# DO NOT CHANGE ANYTHING HERE UNLESS YOU KNOW WHAT YOU ARE DOING!
#
# all variables starting with _ are relative to PROJECT_DIR
# make everything relative to PROJECT_DIR
_SRC_DIRS = $(addprefix $(PROJECT_DIR)/, $(SRC_DIRS))
_SRC_FLS = $(addprefix $(PROJECT_DIR)/, $(SRC_FLS))
_CSS_FLS = $(addprefix $(PROJECT_DIR)/, $(CSS_FLS))
_CSS_DIRS = $(addprefix $(PROJECT_DIR)/, $(CSS_DIRS))
_SASS_INCLUDE_DIRS = $(addprefix $(PROJECT_DIR)/, $(SASS_INCLUDE_DIRS))
_RES_DIRS = $(addprefix $(PROJECT_DIR)/, $(RESOURCE_DIRS))
_RES_FLS = $(addprefix $(PROJECT_DIR)/, $(RESOURCE_FLS))
_COMMON_DIR = $(addprefix $(PROJECT_DIR)/, $(COMMON_DIR))
_INCLUDE_DIR = $(addprefix $(PROJECT_DIR)/, $(INCLUDE_DIR))
# NORMAL SRC
# all SRC_DIRS + CSS_DIRS + all subdirs of each srcdir
_SRC_SUB_DIRS = $(foreach srcdir, $(_SRC_DIRS) $(_CSS_DIRS), $(shell find $(srcdir)/ -type d 2>/dev/null))
# add files in project dir
_SRC_FLS += $(shell find $(PROJECT_DIR)/ -maxdepth 1 -type f)
# add files src dirs, recursively
_SRC_FLS += $(foreach srcdir, $(_SRC_DIRS), $(shell find $(srcdir)/ -type f 2>/dev/null))
_CSS_FLS += $(foreach srcdir, $(_CSS_DIRS), $(shell find $(srcdir)/ -type f 2>/dev/null))
OUT_DIRS = $(OUT_DIR)/ $(patsubst $(PROJECT_DIR)/%, $(OUT_DIR)/%, $(_SRC_SUB_DIRS))
# path of the (css/sass) source files after being processed
OUT_FLS = $(patsubst $(PROJECT_DIR)/%, $(OUT_DIR)/%, $(_SRC_FLS))
OUT_FLS += $(patsubst $(PROJECT_DIR)/%, $(OUT_DIR)/%, $(foreach cssfile, $(_CSS_FLS), $(shell echo $(cssfile) | sed 's/\.s[ac]ss$$/.css/')))
# RESOURCES
_RES_SUB_DIRS = $(foreach srcdir, $(_RES_DIRS), $(shell find $(srcdir)/ -type d 2>/dev/null))
_RES_FLS += $(foreach srcdir, $(_RES_DIRS), $(shell find $(srcdir)/ -type f 2>/dev/null))
RES_OUT_DIRS = $(OUT_DIR)/ $(patsubst $(PROJECT_DIR)/%, $(OUT_DIR)/%, $(_RES_SUB_DIRS))
RES_OUT_FLS = $(patsubst $(PROJECT_DIR)/%, $(OUT_DIR)/%, $(_RES_FLS))
# MULTILANG
ifdef COMMON_DIR
_ML_SRC_FLS = $(shell find $(_COMMON_DIR)/ -type f)
_ML_SRC_SUB_DIRS= $(shell find $(_COMMON_DIR)/ -type d)
# will contain one subdir for each lang, each of which contains every file from ML_SRC_FLS
ML_OUT_DIR = $(OUT_DIR)
ML_OUT_LANG_DIRS= $(foreach lang, $(LANGS), $(addprefix $(ML_OUT_DIR)/, $(lang)))
ML_OUT_DIRS = $(foreach lang, $(LANGS), $(patsubst $(_COMMON_DIR)/%, $(ML_OUT_DIR)/$(lang)/%, $(_ML_SRC_SUB_DIRS)))
ML_OUT_FLS = $(foreach lang, $(LANGS), $(patsubst $(_COMMON_DIR)/%, $(ML_OUT_DIR)/$(lang)/%, $(_ML_SRC_FLS)))
endif
ifdef FAVICON_DIR
FAVICON_OUT_DIR = $(addprefix $(OUT_DIR)/,$(FAVICON_DIR))
else
FAVICON_OUT_DIR = $(OUT_DIR)
endif
ifdef FAVICON_SRC
_FAVICON = $(addprefix $(PROJECT_DIR)/,$(FAVICON_SRC))
FAVICON_ICO = $(FAVICON_OUT_DIR)/favicon.ico
APPLE_ICONS = $(addsuffix .png,$(addprefix apple-touch-icon-,$(APPLE_ICON_SIZES)))
WINDOWS_ICONS = $(addsuffix .png,$(addprefix mstile-,$(WINDOWS_ICON_SIZES)))
ANDROID_ICONS = $(addsuffix .png,$(addprefix android-chrome-,$(ANDROID_ICON_SIZES)))
FAVICON_ICONS = $(addsuffix .png,$(addprefix favicon-,$(FAVICON_ICON_SIZES)))
FAVICONS_PNG = $(addprefix $(FAVICON_OUT_DIR)/,$(APPLE_ICONS) $(WINDOWS_ICONS) $(ANDROID_ICONS) $(FAVICON_ICONS))
FAVICONS = $(FAVICONS_PNG) $(FAVICON_ICO)
endif
ifdef THUMB_OUT_DIR
# files for which to generate thumbnails
_THUMB_FLS = $(filter $(foreach type, $(THUMB_FOR_TYPES), %.$(type)), $(_RES_FLS))
THUMB_OUT_FLS = $(addsuffix .jpg, $(basename $(patsubst $(PROJECT_DIR)/%, $(OUT_DIR)/$(THUMB_OUT_DIR)/%, $(_THUMB_FLS))))
THUMB_OUT_DIRS = $(sort $(dir $(THUMB_OUT_FLS))) # sort for removing duplicates
endif
# needed for creating them
_DEP_DIRS = $(sort $(patsubst $(OUT_DIR)/%, $(DEP_DIR)/%, $(OUT_DIRS) $(ML_OUT_DIRS)))
# needed for reading
_DEP_FLS = $(shell find $(DEP_DIR) -type f -name '*.d' 2>/dev/null)
ifdef SITEMAP
SITEMAP_OUT = $(addprefix $(OUT_DIR)/, $(SITEMAP))
HTML_PP_CMD += --sitemap-temp-file "$(SITEMAP_TEMP_FILE)" --sitemap-base-url $(WEBSITE_URL) --sitemap-webroot-dir "$(OUT_DIR)"
endif
ifdef SITEMAP_REMOVE_EXT
HTML_PP_CMD += --sitemap-remove-ext
endif
# SASS, add load-paths
_SASS_CMD = $(SASS_CMD) $(foreach includedir, $(_SASS_INCLUDE_DIRS), --load-path=$(includedir)) --source-map-urls=absolute
# PRINTING
FMT_VAR_SRC ="Variable '\e[1;34m%s\e[0m': \e[0;33m%s\e[0m\n"
FMT_VAR_OUT ="Variable '\e[1;34m%s\e[0m': \e[0;35m%s\e[0m\n"
FMT_DIR ="\e[1;34mMaking directory\e[0m: \e[0;35m%s\e[0m\n"
FMT_OUT_HTML ="\e[1;34mBuilding html\e[0m: \e[1;33m%s\e[0m at \e[1;35m%s\e[0m\n"
FMT_OUT_CSS ="\e[1;34mBuilding css\e[0m: \e[1;33m%s\e[0m at \e[1;35m%s\e[0m\n"
FMT_OUT_THUMB ="\e[1;34mGenerating thumbnail\e[0m: \e[1;33m%s\e[0m at \e[1;35m%s\e[0m\n"
FMT_OUT_SITEMAP ="\e[1;34mGenerating sitemap\e[0m: \e[1;35m%s\e[0m\n"
FMT_OUT_FAVICON ="\e[1;34mGenerating favicon\e[0m: \e[1;33m%s\e[0m at \e[1;35m%s\e[0m\n"
FMT_OUT_OTHER ="\e[1;34mBuilding\e[0m: \e[1;33m%s\e[0m at \e[1;35m%s\e[0m\n"
FMT_OUT_ML_HTML ="\e[1;34mBuilding html\e[0m in lang \e[1;34m%s\e[0m: \e[1;33m%s\e[0m at \e[1;35m%s\e[0m\n"
FMT_OUT_ML_OTHER ="\e[1;34mBuilding\e[0m in lang \e[1;34m%s\e[0m: \e[1;33m%s\e[0m at \e[1;35m%s\e[0m\n"
# .SUFFIXES:
# .SUFFIXES: .html .md
.PHONY: default normal multilang resources sitemap favicons thumbnails print start stop clean cleaner
.DEFAULT_GOAL = all
# include all the dependency makefiles
include $(_DEP_FLS)
all: normal multilang resources thumbnails sitemap favicons
normal: $(OUT_FLS)
sitemap: $(SITEMAP_OUT)
favicons: $(FAVICONS) $(FAVICON_ICO)
multilang: $(ML_OUT_FLS)
resources: $(RES_OUT_FLS)
thumbnails: $(THUMB_OUT_FLS)
print:
@printf $(FMT_VAR_SRC) "PROJECT_DIR" "$(PROJECT_DIR)"
@printf $(FMT_VAR_OUT) "OUT_DIRS" "$(OUT_DIRS)"
@printf $(FMT_VAR_SRC) "_INCLUDE_DIR" "$(_INCLUDE_DIR)"
@printf $(FMT_VAR_SRC) "_SRC_FLS" "$(_SRC_FLS)"
@printf $(FMT_VAR_OUT) "OUT_FLS" "$(OUT_FLS)"
@printf $(FMT_VAR_SRC) "_RES_FLS" "$(_RES_FLS)"
@printf $(FMT_VAR_OUT) "RES_OUT_FLS" "$(RES_OUT_FLS)"
@printf $(FMT_VAR_OUT) "_CSS_FLS" "$(_CSS_FLS)"
ifdef COMMON_DIR
@printf $(FMT_VAR_SRC) "_ML_SRC_FLS" "$(_ML_SRC_FLS)"
@printf $(FMT_VAR_OUT) "ML_OUT_FLS" "$(ML_OUT_FLS)"
endif
@printf $(FMT_VAR_SRC) "_DEP_FLS" "$(_DEP_FLS)"
ifdef THUMB_OUT_DIR
@printf $(FMT_VAR_SRC) "THUMB_OUT_DIR" "$(THUMB_OUT_DIR)"
@printf $(FMT_VAR_OUT) "_THUMB_FLS" "$(_THUMB_FLS)"
@printf $(FMT_VAR_OUT) "THUMB_OUT_FLS" "$(THUMB_OUT_FLS)"
@printf $(FMT_VAR_OUT) "THUMB_OUT_DIRS" "$(THUMB_OUT_DIRS)"
endif
@# @printf $(FMT_VAR_SRC) "y" "$(y)"
# DIRECTORIES
$(sort $(ML_OUT_DIRS) $(_DEP_DIRS) $(RES_OUT_DIRS) $(OUT_DIRS) $(THUMB_OUT_DIRS) $(FAVICON_OUT_DIR)):
@printf $(FMT_DIR) "$@"
@mkdir -p $@
# MULTILANG RULES
ifdef COMMON_DIR
# $@ is the target to trigger the rule, but all languages have to be built now
$(foreach out_dir, $(ML_OUT_LANG_DIRS), $(out_dir)/%.html): $(_COMMON_DIR)/%.html | $(ML_OUT_DIRS) $(_DEP_DIRS)
@RAW_TARGET=`echo $@ $(foreach lang, $(LANGS), | sed 's|$(ML_OUT_DIR)/$(lang)/||')`;\
for lang in $(LANGS); do \
target=$(ML_OUT_DIR)/$$lang/$$RAW_TARGET;\
printf $(FMT_OUT_ML_HTML) "$$lang" "$<" "$$target"; \
$(HTML_PP_CMD) --input "$<" --output "$$target" --var include_dir=$(_INCLUDE_DIR) --var lang=$$lang --output-deps "`echo $${target}.d | sed 's|$(OUT_DIR)/|$(DEP_DIR)/|'`"; \
done
# rule for all not html files
$(foreach out_dir, $(ML_OUT_LANG_DIRS), $(out_dir)/%): $(_COMMON_DIR)/% | $(ML_OUT_DIRS)
@lang=`echo $(patsubst $(ML_OUT_DIR)/%, %, $@) | awk -F "/" '{print $$1}'`; \
printf $(FMT_OUT_ML_OTHER) "$$lang" "$<" "$@" ; \
cp $< $@
endif
ifdef FAVICONS
# must be first
$(FAVICON_ICO): $(_FAVICON) | $(FAVICON_OUT_DIR)
@printf $(FMT_OUT_FAVICON) "$<" "$@"
@convert "$<" -define icon:auto-resize=16,32,48 "$@"
$(FAVICONS_PNG): $(_FAVICON) | $(FAVICON_OUT_DIR)
@printf $(FMT_OUT_FAVICON) "$<" "$@"
@# resize to 512x512 and pad with transparency in case resize did not resize to correct size
@size=$$(echo "$@" | grep -o -P '\d{2,4}x\d{2,4}');\
convert "$<" -resize "$${size}" -background none -gravity center -extent "$${size}" "$@"
endif
# THUMBNAILS
$(OUT_DIR)/$(THUMB_OUT_DIR)/%.jpg: | $(THUMB_OUT_DIRS)
@fulltarget="$@"; \
target="$(patsubst $(OUT_DIR)/$(THUMB_OUT_DIR)/%.jpg,%,$@)"; \
sources=($(_THUMB_FLS)); \
source=$$(printf "%s\n" $${sources[@]} | grep "$$target"'\.'); \
printf $(FMT_OUT_THUMB) "$$source" "$$fulltarget"; \
case "$${source##*.}" in \
"mp4-use-magick-as-well") ffmpegthumbnailer -i "$$source" -o "$$fulltarget" -s 300 -q 5;; \
"pdf") pdftoppm -f 1 -singlefile -jpeg -r 50 "$$source" "$${fulltarget%.*}";; \
"mp3"|"flac"|"wav") ffmpeg -hide_banner -i "$$source" "$$fulltarget" -y >/dev/null;; \
*) magick "$${source}[0]" -thumbnail '$(THUMB_SIZE)x$(THUMB_SIZE)>' "$@";; \
esac
# SITEMAP
ifdef SITEMAP_OUT
$(SITEMAP_OUT): $(OUT_FLS) $(ML_OUT_FLS) # build sitemap after all other files
@printf $(FMT_OUT_SITEMAP) "$@"
@$(HTML_PP_CMD) --sitemap-generate "$@"
endif
#
# (NORMAL/RE-)SOURCE RULES
#
$(OUT_DIR)/%.html: $(PROJECT_DIR)/%.html | $(OUT_DIRS) $(_DEP_DIRS)
@printf $(FMT_OUT_HTML) "$<" "$@";
@$(HTML_PP_CMD) --input "$<" --output "$@" --var include_dir=$(_INCLUDE_DIR) --output-deps "$(subst $(DEP_DIR)/$(PROJECT_DIR), $(DEP_DIR), $(DEP_DIR)/$<.d)";
@# remove comments and empty lines. two separate lines bc the substitution might create new empty lines
@#awk -i inplace '{FS="" sub(/<!--.*-->/,"")}1' $@
@#awk -i inplace '{if (NF != 0) print}' $@
# SASS
$(OUT_DIR)/%.css: $(PROJECT_DIR)/%.sass | $(OUT_DIRS) $(_DEP_DIRS)
@printf $(FMT_OUT_CSS) "$<" "$@";
@$(_SASS_CMD) --indented "$<" "$@" || { rm "$@"; exit 1; }
@depfile=$(patsubst $(OUT_DIR)/%,$(DEP_DIR)/%,$@).d; echo -n "$@: " > "$$depfile"; \
jq -r '.sources | @sh' $@.map | tr -d \' | sed 's|file://||g' >> "$$depfile"; \
rm $@.map
@# generate a dependecy file from the source map and delete the map
# SCSS
$(OUT_DIR)/%.css: $(PROJECT_DIR)/%.scss | $(OUT_DIRS) $(_DEP_DIRS)
@printf $(FMT_OUT_CSS) "$<" "$@";
@$(_SASS_CMD) --no-indented "$<" "$@" || { rm "$@"; exit 1; }
@# generate a dependecy file from the source map and delete the map
@depfile=$(patsubst $(OUT_DIR)/%,$(DEP_DIR)/%,$@).d; echo -n "$@: " > "$$depfile"; \
jq -r '.sources | @sh' $@.map | tr -d \' | sed 's|file://||g' >> "$$depfile"; \
rm $@.map
# this rule must be last!
$(OUT_DIR)/%: $(PROJECT_DIR)/% | $(OUT_DIRS) $(RES_OUT_DIRS)
@printf $(FMT_OUT_OTHER) "$<" "$@"
@cp -r $< $@
# .DEFAULT:
# @echo "MISSING RULE: $@"
start:
/usr/sbin/nginx -c nginx.conf -p $(shell pwd)&
firefox http://localhost:8080/
stop:
killall nginx
clean:
-@rm $(OUT_FLS) $(ML_OUT_FLS) $(SITEMAP_TEMP_FILE) $(SITEMAP) 2>/dev/null
-@rm -r $(DEP_DIR) 2>/dev/null
cleaner:
-@rm -r $(OUT_DIR)
-@rm -r $(DEP_DIR) 2>/dev/null

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta http-equiv="content-type" content="text/html">
<meta charset="utf-8">
<meta name="description" content="Diese Seite ist nur auf deutsch">
<meta name="keywords" content="deutsche,keywords,beispiel">
<meta name="author" content="Max Mustermann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coole Seite</title>
<link rel="icon" type="image/x-icon" href="/favicon/favicon.ico">
<link rel="shortcut-icon" href="/favicon/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="/favicon/apple-touch-icon-180x180.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="/style/main.css", id="mainStyleSheet">
</head>
<script src="/script/test.js"></script>
<body>
<main>
<h1>Hallo</h1>
<div class="img_text">
<img class="example" src="/resources/example.svg" alt="Beispielbild" title="bUwUma"/>
<p>
Diese Seite ist nur auf deutsch verfügbar
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam.
</p>
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta http-equiv="content-type" content="text/html">
<meta charset="utf-8">
<meta name="description" content="Voreingestellte Beschreibung">
<meta name="keywords" content="Index,Beispiel">
<meta name="author" content="Max Mustermann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Index</title>
<link rel="icon" type="image/x-icon" href="/favicon/favicon.ico">
<link rel="shortcut-icon" href="/favicon/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="/favicon/apple-touch-icon-180x180.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="/style/main.css", id="mainStyleSheet">
</head>
<script src="/script/test.js"></script>
<body>
<div class="sidenav">
<ul>
<li class="menudrop">&#9776;</li>
<li class="sidenav_section_name">Abschnitte</li>
<li class="sidenav_section_links">
<ul>
<li class="sidenav_link"><a href="#interesting">Dieser Abschnitt ist sehr interessant</a></li>
<li class="sidenav_link"><a href="#section3">Geänderter Name!</a></li>
</ul>
</li>
<li class="sidenav_section_name">Sonstiges:</li>
<li class="sidenav_section_links">
<ul>
<li class="sidenav_link"><a href="/en/index.html">Index Englisch</a></li>
<li class="sidenav_link"><a href="/en/en-only.html">Nur englische Seite</a></li>
<li class="sidenav_link"><a href="/de/de-only.html">Nur deutsche Seite</a></li>
</ul>
</li>
</ul>
</div>
<main>
<h1>Willkommen auf der Deutschen Version</h1>
<p>Das Navigationsmenü wurde anhand der Überschriften erstellt und einige extra links aus <code>src/common/index.html</code> wurden eingefügt.</p>
<h2 id="interesting">Dieser Abschnitt ist sehr interessant</h2>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est.</p>
<h2>Diese Überschrift bekommt keinen Eintrag, weil sie keine id hat</h2>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<h3 id="section3">Dieser Abschnitt hat im Menü einen custom Namen</h3>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam.</p>
</main>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html">
<meta charset="utf-8">
<meta name="description" content="Default description">
<meta name="keywords" content="english,keywords,example">
<meta name="author" content="Max Mustermann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>English only</title>
<link rel="icon" type="image/x-icon" href="/favicon/favicon.ico">
<link rel="shortcut-icon" href="/favicon/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="/favicon/apple-touch-icon-180x180.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="/style/main.css", id="mainStyleSheet">
</head>
<script src="/script/test.js"></script>
<body>
<main>
<h1>Hello there!</h1>
<div class="img_text">
<img class="example" src="/resources/example.svg" alt="Example image" title="bUwUma"/>
<p>
This site is only available in engisch.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam.
</p>
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html">
<meta charset="utf-8">
<meta name="description" content="Default description">
<meta name="keywords" content="Index,Example">
<meta name="author" content="Max Mustermann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Index</title>
<link rel="icon" type="image/x-icon" href="/favicon/favicon.ico">
<link rel="shortcut-icon" href="/favicon/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="/favicon/apple-touch-icon-180x180.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="/style/main.css", id="mainStyleSheet">
</head>
<script src="/script/test.js"></script>
<body>
<div class="sidenav">
<ul>
<li class="menudrop">&#9776;</li>
<li class="sidenav_section_name">Sidenav Section</li>
<li class="sidenav_section_links">
<ul>
<li class="sidenav_link"><a href="#interesting">This section is super interesting</a></li>
<li class="sidenav_link"><a href="#section3">Custom named section!</a></li>
</ul>
</li>
<li class="sidenav_section_name">Other:</li>
<li class="sidenav_section_links">
<ul>
<li class="sidenav_link"><a href="/de/index.html">Index Deutsch</a></li>
<li class="sidenav_link"><a href="/en/en-only.html">Englisch only page</a></li>
<li class="sidenav_link"><a href="/de/de-only.html">German only page</a></li>
</ul>
</li>
</ul>
</div>
<main>
<h1>Welcome to the english version</h1>
<p>The navigation menu was generated from the headings and some custom links defined in <code>src/common/index.html</code>.</p>
<h2 id="interesting">This section is super interesting</h2>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est.</p>
<h2>This heading does not get an entry, because it has no id</h2>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<h3 id="section3">This section has a custom name in the menu</h3>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam.</p>
</main>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.700001 12.700001"
version="1.1"
id="svg1"
inkscape:version="1.3.1 (91b66b0783, 2023-11-16, custom)"
sodipodi:docname="example.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="9.3924909"
inkscape:cx="33.537429"
inkscape:cy="32.898621"
inkscape:window-width="1900"
inkscape:window-height="1034"
inkscape:window-x="8"
inkscape:window-y="34"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-86.894793,-82.841363)">
<path
style="fill:#ff0000;stroke-width:0.0981705"
d="M 87.064776,94.98403 99.462221,94.21962 97.232099,83.103959 c -2.136109,2.741712 -5.04605,3.605895 -8.670918,2.735482 0.842445,5.641412 -0.255599,7.531012 -1.496405,9.144589 z"
id="path5"
sodipodi:nodetypes="ccccc" />
<text
xml:space="preserve"
style="font-size:1.57072px;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="90.043213"
id="text1"><tspan
sodipodi:role="line"
style="font-size:1.57072px;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="90.043213"
id="tspan3">bUwUma</tspan><tspan
sodipodi:role="line"
style="font-size:1.57072px;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="92.006615"
id="tspan5">---</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.700001 12.700001"
version="1.1"
id="svg1"
inkscape:version="1.3.1 (91b66b0783, 2023-11-16, custom)"
sodipodi:docname="favicon.png"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="9.3924909"
inkscape:cx="33.537429"
inkscape:cy="32.898621"
inkscape:window-width="1900"
inkscape:window-height="1034"
inkscape:window-x="8"
inkscape:window-y="34"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-86.894793,-82.841363)">
<path
style="fill:#ff0000;stroke-width:0.0981705"
d="M 87.064776,94.98403 99.462221,94.21962 97.232099,83.103959 c -2.136109,2.741712 -5.04605,3.605895 -8.670918,2.735482 0.842445,5.641412 -0.255599,7.531012 -1.496405,9.144589 z"
id="path5"
sodipodi:nodetypes="ccccc" />
<text
xml:space="preserve"
style="font-size:1.57072px;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="90.043213"
id="text1"><tspan
sodipodi:role="line"
style="font-size:1.57072px;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="90.043213"
id="tspan3">bUwUma</tspan><tspan
sodipodi:role="line"
style="font-size:1.57072px;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="92.006615"
id="tspan5">---</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
console.log("This script will indeed be loaded!");

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
</urlset>

View File

@ -0,0 +1,42 @@
body {
background-color: #eee;
}
h1 {
color: blue;
}
p {
border: solid 1px #cc0;
}
.img_text img {
width: 30%;
height: auto;
float: left;
}
.img_text p {
width: 69%;
float: left;
}
.sidenav {
border: solid 2px blue;
}
.sidenav ul {
width: fit-content;
}
.sidenav li {
display: none;
}
.sidenav:hover li {
display: block;
}
.sidenav .menudrop {
display: block;
}
.sidenav:hover .menudrop, .sidenav li {
display: none;
}
/*# sourceMappingURL=main.css.map */

904
example/html-preprocessor Executable file
View File

@ -0,0 +1,904 @@
#!/bin/python3
import os
from os import path
import re
from sys import argv
from collections.abc import Callable
import argparse
import pickle
"""
TODO:
- more testing
- reintroduce the nav_selected class on nav feature
"""
"""
************************************************************ SETTINGS ************************************************************
"""
sidenav_format = """\
<div class="sidenav">
<ul>
<li class="menudrop">&#9776;</li>
#sidenav-content
</ul>
</div>
"""
sidenav_content_link = "<li class=\"sidenav_link\"><a href=\"#link\">#name</a></li>"
sidenav_content_section = """\
<li class="sidenav_section_name">#name</li>
<li class="sidenav_section_links">
<ul>
#links
</ul>
</li>"""
exit_on_include_failure = False
sitemap_begin = """\
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n"""
sitemap_end = "</urlset>"
"""
************************************************************ REGULAR EXPRESSIONS ************************************************************
"""
# SIDENAV
# heading with id
re_sidenav_heading = r"<h\d.*id=(?:\"|\')([a-zA-Z0-9_\-]+)(?:\"|\').*>(.+)</h\d>"
# custom entry
re_sidenav_custom = r"href=(?:\"|\')([^\"\' ]+)(?:\"|\') +name=(?:\"|\')(.+)(?:\"|\')"
# commas
re_set_map = r"([a-zA-Z0-9_]+) *\? *\{( *(?:[a-zA-Z0-9_*]+ *: *[^,]*, *)+[a-zA-Z0-9_*]+ *: *[^,]*) *,? *\}"
# semicolons
re_set_map_alt = r"([a-zA-Z0-9_]+) *\? *\{( *(?:[a-zA-Z0-9_*]+ *: *[^;]* *; *)+[a-zA-Z0-9_*]+ *: *[^;]*) *;? *\}"
""" #$(myvar) """
re_variable_use = r"#\$\(([a-zA-Z0-9_]+)\)"
""" only in comments """
re_preprocessor_command = r"[\t ]*#([a-zA-Z]+) *(.*)[\t ]*"
# https://www.w3.org/TR/NOTE-datetime
re_w3cdate = r"\d{4}-(?)]-\d{2}"
r"\d{4}-(?:0[1-9]|1[0-2])-(?:[0-2]\d|3[01])(T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d([\+\-](?:0\d|1[0-2]):[0-5]\d)?)?"
COMMENT_BEGIN = "<!--"
COMMENT_END = "-->"
"""
************************************************************ GLOBALS ************************************************************
"""
glob_dependcies: list[str] = []
exit_codes = {
"FileNotFound": 2,
"MarkdownConversionError": 3,
}
error_levels = {
"light": 0,
"serious": 1,
"critical": 2,
}
exit_on_error_level = error_levels["serious"]
# url that the currently processed file have
current_file_url = ""
"""
************************************************************ UTILITY ************************************************************
"""
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
CYAN = '\033[96m'
GRAY = '\033[97m'
RESET = '\033[0m'
BOLD = '\033[1m'
WHITE = '\033[37m'
DEBUG = False
def pdebug(*args, **keys):
fname, *_args = args
if DEBUG: print(f"{CYAN}{fname}{GRAY}", *_args, RESET, **keys)
TRACE = False
def ptrace(*args, **keys):
fname, *_args = args
if TRACE: print(f"{BLUE}{fname}{GRAY}", *_args, RESET, **keys)
def error(*args, level:int=exit_on_error_level, exit_code:int=1, **keys):
fname, *_args = args
if level >= exit_on_error_level:
print(f"{RED}ERROR: {fname}{RESET}", *_args, RESET, **keys)
exit(exit_code)
else:
print(f"{YELLOW}WARNING: {fname}{RESET}", *_args, RESET, **keys)
def line_is_link_to_path(line, path):
# check if the line is a link to html thats currently being processed
match = re.search(r"<a href=(\"|\')(.+)(\"|\')>(.+)</a>", line)
if match:
# get filename
match = re.match(r"[a-zA-Z0-9_\-]+\.html", match.groups()[1])
if match and match.group() in path:
return True
return False
def pos2line(s: str, pos:int):
return s[:pos].count('\n') + 1
def generate_dependecy_file(filename:str, deps:list[str]):
line1 = f"{filename}:"
s = ""
for dep in deps:
line1 += f" {dep}"
s += f"{dep}:\n"
return line1 #+ "\n" + s
def evaluate_condition(input_string) -> bool:
words = re.split(r"(==|!=|&&|\|\|)", input_string.replace(" ", ""))
for i in range(len(words)):
if words[i] not in ["==", "!=", "&&", "||"]:
words[i] = '"' + words[i].replace('"', r'\"') + '"'
condition = "".join(words).replace("&&", " and ").replace("||", " or ")
ptrace("evaluate_conditon", f"Evaluating condition {condition}")
try:
return eval(condition)
except SyntaxError:
error("evaluate_conditon", f"Pythonized condition is invalid: {condition}", level=error_levels["light"])
return False
"""
************************************************************ SITEMAP ************************************************************
"""
class Sitemap:
urls:dict = {}
def __init__(self, url=None):
self.url = url
self.priority = None
self.changefreq = None
self.lastmod = None
def set_url(self, url):
self.url = url
def set_priority(self, priority):
try:
priority = float(priority)
except ValueError:
error("Sitemap.set_priority", f"invalid priority: '{priority}'", level=error_levels["serious"])
if not (type(priority) == float and 0.0 <= priority and priority <= 1.0):
error("Sitemap.set_priority", f"invalid priority: '{priority}'", level=error_levels["serious"])
self.priority = priority
def set_changefreq(self, changefreq):
if not (type(changefreq) == str and changefreq in ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]):
error("Sitemap.set_changefreq", f"invalid changefreq: '{changefreq}'", level=error_levels["serious"])
self.changefreq = changefreq
def set_lastmod(self, lastmod):
if not (type(lastmod) == str and re.fullmatch(re_w3cdate, lastmod)):
error("Sitemap.set_lastmod", f"invalid lastmod: '{lastmod}'", level=error_levels["serious"])
self.lastmod = lastmod
def get_entry(self):
s = f"<url>\n\t<loc>{self.url}</loc>"
if self.priority is not None: s += f"\n\t<priority>{self.priority}</priority>"
if self.changefreq is not None: s += f"\n\t<changefreq>{self.changefreq}</changefreq>"
if self.lastmod is not None: s += f"\n\t<lastmod>{self.lastmod}</lastmod>"
s += "\n</url>"
return s
def __repr__(self) -> str:
return f"Sitemap(url={self.url}, priority={self.priority}, changefreq={self.changefreq}, lastmod={self.lastmod})"
@staticmethod
def gen_sidemap():
s = sitemap_begin
for url in Sitemap.urls.values():
s += "\t" + url.get_entry().replace("\n", "\n\t").strip("\t") + "\n"
s += sitemap_end
return s
@staticmethod
def cmd_sitemap(args:str, variables:dict[str,str]) -> str:
space = args.find(" ")
if space < 0:
space = len(args)
cmd = args[:space]
cmd_args = ""
if 0 < space and space < len(args) - 1:
cmd_args = args[space+1:].strip(" ")
pdebug("cmd_sitemap", f"cmd='{cmd}' cmd_args='{cmd_args}'")
if not current_file_url in Sitemap.urls:
Sitemap.urls[current_file_url] = Sitemap()
if cmd == "include":
if cmd_args:
Sitemap.urls[current_file_url].set_url(cmd_args)
else:
Sitemap.urls[current_file_url].set_url(current_file_url)
elif cmd == "priority":
Sitemap.urls[current_file_url].set_priority(cmd_args)
elif cmd == "changefreq":
Sitemap.urls[current_file_url].set_changefreq(cmd_args)
elif cmd == "lastmod":
Sitemap.urls[current_file_url].set_lastmod(cmd_args)
else:
error("cmd_sitemap", f"Invalid command '{cmd}'", error_levels["serious"])
ptrace("cmd_sitemap", f"Sitemap[{current_file_url}] is now: {Sitemap.urls[current_file_url]}")
return ""
"""
************************************************************ SIDENAV ************************************************************
"""
def replace_and_respect_indent(string, replace, replacement):
"""
replace all occurences of 'replace' with 'replacement', add the whitespaces in front of 'replace' to every line of 'replacement'
"""
i = string.find(replace)
while i >= 0:
line_begin = string.rfind("\n", 0, i) + 1
indent = string[line_begin:i]
string = string[:line_begin] + replacement.replace("\n", "\n" + indent) + string[i+len(replace):]
i = string.find(replace)
return string
class Sidenav:
class Link:
def __init__(self, name: str, link: str):
self.link = link
self.name = name
def __repr__(self):
return f"Link: name={self.name}, link={self.link}"
def get(self):
return sidenav_content_link.replace("#name", self.name).replace("#link", self.link)
class Section:
def __init__(self, name: str):
self.name = name
self.links = []
def add_link(self, link):
self.links.append(link)
def __repr__(self):
return f"Section: name={self.name}"
def get(self):
links = "".join([ link.get() + "\n" for link in self.links ])
return replace_and_respect_indent(sidenav_content_section.replace("#name", self.name), "#links", links)
entries: list[Link|Section] = []
skip_next = False
custom_name = None
@staticmethod
def addEntry(name: str, link: str):
if Sidenav.skip_next:
Sidenav.skip_next = None
return
if Sidenav.custom_name:
name = Sidenav.custom_name
Sidenav.custom_name = None
if len(Sidenav.entries) > 0 and type(Sidenav.entries[-1]) == Sidenav.Section:
Sidenav.entries[-1].add_link(Sidenav.Link(name, link))
else:
Sidenav.entries.append(Sidenav.Link(name, link))
@staticmethod
def addSection(name):
Sidenav.entries.append(Sidenav.Section(name))
@staticmethod
def setCustomName(name: str):
Sidenav.custom_name = name
@staticmethod
def skipNext():
Sidenav.skip_next = True
@staticmethod
def generate() -> str:
pdebug("Sidenav.generate", f"found the following entries: {Sidenav.entries}")
entries = "".join([entry.get() + "\n" for entry in Sidenav.entries])
return replace_and_respect_indent(sidenav_format, "#sidenav-content", entries)
@staticmethod
def cmd_sidenav(args:str, variables:dict[str,str]) -> str:
space = args.find(" ")
if space < 0:
space = len(args)
cmd = args[:space]
cmd_args = ""
if 0 < space and space < len(args) - 1:
cmd_args = args[space+1:].strip(" ")
pdebug("cmd_sidenav", f"cmd='{cmd}' cmd_args='{cmd_args}'")
if cmd == "skip":
Sidenav.skipNext()
elif cmd == "section":
Sidenav.addSection(cmd_args)
elif cmd == "name":
Sidenav.setCustomName(cmd_args)
elif cmd == "custom":
match = re.fullmatch(re_sidenav_custom, cmd_args)
if match:
Sidenav.addEntry(match.groups()[1], match.groups()[0])
else:
error("cmd_sidenav", f"Invalid argument for command 'custom': '{cmd_args}'", level=error_levels["light"])
elif cmd == "include":
return Sidenav.generate()
else:
error("cmd_sidenav", f"Invalid command: '{cmd}'", level=error_levels["light"])
return ""
"""
************************************************************ COMMANDS ************************************************************
All these commands take one arg with trimmed whitespaces.
The arg may be anything
They all need to return a string, which will be placed
into the source file at the place where the command was.
"""
def cmd_include(args: str, variables:dict[str, str]={}) -> str:
args = args.split(' ')
pdebug("cmd_include", f"args='{args}', variables='{variables}'")
filename = args[0]
content = ""
try:
with open(filename) as file:
content = file.read()
if len(args) > 1: # if section was specified
target_section = args[1]
p = HTMLParser(content, {})
p.pos["start"] = p.pos["end"] = -1
while p.i < len(p): # at start of new line or end of comment
p.find_line_end()
ptrace("cmd_include", f"Processing at i={p.i} in line {pos2line(p.file, p.i)}: '{p[p.i:p.pos['line_end']]}'")
if not p.find_comment_begin(): continue
if not p.find_comment_end(): continue
p.replace_multiline_comments()
match = p.find_command()
if match:
command = match.groups()[0]
cmd_args = match.groups()[1].replace('\t', ' ').strip(' ')
pdebug("cmd_include", f"Found command '{command}' with args '{cmd_args}'")
if command == "section":
if cmd_args.startswith(target_section):
p.pos["start"] = max(p.pos["cmt_end"] + len(COMMENT_END), p.pos["line_end"] + 1)
elif p.pos["start"] >= 0: #end
p.pos["end"] = max(p.pos["cmt_end"] + len(COMMENT_END), p.pos["line_end"] + 1)
# p.pos["end"] = p.pos["cmt_beg"]
p.replace_command_with_output("")
p.command_end() # remove the command (+comment)
if p.pos["start"] >= 0 and p.pos["end"] > 0: break
continue
# section cmd in multiline comment is not supported, so simply jump to end of comment
p.i = p.pos["cmt_end"] + len(COMMENT_END)
p.pos["cmt_beg"] = -1
p.pos["cmd_beg"] = -1
p.pos["cmt_end"] = -1
p.pos["cmd_end"] = -1
if p.pos["start"] >= 0:
if p.pos["end"] < 0:
p.pos["end"] = len(p)
content = p[p.pos["start"]:p.pos["end"]]
else:
error("cmd_include", f"Could not find section {target_section} in file {filename}")
except FileNotFoundError:
error("cmd_include", f"Could not open file '{filename}'", level=error_levels["serious"], exit_code=exit_codes["FileNotFound"])
content = f"<!-- Could not include '{filename}' -->"
if filename.endswith(".md"):
try:
from markdown import markdown
content = markdown(content, output_format="xhtml")
except:
error("cmd_include", f"Could convert markdown to html for file '{filename}'. Is python-markdown installed?", level=error_levels["critical"], exit_code=exit_codes["MarkdownConversionError"])
content = f"<!-- Could not convert to html: '{filename}' -->"
glob_dependcies.append(filename)
return content
def cmd_section(args: str, variables:dict[str, str]={}) -> str:
return ""
def cmd_return(args: str, variables:dict[str, str]={}) -> str:
# re_set_map = r"([a-zA-Z0-9_]+)\?\{(([a-zA-Z0-9_]+:.+,)*([a-zA-Z0-9_]+:.+))\}"
# <!-- #set section=lang?{*:Fallback,de:Abschnitt,en:Section} -->
space = args.find(' ')
pdebug("cmd_set", f"varname='{args[:space]}, 'arg='{args[space+1:]}', variables='{variables}'")
if not (space > 0 and space < len(args)-1):
variables[args] = ""
pdebug("cmd_set", f"Setting to empty string: {args}")
else:
varname = args[:space]
variables[varname] = ""
# check if map assignment with either , or ;
separator = ','
match = re.fullmatch(re_set_map, args[space+1:].strip(' '))
if not match:
match = re.fullmatch(re_set_map_alt, args[space+1:].strip(' '))
separator = ';'
if match:
pdebug("cmd_set", f"Map {match.group()}")
depends = match.groups()[0]
if not depends in variables:
pdebug("cmd_set", f"Setting from map, but depends='{depends}' is not in variables")
return ""
depends_val = variables[depends]
for option in match.groups()[1].split(separator):
option = option.strip(" ")
pdebug("cmd_set", f"Found option {option}")
colon = option.find(':') # we will find one, regex guarantees
if option[:colon].strip(" ") == depends_val or option[:colon].strip(" ") == "*":
variables[varname] = option[colon+1:].strip(" ")
else: # simple asignment
value = args[space+1:].strip(" ")
variables[varname] = value
pdebug("cmd_set", f"Assignment {varname} -> {value}")
return variables[varname]
return ""
def cmd_set(args: str, variables:dict[str, str]={}) -> str:
cmd_return(args, variables)
return ""
def cmd_unset(args: str, variables:dict[str, str]={}) -> str:
variable = args.strip(' ')
if variable not in variables:
pdebug("cmd_unset", f"variable '{variable}' is not set", level=error_levels["light"])
else:
variables.pop(variable)
return ""
def cmd_default(args: str, variables:dict[str, str]={}) -> str:
separator = args.find(' ')
if args[:separator] not in variables:
cmd_return(args, variables)
return ""
def cmd_comment(args: str, variables:dict[str, str]={}) -> str:
return f"<!-- {args} -->"
def cmd_uncomment(args: str, variables:dict[str, str]={}) -> str:
return args
def cmd_error(args: str, variables:dict[str, str]={}) -> str:
error("cmd_error", f"Encounted 'error' command: {args}", level=error_levels["critical"])
return ""
def cmd_warning(args: str, variables:dict[str, str]={}) -> str:
error("cmd_warning", f"Encounted 'warning' command: {args}", level=error_levels["light"])
return ""
command2function:dict[str, Callable[[str, dict[str,str]], str]] = {
"include": cmd_include,
"section": cmd_section,
"return": cmd_return,
"set": cmd_set,
"unset": cmd_unset,
"default": cmd_default,
"comment": cmd_comment,
"uncomment": cmd_uncomment,
"sidenav": Sidenav.cmd_sidenav,
"sitemap": Sitemap.cmd_sitemap,
"warning": cmd_warning,
"error": cmd_error,
}
"""
************************************************************ PARSING ************************************************************
"""
class Parser():
"""
General purpose parser class
It has states and positions in a text, which are updated when portions of the text are replaced or removed
"""
def __init__(self, file):
self.file = file
self.pos: dict[str, int] = {}
self.state: dict[str, bool] = {}
def remove(self, start, stop, ignore_bounds=[]):
"""remove range [start, stop) of text and update positions"""
delete_length = stop - start
nl, esl = "\n", "\\n"
ptrace("Parser.remove", f"Deleting range [{start}, {stop}) of length {delete_length}: '{self.file[start:stop].replace(nl, esl)}'")
assert(stop >= start)
assert(stop <= len(self.file))
self.file = self.file[:start] + self.file[stop:]
for k,pos in self.pos.items():
if pos >= stop: self.pos[k] -= delete_length
elif pos > start and not k in ignore_bounds: error("Parser.remove", f"Position {k}={pos} within deleted range [{start},{stop})", level=error_levels["light"])
def replace(self, start, stop, replacement, ignore_bounds=[]):
assert(stop >= start)
assert(stop <= len(self.file))
ptrace("Parser.replace", f"Replacing range [{start}, {stop}): '{self.file[start:stop]}' with '{replacement}'")
self.file = self.file[:start] + replacement + self.file[stop:]
length_difference = stop - start - len(replacement)
for k,pos in self.pos.items():
if pos >= stop: self.pos[k] -= length_difference
elif pos > start and k not in ignore_bounds: error("Parser.replace", f"Position {k}={pos} within replaced range [{start},{stop})", level=error_levels["light"])
def __getitem__(self, key):
return self.file[key]
def __len__(self):
return len(self.file)
class HTMLParser(Parser):
"""
Parse a html file
Each function operates the positon indicated by i until the position "line_end"
"""
def __init__(self, file, variables:dict[str, str], remove_comments=False):
super().__init__(file)
self.i = 0
self.variables = variables
self.pos["cmt_beg"] = -1
self.pos["cmt_end"] = -1
self.pos["cmd_beg"] = -1
self.pos["cmd_end"] = -1
self.pos["line_end"] = -1
self.pos["conditional_block_beg"] = -1 # char pos of the first char of the last block, if waiting for elif, else or endif
self.state["cmd_in_cmt"] = False
self.state["last_condition"] = False # if the last if condition was true
self.remove_comments = remove_comments
def use_variables(self):
"""replace variable usages in the current line"""
self.replace(self.i, self.pos["line_end"], substitute_variables(self[self.i:self.pos["line_end"]], self.variables))
ptrace("HTMLParser.use_variables", f"Line after variable substitution:", self.file[self.i:self.pos["line_end"]])
def add_sidenav_headings(self):
"""check if heading for sidenav in line"""
match = re.search(re_sidenav_heading, self[self.i:self.pos["line_end"]])
if match:
Sidenav.addEntry(match.groups()[1], f"#{match.groups()[0]}")
ptrace("HTMLParser.add_sidenav_headings:", f"Found heading with id:", match.groups())
def get_leading_whitespaces(self):
"""returns the whitespaces at the start of the line"""
# find last newline
line_beg = self.file.rfind("\n", 0, self.i)
if line_beg < 0: line_beg = 0
else: line_beg += 1 # start after newline
match = re.match(r"^([ \t]*)", self.file[line_beg:self.pos['line_end']])
if not match: return ""
else: return match.groups()[0]
# Parsing functions
def find_line_end(self):
"""
line_end -> position of next newline char or EOF
"""
self.pos["line_end"] = self.file.find('\n', self.i+1)
if self.pos["line_end"] < 0: self.pos["line_end"] = len(self)
def find_comment_begin(self) -> bool:
"""
find the beginning of a comment in the current line
if comment begin was found, jump into the comment, return True
cmt_beg -> beginning of COMMENT_BEGIN
i -> first character after COMMENT_BEGIN / line_end + 1
"""
# look for comment begin
if self.pos["cmt_beg"] < 0: # if not in comment, find next comment
self.pos["cmt_beg"] = self.file.find(COMMENT_BEGIN, self.i, self.pos["line_end"])
if self.pos["cmt_beg"] < 0:
self.i = self.pos["line_end"] + 1
return False
else:
# jump to comment_begin
old_i = self.i
self.i = self.pos["cmt_beg"] + len(COMMENT_BEGIN) # after comment begin
ptrace(f"HTMLParser.find_comment_begin", f"Found comment begin, jumping from pos {old_i} to {self.i}")
return True
return True # still in previous comment
def find_comment_end(self):
"""
call after find_comment_begin returns true to update the cmt_end
call continue when returning false
cmt_end -> beginning of COMMENT_END / ---
cmt_beg -> --- / -1 when invalid comment
"""
# in comment, i at the character after COMMENT_BEGIN
self.pos["cmt_end"] = self.file.find(COMMENT_END, self.i) #, self.pos["line_end"])
# sanity checks
if self.pos["cmt_end"] < 0:
error("HTMLParser.find_comment_end", f"Comment starting in line {pos2line(self.file, self.pos['cmt_beg'])} is never ended.", level=error_levels["serious"])
return False
else:
tmp_next_begin = self.file.find(COMMENT_BEGIN, self.i)
if 0 < tmp_next_begin and tmp_next_begin < self.pos["cmt_end"]:
error("HTMLParser.find_comment_end", f"Found next comment begin before the comment starting in line {pos2line(self.file, self.pos['cmt_beg'])} is ended! Skipping comment. Comment without proper closing tags: '{self.file[self.i:self.pos['line_end']]}'", level=error_levels["light"])
self.pos["cmt_beg"] = -1
return False
return True
def replace_multiline_comments(self):
"""
if in a multiline comment, turn every line into a separate comment
"""
# not a multiline comment
if self.pos["line_end"] > self.pos["cmt_end"]: return
indent = self.get_leading_whitespaces()
self.replace(self.pos["cmt_beg"], self.pos["cmt_end"], self.file[self.pos["cmt_beg"]:self.pos["cmt_end"]].replace("\n", "-->\n" + indent + "<!--"), ignore_bounds=["line_end"])
self.find_line_end()
self.find_comment_end()
def find_command(self):
# either at newline (if in multiline comment) or at comment end
self.pos["cmd_beg"] = self.i
self.pos["cmd_end"] = min(self.pos["line_end"], self.pos["cmt_end"])
assert self.pos["cmd_end"] >= self.i, f"cmd_end={self.pos['cmd_end']}, i={self.i}, line_end={self.pos['line_end']}, cmt_end={self.pos['cmt_end']}"
ptrace("HTMLParser.find_command", f"Possible command end: {self.pos['cmd_end']}, possible command: '{self[self.i:self.pos['cmd_end']]}'")
# find commands
match = re.fullmatch(re_preprocessor_command, self[self.i:self.pos["cmd_end"]].strip(" "))
if match:
self.state["cmd_in_cmt"] = True
return match
def replace_command_with_output(self, command_output):
# keep indent level
indent = self.get_leading_whitespaces()
self.replace(self.i, self.pos["cmd_end"], command_output.replace("\n", "\n" + indent))
ptrace(f"HTMLParser.replace_command_with_output", f"After command, the line is now '{self.file[self.i:self.pos['line_end']]}'")
def command_end(self):
if self.pos["cmd_end"] == self.pos["cmt_end"]: # reached end of comment
if self.state["cmd_in_cmt"] or self.remove_comments:
remove_newline = 0
if self[self.pos["cmt_beg"]-1] == '\n' and self[self.pos["cmt_end"]+len(COMMENT_END)] == '\n': # if the comment consumes the whole line, remove the entire line
remove_newline = 1
if self.state["cmd_in_cmt"]: # remove comment tags if a command was found
ptrace("HTMLParser.command_end", f"Deleting opening comment tags")
self.remove(self.pos["cmt_beg"], self.pos["cmt_beg"] + len(COMMENT_BEGIN))
self.remove(self.pos["cmt_end"], self.pos["cmt_end"] + len(COMMENT_END) + remove_newline, ignore_bounds=["cmt_end", "cmd_end", "line_end"])
# process the line again, because a command might have inserted new comments
self.i -= len(COMMENT_BEGIN)
elif self.remove_comments: # remove entire comment
self.remove(self.pos["cmt_beg"], self.pos["cmt_end"] + len(COMMENT_END) + remove_newline, ignore_bounds=["cmt_end", "cmd_beg", "cmd_end", "line_end"])
self.i = self.pos["cmt_beg"]
self.state["cmd_in_cmt"] = False
self.pos["cmt_beg"] = -1
self.pos["cmd_beg"] = -1
self.pos["cmt_end"] = -1
self.pos["cmd_end"] = -1
else: # multiline comment
self.pos["cmt_end"] = -1
self.pos["cmd_end"] = -1
self.i = self.pos["line_end"] + 1
ptrace(f"HTMLParser.command_end", f"Multiline comment, jumping to next line.")
# i = possible_command_end commented, because if something containing new commands is inserted we need to parse that as well
def parse_file(_file:str, variables:dict[str,str], remove_comments):
p = HTMLParser(_file, variables, remove_comments=remove_comments)
sidenav_include_pos = -1
while p.i < len(p): # at start of new line or end of comment
p.find_line_end()
ptrace("parse_file", f"Processing at i={p.i} in line {pos2line(p.file, p.i)}: '{p[p.i:p.pos['line_end']]}'")
p.use_variables()
p.add_sidenav_headings()
if not p.find_comment_begin(): continue
if not p.find_comment_end(): continue
p.replace_multiline_comments()
match = p.find_command()
if match:
command = match.groups()[0]
args = match.groups()[1].replace('\t', ' ').strip(' ')
pdebug("parse_file", f"Found command '{command}' with args '{args}'")
# delete from previous block if
if command in ["elif", "else", "endif"]:
if p.pos["conditional_block_beg"] < 0: error("parse_file", f"Misplaced '{command}' in line {pos2line(p.file, p.i)}")
if p.state["last_condition"]:
# delete block from here at next endif
p.state["last_condition"] = False
else:
# delete block from last condition statement
ptrace("parse_file", f"> Deleting block from last condition")
p.remove(p.pos["conditional_block_beg"], p.pos["cmt_beg"])
p.i = p.pos["cmd_beg"]
p.pos["conditional_block_beg"] = p.i
if command == "endif":
p.pos["conditional_block_beg"] = -1
p.state["last_condition"] = False
p.state["any_condition"] = False
# evaluate ifs
if command == "if":
p.pos["conditional_block_beg"] = p.i
p.state["last_condition"] = evaluate_condition(args)
p.state["any_condition"] = p.state["last_condition"]
pdebug("parse_file", f"Command {command} condition evaluated to {p.state['last_condition']}")
cmd_output = ""
elif command =="elif":
p.pos["conditional_block_beg"] = p.i
p.state["last_condition"] = evaluate_condition(args) if not p.state["any_condition"] else False
if p.state["last_condition"]:
p.state["any_condition"] = True
pdebug("parse_file", f"Command {command} condition evaluated to {p.state['last_condition']}")
cmd_output = ""
elif command == "else":
p.pos["conditional_block_beg"] = p.i
p.state["last_condition"] = True if not p.state["any_condition"] else False
cmd_output = ""
elif p.pos["conditional_block_beg"] < 0 or p.state["last_condition"]:
if command == "sidenav" and args == "include": # if args contains anything else this wont work
sidenav_include_pos = p.pos["cmt_beg"] # remove the comment
cmd_output = ""
elif command == "endif":
cmd_output = ""
elif command not in command2function:
error("parse_file", f"Invalid command in line {pos2line(p.file, p.i)}: {command}", level=error_levels["light"])
cmd_output = ""
else:
cmd_output = command2function[command](args, variables)
else:
cmd_output = ""
p.replace_command_with_output(cmd_output)
else:
pdebug("parse_file", f"Did not find command in comment {p.file[p.pos['cmt_beg']:p.pos['cmt_end']+len(COMMENT_END)]}")
p.command_end()
if sidenav_include_pos >= 0:
p.i = sidenav_include_pos # required before get_leading_whitespaces
p.find_line_end() # required before get_leading_whitespaces
indent = p.get_leading_whitespaces()
return p.file[:sidenav_include_pos] + Sidenav.generate().replace("\n", "\n" + indent) + p.file[sidenav_include_pos:]
else:
return p.file
def substitute_variables(html:str, variables:dict[str, str]):
"""
find usage of variables and replace them with their value
"""
matches = []
for match in re.finditer(re_variable_use, html):
matches.append(match)
html_list = list(html)
for match in reversed(matches):
pdebug("substitute_variables", f"Found variable usage {match.groups()[0]}, match from {match.start()} to {match.end()}")
value = ""
if match.groups()[0] in variables: value = variables[match.groups()[0]]
else:
pdebug("substitute_variables", f"Variable {match.groups()[0]} is used but not defined")
for _ in range(match.start(), match.end()):
html_list.pop(match.start())
html_list.insert(match.start(), value.strip(" "))
return ''.join(html_list)
"""
************************************************************ COMMAND LINE ************************************************************
"""
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog="bUwUma html preprocessor")
parser.add_argument("--input", action="store", help="path to the input file", default="")
parser.add_argument("--output", action="store", help="output to this file", default="")
parser.add_argument("--inplace", action="store_true", help="overwrite input file")
parser.add_argument("--preserve-comments", action="store_true", help="do not remove normal html comments", default=False)
parser.add_argument("--var", action="append", help="set a variable --var varname=value", default=[])
parser.add_argument("--output-deps", action="store", help="output a Makefile listing all dependencies", default="")
parser.add_argument("--sitemap-generate", action="store", help="generate the sitemap from the sitemap-temp-file", default="")
parser.add_argument("--sitemap-temp-file", action="store", help="file for storing sitemap data during build process", default="/tmp/sitemap.pkl")
parser.add_argument("--sitemap-webroot-dir", action="store", help="directory of the webroot, without trailing /. This will be removed from the output path for generating the sitemap url entry", default="")
parser.add_argument("--sitemap-base-url", action="store", help="base url of the website, without trailing /", default="https://www.example.com")
parser.add_argument("--sitemap-remove-ext", action="store_true", help="remove the file extenstion in the sitemap entry")
parser.add_argument("--exit-on", action="store", help="exit when an error of the given severity occures", choices=["light", "serious", "critical"], default="serious")
parser.add_argument("--debug", action="store_true", help="be more verbose", default=False)
parser.add_argument("--trace", action="store_true", help="be extremly verbose", default=False)
variables:dict[str, str] = {}
args = parser.parse_args()
for var in args.var:
sep = var.find('=')
if sep > 0 and sep < len(var) - 1:
variables[var[:sep].strip(" ")] = var[sep+1:].strip(" ")
else:
parser.error(f"Invalid argument: --var '{var}'\n\tUsage: --var <varname>=<value>")
args.input = args.input.strip(" ")
args.output = args.output.strip(" ")
args.output_deps = args.output_deps.strip(" ")
args.sitemap_temp_file = args.sitemap_temp_file.strip(" ")
args.sitemap_generate = args.sitemap_generate.strip(" ")
TRACE = args.trace
if args.trace: args.debug = True
DEBUG = args.debug
# either input file or sitemap_generate is required
if not (bool(args.input) ^ bool(args.sitemap_generate)):
parser.error(f"Exactly one if --input or --sitemap-generate must be given")
if args.input:
if args.sitemap_webroot_dir:
current_file_url = args.sitemap_base_url + args.output.replace(args.sitemap_webroot_dir, "")
else:
current_file_url = args.sitemap_base_url + args.output
if args.sitemap_remove_ext:
current_file_url = os.path.splitext(current_file_url)[0]
pdebug("main", f"current_file={current_file_url}")
# sanity checks
if not path.isfile(args.input):
parser.error(f"Invalid input file:: {args.input}")
if args.output:
if not path.isdir(path.dirname(args.output)):
parser.error(f"Invalid path to output file - directory does not exist: '{path.dirname(args.output)}'")
elif args.inplace:
args.output = args.input
if args.inplace and args.output:
parser.error(f"Only one of --output or --inplace mut be given")
if args.output_deps:
if not path.isdir(path.dirname(args.output_deps)):
parser.error(f"Invalid path to dependency file - directory does not exist: '{path.dirname(args.output_deps)}'")
if not args.output:
parser.error(f"--output-deps requires either --output <file> our --inplace")
if args.sitemap_temp_file:
if path.isfile(args.sitemap_temp_file):
with open(args.sitemap_temp_file, "rb") as file:
Sitemap.urls = pickle.load(file)
# get html
with open(args.input, "r") as file:
target_html = file.read()
output_html = parse_file(target_html, variables, not args.preserve_comments)
# remove empty lines
output_html = re.sub(r"[\t\r ]*\n(?:[\t\r ]*\n)+", r"\n", output_html)
# pdebug(f"Output: {output_html}")
# save
if args.output:
with open(args.output, "w") as file:
file.write(output_html)
else:
print(output_html)
if args.output_deps:
if args.output != args.input:
glob_dependcies.append(args.input)
depfile = generate_dependecy_file(args.output, glob_dependcies)
pdebug("main", f"Writing dependency file to {os.path.abspath(args.output_deps)}: {depfile}")
with open(args.output_deps, "w") as file:
file.write(depfile)
if args.sitemap_temp_file:
with open(args.sitemap_temp_file, "wb") as file:
pickle.dump(Sitemap.urls, file)
else: # sitemap_generate
if not path.isfile(args.sitemap_temp_file):
parser.error(f"Invalid sitemap-temp-file: '{args.sitemap_temp_file}'")
with open(args.sitemap_temp_file, "rb") as file:
Sitemap.urls = pickle.load(file)
sitemap = Sitemap.gen_sidemap()
pdebug("main", f"Writing sitemap to {os.path.abspath(args.sitemap_generate)}")
with open(args.sitemap_generate, "w") as file:
file.write(sitemap)

34
example/nginx.conf Normal file
View File

@ -0,0 +1,34 @@
worker_processes 1;
error_log stderr;
daemon off;
pid nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
access_log /dev/null;
server {
listen 8080;
server_name localhost;
location / {
root build;
index /en/index.html;
}
#error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

1
example/nginx.pid Normal file
View File

@ -0,0 +1 @@
6663

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="#$(lang)">
<!--
Since it is in the COMMON_DIR, this file will be built twice: /en/index.html and /de/index.html
The lang variable is automatically set for this file
#set title Index
#set keywords lang?{*:Index,Example;de:Index,Beispiel}
The include_dir variable is set in the bUwUma Makefile and is automatically set,
#include #$(include_dir)/head.html
-->
<body>
<!--
#sidenav include
#set sidenav_section_name lang?{*:Sidenav Section;de:Abschnitte}
#sidenav section #$(sidenav_section_name)
-->
<main>
<!-- #include #$(include_dir)/index.#$(lang).md -->
</main>
<!--
#set sidenav_section_other lang?{*:Other;de:Sonstiges}
#sidenav section #$(sidenav_section_other):
#if #$(lang) == de
#sidenav custom href="/en/index.html" name="Index Englisch"
#else
#sidenav custom href="/de/index.html" name="Index Deutsch"
#endif
#set sidenav_link lang?{*:Englisch only page;de:Nur englische Seite}
#sidenav custom href="/en/en-only.html" name="#$(sidenav_link)"
#set sidenav_link lang?{*:German only page;de:Nur deutsche Seite}
#sidenav custom href="/de/de-only.html" name="#$(sidenav_link)"
-->
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<!--
The lang variable must be set manually (for the included head), since this site is not in COMMON_DIR
#set lang de
-->
<html lang="#$(lang)">
<!--
#set title Coole Seite
#set description Diese Seite ist nur auf deutsch
#include #$(include_dir)/head.html
-->
<body>
<main>
<h1>Hallo</h1>
<div class="img_text">
#$(example_img)
<p>
Diese Seite ist nur auf deutsch verfügbar
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam.
</p>
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<!--
The lang variable must be set manually (for the included head), since this site is not in COMMON_DIR
#set lang en
-->
<html lang="#$(lang)">
<!--
#set title English only
#include #$(include_dir)/head.html
-->
<body>
<main>
<h1>Hello there!</h1>
<div class="img_text">
#$(example_img)
<p>
This site is only available in engisch.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam.
</p>
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,32 @@
<head>
<!--
If not overridden, these values will be used, depending on the lang variable
#default description lang?{*:Default description,de:Voreingestellte Beschreibung}
#default keywords lang?{*:english,keywords,example;de:deutsche,keywords,beispiel}
#default title lang?{*:bUwUma works!;de:bUwUma funktioniert!}
#default stylesheet /style/main.css
-->
<meta http-equiv="content-type" content="text/html">
<meta charset="utf-8">
<meta name="description" content="#$(description)">
<meta name="keywords" content="#$(keywords)">
<meta name="author" content="Max Mustermann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>#$(title)</title>
<link rel="icon" type="image/x-icon" href="/favicon/favicon.ico">
<link rel="shortcut-icon" href="/favicon/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="/favicon/apple-touch-icon-180x180.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="#$(stylesheet)", id="mainStyleSheet">
<!-- #uncomment #$(extra_head) -->
</head>
<script src="/script/test.js"></script>
<!--
This image can now be included by simply using the variable example_img
#set img_title bUwUma
#set img_alt lang?{*:Example image;de:Beispielbild}
#set example_img <img class="example" src="/resources/example.svg" alt="#$(img_alt)" title="#$(img_title)"/>
-->

View File

@ -0,0 +1,14 @@
# Willkommen auf der Deutschen Version
Das Navigationsmenü wurde anhand der Überschriften erstellt und einige extra links aus `src/common/index.html` wurden eingefügt.
<h2 id="interesting">Dieser Abschnitt ist sehr interessant</h2>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est.
## Diese Überschrift bekommt keinen Eintrag, weil sie keine id hat
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
<!-- #sidenav name Geänderter Name! -->
<h3 id="section3">Dieser Abschnitt hat im Menü einen custom Namen</h3>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam.

View File

@ -0,0 +1,13 @@
# Welcome to the english version
The navigation menu was generated from the headings and some custom links defined in `src/common/index.html`.
<h2 id="interesting">This section is super interesting</h2>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est.
## This heading does not get an entry, because it has no id
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
<!-- #sidenav name Custom named section! -->
<h3 id="section3">This section has a custom name in the menu</h3>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam.

View File

@ -0,0 +1,9 @@
.img_text
img
width: 30%
height: auto
float: left
p
width: 69%
float: left

View File

@ -0,0 +1,12 @@
.sidenav
border: solid 2px blue
ul
width: fit-content
li
display: none
&:hover li
display: block
.menudrop
display: block
&:hover .menudrop, li
display: none

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.700001 12.700001"
version="1.1"
id="svg1"
inkscape:version="1.3.1 (91b66b0783, 2023-11-16, custom)"
sodipodi:docname="example.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="9.3924909"
inkscape:cx="33.537429"
inkscape:cy="32.898621"
inkscape:window-width="1900"
inkscape:window-height="1034"
inkscape:window-x="8"
inkscape:window-y="34"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-86.894793,-82.841363)">
<path
style="fill:#ff0000;stroke-width:0.0981705"
d="M 87.064776,94.98403 99.462221,94.21962 97.232099,83.103959 c -2.136109,2.741712 -5.04605,3.605895 -8.670918,2.735482 0.842445,5.641412 -0.255599,7.531012 -1.496405,9.144589 z"
id="path5"
sodipodi:nodetypes="ccccc" />
<text
xml:space="preserve"
style="font-size:1.57072px;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="90.043213"
id="text1"><tspan
sodipodi:role="line"
style="font-size:1.57072px;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="90.043213"
id="tspan3">bUwUma</tspan><tspan
sodipodi:role="line"
style="font-size:1.57072px;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="92.006615"
id="tspan5">---</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.700001 12.700001"
version="1.1"
id="svg1"
inkscape:version="1.3.1 (91b66b0783, 2023-11-16, custom)"
sodipodi:docname="favicon.png"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="9.3924909"
inkscape:cx="33.537429"
inkscape:cy="32.898621"
inkscape:window-width="1900"
inkscape:window-height="1034"
inkscape:window-x="8"
inkscape:window-y="34"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-86.894793,-82.841363)">
<path
style="fill:#ff0000;stroke-width:0.0981705"
d="M 87.064776,94.98403 99.462221,94.21962 97.232099,83.103959 c -2.136109,2.741712 -5.04605,3.605895 -8.670918,2.735482 0.842445,5.641412 -0.255599,7.531012 -1.496405,9.144589 z"
id="path5"
sodipodi:nodetypes="ccccc" />
<text
xml:space="preserve"
style="font-size:1.57072px;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="90.043213"
id="text1"><tspan
sodipodi:role="line"
style="font-size:1.57072px;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="90.043213"
id="tspan3">bUwUma</tspan><tspan
sodipodi:role="line"
style="font-size:1.57072px;text-align:center;text-anchor:middle;fill:#ffffff;stroke-width:0.0981705"
x="93.495735"
y="92.006615"
id="tspan5">---</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
console.log("This script will indeed be loaded!");

View File

@ -0,0 +1,15 @@
// This sass file be compiled to css
body
background-color: #eee
h1
color: blue
p
border: solid 1px #cc0
@import "images"
@import "sidenav"

View File

@ -24,7 +24,13 @@ sidenav_format = """\
</div>
"""
sidenav_content_link = "<li class=\"sidenav_link\"><a href=\"#link\">#name</a></li>"
sidenav_content_section = "<li class=\"sidenav_section\">#name</li>"
sidenav_content_section = """\
<li class="sidenav_section_name">#name</li>
<li class="sidenav_section_links">
<ul>
#links
</ul>
</li>"""
exit_on_include_failure = False
@ -236,11 +242,40 @@ class Sitemap:
"""
************************************************************ SIDENAV ************************************************************
"""
def replace_and_respect_indent(string, replace, replacement):
"""
replace all occurences of 'replace' with 'replacement', add the whitespaces in front of 'replace' to every line of 'replacement'
"""
i = string.find(replace)
while i >= 0:
line_begin = string.rfind("\n", 0, i) + 1
indent = string[line_begin:i]
string = string[:line_begin] + replacement.replace("\n", "\n" + indent) + string[i+len(replace):]
i = string.find(replace)
return string
class Sidenav:
LINK = 0
SECTION = 1
# 0: link, 1: section
entries: list[tuple[int, str, str]] = []
class Link:
def __init__(self, name: str, link: str):
self.link = link
self.name = name
def __repr__(self):
return f"Link: name={self.name}, link={self.link}"
def get(self):
return sidenav_content_link.replace("#name", self.name).replace("#link", self.link)
class Section:
def __init__(self, name: str):
self.name = name
self.links = []
def add_link(self, link):
self.links.append(link)
def __repr__(self):
return f"Section: name={self.name}"
def get(self):
links = "".join([ link.get() + "\n" for link in self.links ])
return replace_and_respect_indent(sidenav_content_section.replace("#name", self.name), "#links", links)
entries: list[Link|Section] = []
skip_next = False
custom_name = None
@staticmethod
@ -251,10 +286,13 @@ class Sidenav:
if Sidenav.custom_name:
name = Sidenav.custom_name
Sidenav.custom_name = None
Sidenav.entries.append((Sidenav.LINK, name, link))
if len(Sidenav.entries) > 0 and type(Sidenav.entries[-1]) == Sidenav.Section:
Sidenav.entries[-1].add_link(Sidenav.Link(name, link))
else:
Sidenav.entries.append(Sidenav.Link(name, link))
@staticmethod
def addSection(name):
Sidenav.entries.append((Sidenav.SECTION, name, ""))
Sidenav.entries.append(Sidenav.Section(name))
@staticmethod
def setCustomName(name: str):
Sidenav.custom_name = name
@ -264,26 +302,8 @@ class Sidenav:
@staticmethod
def generate() -> str:
pdebug("Sidenav.generate", f"found the following entries: {Sidenav.entries}")
sidenav:list[str] = sidenav_format.split('\n')
content_i = -1
for i in range(len(sidenav)): # find in which line the entries need to be placed
if "#sidenav-content" in sidenav[i]:
content_i = i
break
if content_i >= 0:
indent = sidenav.pop(content_i).replace("#sidenav-content", "")
added_links = []
for i in reversed(range(len(Sidenav.entries))):
entry = Sidenav.entries[i]
if entry[0] == Sidenav.LINK:
if entry[2] in added_links: continue # no duplicates
added_links.append(entry[2])
sidenav.insert(content_i, indent + sidenav_content_link.replace("#name", entry[1]).replace("#link", entry[2]))
else:
sidenav.insert(content_i, indent + sidenav_content_section.replace("#name", entry[1]))
sidenav_s = ""
for line in sidenav: sidenav_s += line + "\n" # cant use "".join because of newlines
return sidenav_s
entries = "".join([entry.get() + "\n" for entry in Sidenav.entries])
return replace_and_respect_indent(sidenav_format, "#sidenav-content", entries)
@staticmethod
def cmd_sidenav(args:str, variables:dict[str,str]) -> str:
space = args.find(" ")

View File

@ -3,13 +3,9 @@ error_log stderr;
daemon off;
pid nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
@ -20,13 +16,9 @@ http {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /home/user/www/;
index de/index.html;
index /en/index.html;
}
#error_page 404 /404.html;