diff --git a/example/.dependencies/de/de-only.html.d b/example/.dependencies/de/de-only.html.d new file mode 100644 index 0000000..3d2a3a8 --- /dev/null +++ b/example/.dependencies/de/de-only.html.d @@ -0,0 +1 @@ +build/de/de-only.html: src/include/head.html src/de/de-only.html \ No newline at end of file diff --git a/example/.dependencies/de/index.html.d b/example/.dependencies/de/index.html.d new file mode 100644 index 0000000..aea605f --- /dev/null +++ b/example/.dependencies/de/index.html.d @@ -0,0 +1 @@ +build/de/index.html: src/include/head.html src/include/index.de.md src/common/index.html \ No newline at end of file diff --git a/example/.dependencies/en/en-only.html.d b/example/.dependencies/en/en-only.html.d new file mode 100644 index 0000000..eaaf51b --- /dev/null +++ b/example/.dependencies/en/en-only.html.d @@ -0,0 +1 @@ +build/en/en-only.html: src/include/head.html src/en/en-only.html \ No newline at end of file diff --git a/example/.dependencies/en/index.html.d b/example/.dependencies/en/index.html.d new file mode 100644 index 0000000..4536967 --- /dev/null +++ b/example/.dependencies/en/index.html.d @@ -0,0 +1 @@ +build/en/index.html: src/include/head.html src/include/index.en.md src/common/index.html \ No newline at end of file diff --git a/example/.dependencies/style/main.css.d b/example/.dependencies/style/main.css.d new file mode 100644 index 0000000..b81d5cc --- /dev/null +++ b/example/.dependencies/style/main.css.d @@ -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 diff --git a/example/.sitemap.pkl b/example/.sitemap.pkl new file mode 100644 index 0000000..e2ecf72 --- /dev/null +++ b/example/.sitemap.pkl @@ -0,0 +1 @@ +€}”. \ No newline at end of file diff --git a/example/Makefile b/example/Makefile new file mode 100644 index 0000000..e76047f --- /dev/null +++ b/example/Makefile @@ -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 diff --git a/example/build/de/de-only.html b/example/build/de/de-only.html new file mode 100644 index 0000000..8d21727 --- /dev/null +++ b/example/build/de/de-only.html @@ -0,0 +1,32 @@ + + + + + + + + + + Coole Seite + + + + + + + + + + +
+

Hallo

+
+ Beispielbild +

+ 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. +

+
+
+ + diff --git a/example/build/de/index.html b/example/build/de/index.html new file mode 100644 index 0000000..0bf66fa --- /dev/null +++ b/example/build/de/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + Index + + + + + + + + + + +
+ +
+
+

Willkommen auf der Deutschen Version

+

Das Navigationsmenü wurde anhand der Überschriften erstellt und einige extra links aus src/common/index.html wurden eingefügt.

+

Dieser Abschnitt ist sehr interessant

+

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.

+

Dieser Abschnitt hat im Menü einen custom Namen

+

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.

+
+ + diff --git a/example/build/en/en-only.html b/example/build/en/en-only.html new file mode 100644 index 0000000..a6df37d --- /dev/null +++ b/example/build/en/en-only.html @@ -0,0 +1,32 @@ + + + + + + + + + + English only + + + + + + + + + + +
+

Hello there!

+
+ Example image +

+ 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. +

+
+
+ + diff --git a/example/build/en/index.html b/example/build/en/index.html new file mode 100644 index 0000000..04c020e --- /dev/null +++ b/example/build/en/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + Index + + + + + + + + + + +
+ +
+
+

Welcome to the english version

+

The navigation menu was generated from the headings and some custom links defined in src/common/index.html.

+

This section is super interesting

+

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.

+

This section has a custom name in the menu

+

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.

+
+ + diff --git a/example/build/favicon/android-chrome-192x192.png b/example/build/favicon/android-chrome-192x192.png new file mode 100644 index 0000000..3166f32 Binary files /dev/null and b/example/build/favicon/android-chrome-192x192.png differ diff --git a/example/build/favicon/android-chrome-512x512.png b/example/build/favicon/android-chrome-512x512.png new file mode 100644 index 0000000..7264364 Binary files /dev/null and b/example/build/favicon/android-chrome-512x512.png differ diff --git a/example/build/favicon/apple-touch-icon-180x180.png b/example/build/favicon/apple-touch-icon-180x180.png new file mode 100644 index 0000000..8605ddd Binary files /dev/null and b/example/build/favicon/apple-touch-icon-180x180.png differ diff --git a/example/build/favicon/favicon-16x16.png b/example/build/favicon/favicon-16x16.png new file mode 100644 index 0000000..382b169 Binary files /dev/null and b/example/build/favicon/favicon-16x16.png differ diff --git a/example/build/favicon/favicon-32x32.png b/example/build/favicon/favicon-32x32.png new file mode 100644 index 0000000..1846c29 Binary files /dev/null and b/example/build/favicon/favicon-32x32.png differ diff --git a/example/build/favicon/favicon-48x48.png b/example/build/favicon/favicon-48x48.png new file mode 100644 index 0000000..b4fcd78 Binary files /dev/null and b/example/build/favicon/favicon-48x48.png differ diff --git a/example/build/favicon/favicon.ico b/example/build/favicon/favicon.ico new file mode 100644 index 0000000..bec0710 Binary files /dev/null and b/example/build/favicon/favicon.ico differ diff --git a/example/build/favicon/mstile-150x150.png b/example/build/favicon/mstile-150x150.png new file mode 100644 index 0000000..40fdd20 Binary files /dev/null and b/example/build/favicon/mstile-150x150.png differ diff --git a/example/build/resources/example.svg b/example/build/resources/example.svg new file mode 100644 index 0000000..b3ae576 --- /dev/null +++ b/example/build/resources/example.svg @@ -0,0 +1,64 @@ + + + + + + + + + bUwUma--- + + diff --git a/example/build/resources/favicon.png b/example/build/resources/favicon.png new file mode 100644 index 0000000..905b16a --- /dev/null +++ b/example/build/resources/favicon.png @@ -0,0 +1,64 @@ + + + + + + + + + bUwUma--- + + diff --git a/example/build/script/test.js b/example/build/script/test.js new file mode 100644 index 0000000..5546561 --- /dev/null +++ b/example/build/script/test.js @@ -0,0 +1 @@ +console.log("This script will indeed be loaded!"); diff --git a/example/build/sitemap.xml b/example/build/sitemap.xml new file mode 100644 index 0000000..0f8724e --- /dev/null +++ b/example/build/sitemap.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/example/build/style/main.css b/example/build/style/main.css new file mode 100644 index 0000000..44188d5 --- /dev/null +++ b/example/build/style/main.css @@ -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 */ diff --git a/example/html-preprocessor b/example/html-preprocessor new file mode 100755 index 0000000..b376783 --- /dev/null +++ b/example/html-preprocessor @@ -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 = """\ +
+ +
+""" +sidenav_content_link = "
  • #name
  • " +sidenav_content_section = """\ +
  • #name
  • +""" + +exit_on_include_failure = False + +sitemap_begin = """\ + +\n""" +sitemap_end = "" + +""" +************************************************************ REGULAR EXPRESSIONS ************************************************************ +""" +# SIDENAV +# heading with id +re_sidenav_heading = r"(.+)" +# 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 = "" + + +""" +************************************************************ 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"(.+)", 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"\n\t{self.url}" + if self.priority is not None: s += f"\n\t{self.priority}" + if self.changefreq is not None: s += f"\n\t{self.changefreq}" + if self.lastmod is not None: s += f"\n\t{self.lastmod}" + s += "\n" + 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"" + 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"" + 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_]+:.+))\}" + # + 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"" +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 + " + + + + +
    + +
    + + + + diff --git a/example/src/de/de-only.html b/example/src/de/de-only.html new file mode 100644 index 0000000..b69993e --- /dev/null +++ b/example/src/de/de-only.html @@ -0,0 +1,26 @@ + + + + + + +
    +

    Hallo

    +
    + #$(example_img) +

    + 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. +

    +
    +
    + + + diff --git a/example/src/en/en-only.html b/example/src/en/en-only.html new file mode 100644 index 0000000..01800da --- /dev/null +++ b/example/src/en/en-only.html @@ -0,0 +1,25 @@ + + + + + + +
    +

    Hello there!

    +
    + #$(example_img) +

    + 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. +

    +
    +
    + + + diff --git a/example/src/include/head.html b/example/src/include/head.html new file mode 100644 index 0000000..601dc26 --- /dev/null +++ b/example/src/include/head.html @@ -0,0 +1,32 @@ + + + + + + + + + #$(title) + + + + + + + + + + + + diff --git a/example/src/include/index.de.md b/example/src/include/index.de.md new file mode 100644 index 0000000..c0ec312 --- /dev/null +++ b/example/src/include/index.de.md @@ -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. + +

    Dieser Abschnitt ist sehr interessant

    +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. + + +

    Dieser Abschnitt hat im Menü einen custom Namen

    +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. diff --git a/example/src/include/index.en.md b/example/src/include/index.en.md new file mode 100644 index 0000000..c6fa9ef --- /dev/null +++ b/example/src/include/index.en.md @@ -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`. + +

    This section is super interesting

    +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. + + +

    This section has a custom name in the menu

    +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. diff --git a/example/src/include/style/images.sass b/example/src/include/style/images.sass new file mode 100644 index 0000000..614d7af --- /dev/null +++ b/example/src/include/style/images.sass @@ -0,0 +1,9 @@ +.img_text + img + width: 30% + height: auto + float: left + + p + width: 69% + float: left diff --git a/example/src/include/style/sidenav.sass b/example/src/include/style/sidenav.sass new file mode 100644 index 0000000..fe9f3a3 --- /dev/null +++ b/example/src/include/style/sidenav.sass @@ -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 diff --git a/example/src/resources/example.svg b/example/src/resources/example.svg new file mode 100644 index 0000000..b3ae576 --- /dev/null +++ b/example/src/resources/example.svg @@ -0,0 +1,64 @@ + + + + + + + + + bUwUma--- + + diff --git a/example/src/resources/favicon.png b/example/src/resources/favicon.png new file mode 100644 index 0000000..905b16a --- /dev/null +++ b/example/src/resources/favicon.png @@ -0,0 +1,64 @@ + + + + + + + + + bUwUma--- + + diff --git a/example/src/script/test.js b/example/src/script/test.js new file mode 100644 index 0000000..5546561 --- /dev/null +++ b/example/src/script/test.js @@ -0,0 +1 @@ +console.log("This script will indeed be loaded!"); diff --git a/example/src/style/main.sass b/example/src/style/main.sass new file mode 100644 index 0000000..bc74a05 --- /dev/null +++ b/example/src/style/main.sass @@ -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"