commit 63d353a9c731f969c44ae5d30574b30907fe8e17
parent 3b4edf229c88f6d389b0439aff0da914c6747c52
Author: Vincent Forest <vincent.forest@meso-star.com>
Date: Sun, 31 Aug 2025 16:27:02 +0200
Rewriting the generate_header function in C
This new implementation speeds up execution and improves robustness. Its
execution time is reduced by approximately two orders of magnitude,
while the overall generation of an HTML page is reduced by approximately
one order of magnitude.
This new implementation also improves the reliability of this tool,
particularly with regard to the processing of paths and file format
compliance.
Diffstat:
8 files changed, 911 insertions(+), 249 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -15,3 +15,4 @@ man
.index
solstice.html
high-tune
+generate_header
diff --git a/Makefile b/Makefile
@@ -25,7 +25,14 @@ MD2HTML=md2html
# Additional resources to install
RESOURCES=fonts
+# Compilation/Linker flags
+CFLAGS=-std=c99 -Wall -Wextra -Os
+LDFLAGS=-Wl,-z,relro,-z,now -pie
+
+GEN_HEADER=tools/generate_header
+
LINT=$(HTML:.html=.lint) $(SH:.sh=.shlint) templates/redirect.lint
+MD=$(SHTML:.sh=.md)
default: build
@@ -41,12 +48,16 @@ hooks index:
build__: $(HTML) $(HOOK:.sh=.hook)
+# Make generate_header a dependency of hook execution (since they can use it)
+$(HOOK:.sh=.hook): $(GEN_HEADER)
+
clean__:
rm -f .hooks
rm -f $(HTML)
rm -f $(SHTML:.sh=.md)
distclean__: clean__
+ rm -f $(GEN_HEADER) $(GEN_HEADER).o
install__: $(HTML) $(SIG)
@rsync -avzrR --delete-after --progress \
@@ -54,9 +65,18 @@ install__: $(HTML) $(SIG)
$(PREFIX)
################################################################################
+# Tools
+################################################################################
+.c.o:
+ $(CC) $(CFLAGS) -c $< -o $@
+
+tools/generate_header: tools/generate_header.o
+ $(CC) $(CFLAGS) -o $@ tools/generate_header.o $(LDFLAGS)
+
+################################################################################
# Generate content
################################################################################
-$(HTML): scripts/generate_header.sh menu.tsv templates/footer.html
+$(HTML): $(GEN_HEADER) menu.tsv templates/footer.html
.sh.md:
@cd -- "$$(dirname "$<")"; \
@@ -65,7 +85,7 @@ $(HTML): scripts/generate_header.sh menu.tsv templates/footer.html
.md.html:
@echo "Building $@"
@{ \
- $(SHELL) scripts/generate_header.sh $${PWD}/$@; \
+ $(GEN_HEADER) $${PWD}/$@; \
$(MD2HTML) $<; \
cat templates/footer.html; \
} > $@
diff --git a/htrdr/hooks/01-generate-man.sh b/htrdr/hooks/01-generate-man.sh
@@ -32,7 +32,7 @@ man2html() # output_filename
{
{
cd ..
- sh ./scripts/generate_header.sh "${OLDPWD##*/}/$1"
+ tools/generate_header "${OLDPWD##*/}/$1"
mandoc -O man=../man%S/%N.%S.html,fragment -I os=UNIX -T html
cat ./templates/footer.html
cd "${OLDPWD}"
diff --git a/schiff/hooks/01-generate-man.sh b/schiff/hooks/01-generate-man.sh
@@ -32,7 +32,7 @@ man2html() # output_filename
{
{
cd ..
- sh ./scripts/generate_header.sh "${OLDPWD##*/}/$1"
+ tools/generate_header "${OLDPWD##*/}/$1"
sh ./scripts/convert_man.sh
cat ./templates/footer.html
cd "${OLDPWD}"
diff --git a/scripts/generate_header.sh b/scripts/generate_header.sh
@@ -1,243 +0,0 @@
-#!/bin/sh
-
-# Copyright (C) 2017-2025 |Méso|Star> (contact@meso-star.com)
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-set -e
-
-. "./scripts/meso-web.sh"
-
-########################################################################
-# Helper functions
-########################################################################
-# List the indexed content in TSV format. Each line contains the index
-# label, followed by the indexed content, and possibly an indicator of
-# the language used.
-#
-# The input data is the content of an index.tsv file, submitted on
-# standard input.
-indexed_content()
-(
- strip_dummy | while read -r _line; do
- _langs="$(echo "${_line}" | cut -d' ' -f3)"
-
- if [ -z "${_langs}" ]; then
- # There is no lang field defined: print the line as it
- printf '%s\n' "${_line}"
-
- else
- # The lang field is defined. Duplicate the line as many times as
- # there are languages listed. Define the content URI by replacing
- # the @LANG@ string with the language value.
- echo "${_langs}" \
- | sed 's/:/\n/g' \
- | while read -r _translation; do
- echo "${_line}" \
- | sed -e "s/\t[^\t]\{1,\}$/\t${_translation}/g" \
- -e "s/@LANG@/${_translation}/g"
- done
- fi
- done
-)
-
-print_head()
-{
- printf '<!DOCTYPE html>\n'
- printf '<html lang="%s">\n' "${lang}"
- printf '<head>\n'
- printf ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8">\n'
- printf ' <meta name="viewport" content="width=device-width, initial-scale=1">\n'
- printf ' <title>|M|S> %s</title>\n' "${label}"
- printf ' <link rel="stylesheet" title="default" href="%smeso.css">\n' "${root}"
- printf '</head>\n'
- printf '<body>\n'
-}
-
-# Print top-level menu
-print_menu1()
-(
- printf '<div id="menu">\n'
-
- _separator=""
- strip_dummy < "${worktree}/menu.tsv" | while read -r _i; do
-
- # Retrieve the menu label and its associated target
- _label="$(echo "${_i}" | cut -d' ' -f1)"
- _target="$(echo "${_i}" | cut -d' ' -f2)"
-
- echo "${_separator}" && _separator='  | '
-
- if [ "${_target}" = "${section}" ]; then
- printf ' %s\n' "${_label}"
-
- elif [ -f "${worktree}/${_target}/index.tsv" ]; then
- # Get the default page of the section, i.e., the first entry in
- # its index. The menu is a link to it.
- _uri="$(indexed_content < "${worktree}/${_target}/index.tsv" \
- | head -1 | cut -d' ' -f2)"
-
- printf ' <a href="%s%s/%s">%s</a>\n' \
- "${root}" "${_target}" "${_uri}" "${_label}"
-
- # The target is an URL
- elif echo "${_target}" | grep -qe "^[^:]\{1,\}://"; then
- printf ' <a href="%s">%s</a>\n' "${_target}" "${_label}"
-
- # The target is neither a local directory nor a URL. Let's assume
- # it is a remote directory, still relative to the current root once
- # the local content is installed, but managed elsewhere.
- else
- printf ' <a href="%s%s">%s</a>\n' \
- "${root}" "${_target}" "${_label}"
- fi
- done
-
- printf '</div>\n<hr>\n'
-)
-
-# Print the list of translation choices
-# Available langs are submitted on standard input
-print_translations() # uri_template (i.e. URI generic to the lang)
-(
- _uri_template="$1"
-
- printf ' <span style="float: right;">\n'
-
- _separator=""
- while read -r _lang; do
-
- _translation="$(echo "${_uri_template}" | sed "s/@LANG@/${_lang}/g")"
-
- echo "${_separator}" && _separator='  / '
-
- if [ "${content}" = "${_translation}" ]; then
- printf ' <span class="cur">%s</span>\n' "${_lang}"
- else
- printf ' <a href="%s%s/%s">%s</a>\n' \
- "${root}" "${section}" "${_translation}" "${_lang}"
- fi
- done
-
- printf ' </span>\n'
-)
-
-# Print second-level menu
-print_menu2()
-(
-
- printf '<div id="sub-menu">\n'
-
- _separator=""
- strip_dummy < "${worktree}/${section}/index.tsv" | while read -r _i; do
-
- _label="$(echo "${_i}" | cut -d' ' -f1)"
- _uri_template="$(echo "${_i}" | cut -d' ' -f2)"
- _langs="$(echo "${_i}" | cut -d' ' -f3)"
- _lang_default="$(echo "${_langs}" | cut -d ':' -f1)"
-
- _uri="$(echo "${_uri_template}" | sed "s/@LANG@/${lang}/g")"
-
- echo "${_separator}" && _separator='  . '
-
- if [ "${_uri}" = "${content}" ]; then
- # This is the current web page
- printf ' <span class="cur">%s</span>\n' "${_label}"
-
- # Print links to translations if available
- if [ -n "${_langs}" ]; then
- echo "${_langs}" \
- | sed 's/:/\n/g' \
- | print_translations "${_uri_template}"
- fi
-
- elif echo "${_uri}" | grep -qe "^http[s]\{0,1\}://"; then
- # The entry links to an http[s] URL
- printf ' <a href="%s">%s</a>\n' "${_uri}" "${_label}"
-
- else
- # The entry links to a local web page for the section
- _uri="$(echo "${_uri_template}" | sed "s/@LANG@/${_lang_default}/g")"
- printf ' <a href="%s%s/%s">%s</a>\n' \
- "${root}" "${section}" "${_uri}" "${_label}"
- fi
- done
-
- printf '</div>\n'
-)
-
-########################################################################
-# The script
-########################################################################
-if [ "$#" -lt 1 ]; then
- >&2 printf 'usage: %s file\n' "${0##*/}"
- exit 1
-fi
-
-# Define the absolute path of the working directory, i.e. the root
-# directory of the site, and enter it.
-worktree="$(absdir "$(dirname "$0")/../")"
-cd "${worktree}"
-
-# Define the section directory where the input file is located, i.e., the
-# subdirectory in the working directory.
-section="$(absdir "$1")"
-section="$(printf '%s\n' "${section}" | sed "s;^${worktree}[/]\{0,\};;g")"
-section="${section%%/*}"
-
-if [ -z "${section}" ]; then
- >&2 printf \
- '%s: unable to extract the section directory from file %s\n' \
- "${0##*/}" "$1"
- exit 1
-fi
-
-# Find the menu entry corresponding to the section directory
-entry="$(strip_dummy < "${worktree}/menu.tsv" \
- | sed -n "/\t${section}/p")"
-
-if [ -z "${entry}" ]; then
- >&2 printf \
-'%s: the %s directory of the %s file is not a valid menu entry '\
-'(see menu.tsv)\n' "${0##*/}" "${section}" "$1"
- exit 1
-fi
-
-label="$(echo "${entry}" | cut -d' ' -f1)"
-
-if ! [ -e "${section}/index.tsv" ]; then
- >&2 printf \
- '%s: unable to find the index.tsv file in the directory %s\n' \
- "${0##*/}" "${section}"
- exit 1
-fi
-
-# Define whether the entry corresponds to a subsection index, i.e.,
-# whether it is the file to be displayed when entering the subsection.
-content="$(basename "$1")"
-subentry="$(indexed_content < "${worktree}/${section}/index.tsv" \
- | sed -n "/\t${content}/p")"
-
-if [ -n "${subentry}" ]; then
- lang="$(echo "${subentry}" | cut -d' ' -f3 | cut -d':' -f1)"
-fi
-
-lang="${lang:-en}"
-root="$(relpath_to_dir "$1" "${worktree}")"
-
-print_head
-print_menu1
-print_menu2
-
-printf '<div id="content">\n'
diff --git a/solstice/hooks/01-generate-man.sh b/solstice/hooks/01-generate-man.sh
@@ -32,7 +32,7 @@ man2html() # output_filename
{
{
cd ..
- sh ./scripts/generate_header.sh "${OLDPWD##*/}/$1"
+ tools/generate_header "${OLDPWD##*/}/$1"
sh ./scripts/convert_man.sh
cat ./templates/footer.html
cd "${OLDPWD}"
diff --git a/stardis/hooks/01-generate-man.sh b/stardis/hooks/01-generate-man.sh
@@ -39,7 +39,7 @@ man2html() # output_filename
{
cd ..
- sh ./scripts/generate_header.sh "${OLDPWD##*/}/$1"
+ tools/generate_header "${OLDPWD##*/}/$1"
mandoc -O man=../man%S/%N.%S.html,fragment -I os=UNIX -T html
cat ./templates/footer.html
cd "${OLDPWD}"
diff --git a/tools/generate_header.c b/tools/generate_header.c
@@ -0,0 +1,884 @@
+/* Copyright (C) 2017-2025 |Méso|Star> (contact@meso-star.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#define _XOPEN_SOURCE 500 /* realpath */
+#define _POSIX_C_SOURCE 200809L /* stndup */
+
+#include <assert.h>
+#include <errno.h>
+#include <libgen.h> /* basename & dirname */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define MENU_FILENAME "menu.tsv" /* File that lists the menu entries */
+#define MENU_MAX_ENTRIES 16 /* Maximum number of menu entries */
+
+#define INDEX_FILENAME "index.tsv" /* List the indexed content of a section */
+#define INDEX_MAX_ENTRIES 32 /* Maximum number of indexed items per section */
+
+#define DEFAULT_LANG "en" /* Default language */
+
+static const char LANG[] = "@LANG@";
+const size_t SZLANG = sizeof(LANG) - 1/* '\0' */;
+
+static const char* g_cmd = NULL; /* Command name. Setuped on start-up */
+
+struct menu {
+ struct mentry {
+ char* label;
+ char* section;
+ char* mem__; /* Allocated memory */
+ } items[MENU_MAX_ENTRIES];
+
+ int nitems;
+};
+static const struct menu MENU_NULL = {0};
+
+struct index {
+ struct ientry {
+ char* label;
+ char* uri;
+ char* lang;
+ char* template; /* Original URI, i.e. not expanded */
+ char* lang_list; /* List of available languages */
+ char* mem__; /* Allocated memory */
+ } items[INDEX_MAX_ENTRIES];
+
+ int nitems;
+};
+static const struct index INDEX_NULL = {0};
+
+struct str {
+ char* buf;
+ size_t sz; /* Capacity */
+};
+static const struct str STR_NULL = {0};
+
+/*******************************************************************************
+ * Helper functions
+ ******************************************************************************/
+static void
+str_release(struct str* s)
+{
+ assert(s);
+ if(s->buf) free(s->buf);
+ *s = STR_NULL;
+}
+
+/* Read from fp the next non empty line. Returns null if an error occurs */
+static char*
+rline(FILE* fp, struct str* s, int* lines_count/*#read lines*/)
+{
+ size_t nlines = 0;
+
+ do {
+ size_t n = 0; /* #read chars for the current line */
+
+ do {
+ char *ptr = NULL;
+
+ /* Allocate memory space */
+ if(!s->buf) s->buf = realloc(s->buf, s->sz=128);
+ else if(n > 0) s->buf = realloc(s->buf, s->sz+=32);
+ if(!s->buf) { perror("realloc"); goto error; }
+
+ /* Read line data */
+ ptr = fgets(s->buf+n, s->sz - n, fp);
+ if(!ptr) {
+ if(ferror(fp)) { perror("fgets"); goto error; }
+ s->buf[n] = '\0'; /* No read char */
+ }
+
+ n = strlen(s->buf); /* Update the number of read chars */
+
+ } while(!strrchr(s->buf, '\n') && !feof(fp)); /* Are there any chars left? */
+ ++nlines;
+
+ s->buf[strcspn(s->buf, "\n\r#\0")] = '\0'; /* Remove new line and comments */
+
+ } while(strlen(s->buf)==strspn(s->buf, " \t") && !feof(fp)); /* Empty line */
+
+exit:
+ *lines_count = nlines;
+ return s->buf;
+error:
+ str_release(s);
+ goto exit;
+}
+
+static void
+menu_release(struct menu* menu)
+{
+ assert(menu);
+ for(int i=0; i < menu->nitems; ++i) {
+ if(menu->items[i].mem__) free(menu->items[i].mem__);
+ }
+ *menu = MENU_NULL;
+}
+
+static int
+menu_load(struct menu* menu, const char* workdir)
+{
+ struct str s = STR_NULL;
+ char* line = NULL;
+ FILE* fp = NULL;
+ int iline = 0; /* Line index */
+ int n = 0;
+ int err = 0;
+
+ assert(menu && workdir);
+
+ *menu = MENU_NULL;
+
+ /* Open the menu file */
+ if(!(fp=fopen(MENU_FILENAME, "r"))) {
+ perror(__func__);
+ fprintf(stderr,
+ "%s: expecting the \"%s\" file in the \"%s\" working directory\n",
+ g_cmd, MENU_FILENAME, workdir);
+ goto error;
+ }
+
+ for(line=rline(fp, &s, &n); line && line[0]!='\0'; line=rline(fp, &s, &n)) {
+ struct mentry* item = menu->items + menu->nitems;
+ char* ctx = NULL;
+
+ iline += n;
+
+ if(menu->nitems >= MENU_MAX_ENTRIES) {
+ /* There is too many menu entries to parse */
+ errno = ENOMEM;
+ perror(__func__);
+ goto error;
+ }
+
+ ++menu->nitems;
+
+ /* Duplicate the line. It will be divided into sub-strings that the
+ * items's member variables will point to. */
+ if(!(item->mem__ = strdup(line))) { perror("strdup"); goto error; }
+
+ /* Setup item member variables */
+ if(!(item->label = strtok_r(item->mem__, "\t", &ctx))
+ || !(item->section = strtok_r(NULL, "\t", &ctx))) {
+ fprintf(stderr, "%s:%s:%d: invalid menu entry -- %s\n",
+ g_cmd, MENU_FILENAME, iline, line);
+ goto error;
+ }
+ }
+
+ if(!line) goto error; /* A null lines means there is an error */
+
+exit:
+ if(fp) fclose(fp);
+ str_release(&s);
+ return err;
+error:
+ menu_release(menu);
+ err = 1;
+ goto exit;
+}
+
+/* Resolve a pathname. Unlike realpath, handles paths to non-existent files */
+static char*
+get_realpath(const char* path)
+{
+ char* out = NULL; /* realpath */
+ char* buf = NULL; /* working copy of input path */
+ char* tmp = NULL; /* temporary path */
+ char *p0, *p1;
+
+ if((out = realpath(path, NULL))) goto exit;
+ if(errno != ENOENT) goto error;
+
+ /* The path does not name an existing file */
+
+ /* Ensure that path is absolute or start by "./" or "../" */
+ if(path[0] == '/' || !strncmp(path, "./", 2) || !strncmp(path, "../", 3)) {
+ if(!(buf = strdup(path))) goto error;
+ } else {
+ if(!(buf = malloc(2/*"./"*/ + strlen(path) + 1/*'\0'*/))) goto error;
+ strcpy(buf, "./");
+ strcat(buf, path);
+ }
+
+ /* Remove path components until a path is found that exists */
+ for(p0=strrchr(buf, '/'); p0; p1=p0, *p1='\0', p0=strrchr(buf, '/'), *p1='/') {
+
+ *p0 = '\0';
+ tmp = realpath(buf, NULL);
+ *p0 = '/';
+
+ if(!tmp) continue; /* Keep removing path component */
+
+ /* Concatante the existing path with the remaining compopnents */
+ if(!(out = malloc(strlen(tmp) + strlen(p0) + 1/*'\0'*/))) goto error;
+ strcpy(out, tmp);
+ strcat(out, p0);
+ break;
+ }
+
+exit:
+ if(buf) free(buf);
+ if(tmp) free(tmp);
+ return out;
+error:
+ perror(__func__);
+ if(out) { free(out); out = NULL; }
+ goto exit;
+}
+
+/* Return the relative path from path to workdir.
+ * Path must lie in workdir.
+ * Return null if an error occurs */
+static char*
+get_relpath(const char* workdir, const char* path)
+{
+ const size_t sz = strlen(workdir);
+ const char* ptr = NULL;
+ char* buf = NULL;
+ int n = 0; /* Number of directory to travels */
+ int i;
+
+ if(strncmp(workdir, path, sz) || path[sz] != '/') {
+ fprintf(stderr, "%s:%s: %s is not a subpath of %s\n",
+ g_cmd, __func__, path, workdir);
+ goto error;
+ }
+
+ /* Count the number of directories between the workdir and the path */
+ for(n = 0, ptr=path + sz + 1; (ptr=strchr(ptr, '/')); ++n) {
+ /* Successive '/' should be counted as a single '/' */
+ while(++ptr, *ptr=='/');
+ }
+
+ if(!(buf = malloc(n*3/*"../"*/+1/*'\0'*/))) { perror("malloc"); goto error; }
+ for(i=0, buf[0]='\0'; i<n; strcat(buf, "../"), ++i);
+
+exit:
+ return buf;
+error:
+ if(buf) { free(buf); buf = NULL; }
+ goto exit;
+}
+
+/* Returns a pointer to a new index file in which the @LANG@ macro is resolved.
+ * Each entry in the original index file containing an @LANG@ macro is
+ * duplicated as many times as there are languages listed for that entry. In
+ * each of them, the @LANG@ macro is replaced by a value from the language in
+ * the order in which the languages are defined in the original index file. Then
+ * follow the language used to expand the @LANG@ macro and finally the
+ * original uri and the list of languages.
+ *
+ * For example, the following original string :
+ *
+ * Index\tindex-@LANG@.html\tfr:en\n
+ *
+ * is resolved in
+ *
+ * Index\tindex-fr.html\tfr\tindex-@LANG@.html\tfr:en\n
+ * Index\tindex-en.html\ten\tindex-@LANG@.html\tfr:en\n
+ *
+ * Return null if an error occurs */
+static FILE*
+resolve_index(const char* path)
+{
+ char* lang_list = NULL;
+
+ FILE* fp = NULL;
+ FILE* fp2 = NULL;
+
+ struct str s = STR_NULL;
+ char* line = NULL;
+ int iline = 0; /* Line index */
+ int n = 0;
+
+ assert(path);
+
+ /* Open the original index */
+ if(!(fp = fopen(path, "r"))) {
+ fprintf(stderr,"%s: unable to find the file %s\n", g_cmd, path);
+ goto error;
+ }
+
+ /* Open the resolved index */
+ if(!(fp2 = tmpfile())) { perror("tmpfile"); goto error; }
+
+ for(line=rline(fp, &s, &n); line && line[0]!='\0'; line=rline(fp, &s, &n)) {
+ char *label, *uri, *langs, *ctx;
+
+ iline += n;
+
+ label = strtok_r(line, "\t", &ctx);
+ uri = strtok_r(NULL, "\t", &ctx);
+ langs = strtok_r(NULL, "\t", &ctx);
+
+ if(!label || !uri) {
+ fprintf(stderr, "%s:%s:%d: invalid index entry\n", g_cmd, path, iline);
+ goto error;
+ }
+
+ if(!langs) {
+ /* There is no several languages, just write the original entry */
+ fprintf(fp2, "%s\t%s\n", label, uri);
+
+ } else {
+ char* l = NULL;
+
+ /* Keep a copy of the string listing the available languages. This will be
+ * the last field of the expanded entries */
+ if(lang_list) free(lang_list);
+ if(!(lang_list=strdup(langs))) { perror("strdup"); goto error; }
+
+ /* Add as many index entries as there are languages available */
+ for(l=strtok_r(langs, ":", &ctx); l; l=strtok_r(NULL, ":", &ctx)) {
+ char *ptr0, *ptr1;
+
+ fprintf(fp2, "%s\t", label); /* Firstly, print the entry label */
+
+ /* Then print the expanded URI, i.e. the URI in which each instance of
+ * the @LANG@ macro is replaced by the current language */
+ for(ptr0=uri; (ptr1 = strstr(ptr0, LANG)); ptr0=ptr1+SZLANG) {
+ *ptr1 = '\0';
+ fprintf(fp2, "%s%s", ptr0, l);
+ *ptr1 = LANG[0];
+ }
+ fprintf(fp2, "%s\t", ptr0); /* Rest of the URI */
+
+ /* Print the URI language as the 3rd field */
+ fprintf(fp2, "%s\t", l);
+
+ /* And finally, print the original URI (4th field) and the list of
+ * available languages (5th field). */
+ fprintf(fp2, "%s\t%s\n", uri, lang_list);
+ }
+ }
+ }
+
+ rewind(fp2);
+
+exit:
+ str_release(&s);
+ if(fp) fclose(fp);
+ if(lang_list) free(lang_list);
+ return fp2;
+error:
+ if(fp2) { fclose(fp2); fp2 = NULL; }
+ goto exit;
+}
+
+static void
+index_release(struct index* index)
+{
+ assert(index);
+ for(int i=0; i < index->nitems; ++i) {
+ if(index->items[i].mem__) free(index->items[i].mem__);
+ }
+ *index = INDEX_NULL;
+}
+
+static int
+index_load(struct index* index, const char* section)
+{
+ struct str s = STR_NULL;
+ FILE* fp = NULL;
+ char* path = NULL;
+ char* line = NULL;
+ size_t sz = 0;
+ int i = 0;
+ int n = 0;
+ int err = 0;
+
+ assert(index && section);
+
+ /* Allocate the string to store the absolute path to the index file */
+ sz = 2 /* "./" */
+ + strlen(section) + 1/* '/' */
+ + strlen(INDEX_FILENAME) + 1/* '\0' */;
+ if(!(path = malloc(sz))) { perror("malloc"); goto error; }
+
+ /* Define the absolute path to the index file */
+ i = snprintf(path, sz, "./%s/%s", section, INDEX_FILENAME);
+ if(i >= (int)sz) abort(); /* Unexpected error */
+
+ /* Resolve the index file, i.e. expand the @LANG@ macro */
+ if(!(fp = resolve_index(path))) goto error;
+
+ for(line=rline(fp, &s, &n); line && line[0]!='\0'; line=rline(fp, &s, &n)) {
+ struct ientry* item = index->items + index->nitems;
+ char* ctx = NULL;
+
+ if(index->nitems >= INDEX_MAX_ENTRIES) {
+ /* There is too many menu entries to parse */
+ errno = ENOMEM;
+ perror(__func__);
+ goto error;
+ }
+
+ ++index->nitems;
+
+ /* Duplicate the line. It will be divided into sub-strings that the
+ * items's member variables will point to. */
+ if(!(item->mem__ = strdup(line))) { perror("strdup"); goto error; }
+
+ item->label = strtok_r(item->mem__, "\t", &ctx);
+ item->uri = strtok_r(NULL, "\t", &ctx);
+ item->lang = strtok_r(NULL, "\t", &ctx);
+ item->template = strtok_r(NULL, "\t", &ctx);
+ item->lang_list = strtok_r(NULL, "\t", &ctx);
+
+ if(!item->lang) item->lang = DEFAULT_LANG;
+ if(!item->template) item->template = item->uri;
+ if(!item->lang_list) item->lang_list = DEFAULT_LANG;
+
+ /* Already check when resolving index */
+ assert(item->label && item->uri);
+ }
+
+ if(!line) goto error; /* A null lines means there is an error */
+ if(index->nitems == 0) goto error; /* An empty index is an error */
+
+exit:
+ if(path) free(path);
+ if(fp) fclose(fp);
+ str_release(&s);
+ return err;
+error:
+ index_release(index);
+ err = 1;
+ goto exit;
+}
+
+/* Returns null if no entry is found */
+const struct mentry*
+menu_find(const struct menu* menu, const char* section)
+{
+ int i = 0;
+ assert(menu && section);
+
+ for(i = 0; i < menu->nitems; ++i) {
+ if(!strcmp(menu->items[i].section, section)) break;
+ }
+
+ return i < menu->nitems ? menu->items + i : NULL;
+}
+
+/* Returns null if no entry is found */
+const struct ientry*
+index_find
+ (const struct index* index,
+ const char* file) /* relative to the section */
+{
+ int i = 0;
+ assert(index && file);
+
+ for(i = 0; i < index->nitems; ++i) {
+ if(!strcmp(index->items[i].uri, file)) break;
+ }
+
+ return i < index->nitems ? index->items + i : NULL;
+}
+
+/* Return null if an error occurs */
+static char*
+get_section(const char* workdir, const char* path)
+{
+ const size_t sz = strlen(workdir);
+ char *buf;
+ const char *b, *e;
+
+ assert(workdir);
+
+ if(strncmp(path, workdir, sz) != 0 || path[sz] != '/') {
+ fprintf(stderr, "%s:%s: %s is not a subpath of %s\n",
+ g_cmd, __func__, path, workdir);
+ return NULL;
+ }
+
+ buf = (b = path+sz, *b != '/') || !(e = strchr(++b, '/'))
+ ? strdup(b) : strndup(b, e-b);
+
+ if(!buf) perror(__func__);
+ return buf;
+}
+
+/* Return path relative to workdir/section. path must be a subpath into
+ * workdir/section. Return null if an error occurs */
+static char*
+get_section_path(const char* workdir, const char* section, const char* path)
+{
+ const size_t sz0 = strlen(workdir);
+ const size_t sz1 = strlen(section);
+ char* buf;
+
+ if(strncmp(path, workdir, sz0) != 0
+ || strncmp(path+sz0+1, section, sz1) != 0
+ || path[sz0] != '/'
+ || path[sz0+1+sz1] != '/') {
+ fprintf(stderr, "%s:%s: %s is not a subpath in %s/%s\n",
+ g_cmd, __func__, path, workdir, section);
+ return NULL;
+ }
+
+ buf = strdup(path + sz0 + 1/*'/ */ + sz1 + 1/*'/'*/);
+ if(!buf) perror("strdup");
+
+ return buf;
+}
+
+/* Return null if an error occurs */
+static char*
+get_workdir(void)
+{
+ char *buf, *ptr;
+ size_t sz = 256; /* Initial buffer size */
+
+ for(buf = ptr = NULL; !ptr; sz*=2) {
+
+ if(!(buf = realloc(buf, sz))) {
+ perror("realloc");
+ goto error;
+ }
+
+ if(!(ptr = getcwd(buf, sz)) && errno != ERANGE) {
+ perror("getcwd"); /* Unforeseen error */
+ goto error;
+ }
+ }
+
+exit:
+ return buf;
+error:
+ if(buf) { free(buf); buf = NULL; }
+ goto exit;
+}
+
+static void
+print_head(const char* root, const char* label, const char* lang)
+{
+ assert(root && label && label);
+
+ printf("<!DOCTYPE html>\n");
+ printf("<html lang=\"%s\">\n", lang);
+ printf("<head>\n");
+ printf(" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n");
+ printf(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n");
+ printf(" <title>|M|S> %s</title>\n", label);
+ printf(" <link rel=\"stylesheet\" title=\"default\" href=\"%smeso.css\">\n", root);
+ printf("</head>\n");
+ printf("<body>\n");
+}
+
+static void
+print_menu1
+ (const char* root,
+ const struct menu* menu,
+ const struct mentry* selected_entry)
+{
+ struct index index = INDEX_NULL;
+ assert(root && menu && selected_entry);
+
+ printf("<div id=\"menu\">\n");
+
+ for(int i=0; i < menu->nitems; ++i) {
+ const struct mentry* entry = menu->items + i;
+
+ if(i) printf("  | \n");
+
+ if(selected_entry == entry) {
+ printf(" %s\n", entry->label);
+
+ } else if(!index_load(&index, entry->section)) {
+ printf(" <a href=\"%s%s/%s\">%s</a>\n",
+ root, entry->section, index.items[0].uri, entry->label);
+ index_release(&index);
+
+ } else if(!strncmp(entry->section, "http://", 7)
+ || !strncmp(entry->section, "https://",8)) {
+ /* The target is an URL */
+ printf(" <a href=\"%s\">%s</a>\n", entry->section, entry->label);
+
+ } else {
+ /* The target is neither a local directory nor a URL. Let's assume it is
+ * a remote directory, still relative to the current root once the local
+ * content is installed, but managed elsewhere. */
+ printf(" <a href=\"%s%s\">%s</a>\n", root, entry->section, entry->label);
+ }
+ }
+
+ printf("</div>\n</br>\n");
+}
+
+/* Return null if an error occurs */
+static char*
+expand_lang(char* template, const char* value)
+{
+ char* buf = NULL;
+ char* p0 = NULL;
+ char* p1 = NULL;
+ size_t valen = 0;
+ size_t sz = 0;
+
+ assert(template && value);
+
+ valen = strlen(value);
+
+ /* Calculate the size of the expanded string */
+ for(p0=template; (p1 = strstr(p0, LANG)); p0=p1+SZLANG) {
+ *p1 = '\0';
+ sz += strlen(p0) + valen;
+ *p1 = LANG[0];
+ }
+ sz += strlen(p0)/* Rest of the template string */ + 1/* '\0' */;
+
+ /* Allocate the expanded string */
+ if(!(buf=malloc(sz))) goto error;
+
+ /* Expand the string */
+ buf[0] = '\0';
+ for(p0=template; (p1 = strstr(p0, LANG)); p0=p1+SZLANG) {
+ *p1 = '\0';
+ strcat(buf, p0);
+ strcat(buf, value);
+ *p1 = LANG[0];
+ }
+ strcat(buf, p0); /* Rest of template string */
+
+exit:
+ return buf;
+error:
+ perror(__func__);
+ if(buf) { free(buf); buf = NULL; }
+ goto exit;
+}
+
+static int
+print_translations
+ (const char* root,
+ const char* section,
+ const struct ientry* entry)
+{
+ char* l = NULL;
+ char* lang_list = NULL;
+ char* ctx = NULL;
+ int i = 0;
+ int err = 0;
+ assert(entry);
+
+ /* The URI and template are identical, meaning that no translation is
+ * available for this HTML content since its template has no @LANG@ macro */
+ if(!strcmp(entry->uri, entry->template)) goto exit;
+
+ /* Duplicate the lang list */
+ if(!(lang_list=strdup(entry->lang_list))) { perror("malloc"); goto error; }
+
+ /* Retrieve the 2 first langs in the list */
+ l = strtok_r(lang_list, ":", &ctx);
+ l = strtok_r(NULL, ":", &ctx);
+
+ if(!l) goto exit; /* There is only one lang, i.e. there is no translation */
+
+
+ /* Duplicate the lang list again since it was updated by tokenization */
+ free(lang_list);
+ if(!(lang_list=strdup(entry->lang_list))) { perror("malloc"); goto error; }
+
+ printf(" <span style=\"float: right;\">\n");
+
+ for(i=0, l=strtok_r(lang_list, ":", &ctx); l; l=strtok_r(NULL, ":", &ctx)) {
+
+ if(i) printf("  / \n");
+ i += (i==0);
+
+ if(!strcmp(l, entry->lang)) {
+ /* This is the current translation */
+ printf(" <span class=\"cur\">%s</span>\n", l);
+
+ } else {
+ char* translation = NULL;
+
+ if(!(translation = expand_lang(entry->template, l))) goto error;
+ printf(" <a href=\"%s%s/%s\">%s</a>\n", root, section, translation, l);
+ free(translation);
+ }
+ }
+
+ printf(" </span>\n");
+
+exit:
+ if(lang_list) free(lang_list);
+ return err;
+error:
+ err = 1;
+ goto exit;
+}
+
+static int
+print_menu2
+ (const char* root,
+ const char* section,
+ const struct index* index,
+ const struct ientry* selected_entry) /* Can be NULL */
+{
+ char* uri = NULL;
+ int err = 0;
+
+ assert(root && section && index);
+
+ printf("<div id=\"sub-menu\">\n");
+
+ for(int i = 0, n = index->nitems; i < n; ++i) {
+ const struct ientry* entry = index->items + i;
+ const char* lang = selected_entry ? selected_entry->lang : entry->lang;
+
+ if(i) printf("  . \n");
+
+ /* Resolve the URI lang with the selectedy entry if any or the current entry
+ * otherwise */
+ if(!(uri=expand_lang(entry->template, lang))) goto error;
+
+ /* This is the indexed content */
+ if(selected_entry && !strcmp(uri, selected_entry->uri)) {
+ printf(" <span class=\"cur\">%s</span>\n", selected_entry->label);
+ print_translations(root, section, selected_entry);
+
+ /* The entry links to an http[s] URL */
+ } else if(!strncmp(entry->uri, "http://", 7)
+ || !strncmp(entry->uri, "https://",8)) {
+ printf(" <a href=\"%s\">%s</a>\n", entry->uri, entry->label);
+
+ /* The entry links to a local web page for the section */
+ } else {
+ printf(" <a href=\"%s%s/%s\">%s</a>\n",
+ root, section, entry->uri, entry->label);
+ }
+
+ /* Remove elements with the same template to avoid duplicates when the
+ * indexed content offers multiple translations. Note that the following
+ * loop ensures that the index increment by the main loop remains valid,
+ * i.e., does not skip an entry. */
+ while(i+1 < n && !strcmp(entry->template, index->items[i+1].template)) {
+ i += 1;
+ }
+
+ free(uri);
+ uri = NULL;
+ }
+
+ printf("</div>\n");
+
+exit:
+ if(uri) free(uri);
+ return err;
+error:
+ err = 1;
+ goto exit;
+}
+
+/*******************************************************************************
+ * The command
+ ******************************************************************************/
+int
+main(int argc, char** argv)
+{
+ struct menu menu = MENU_NULL;
+ const struct mentry* mentry = NULL;
+
+ struct index index = INDEX_NULL;
+ const struct ientry* ientry = NULL;
+
+ char* path = NULL; /* absolute path */
+ char* dname = NULL; /* dirname of the absolute path */
+ char* bname = NULL; /* basename of the absolute path */
+ char* workdir = NULL; /* path from where the command is run */
+ char* root = NULL; /* relative path from path to workdir */
+ char* section = NULL; /* section name */
+ char* file = NULL; /* path from the section to the file */
+
+ int res = EXIT_SUCCESS;
+
+ if(argc < 2) {
+ fprintf(stderr, "usage: %s path\n", argv[0]);
+ goto error;
+ }
+
+ g_cmd = argv[0];
+
+ /* Retrieve the absolute path to the input file */
+ if(!(path = get_realpath(argv[1]))) goto error;
+
+ /* Extract the directory and file from the input path. Start with the file, as
+ * the dirname function could add a '\0' character to the path to terminate
+ * the directory name instead of copying it to local storage. */
+ bname = strdup(basename(path));
+ dname = strdup(dirname(path));
+
+ /* Still retrieve the absolute path to the input file since it was overwritten
+ * by the basename/dirname calls */
+ free(path);
+ if(!(path = get_realpath(argv[1]))) goto error;
+
+ /* Get the absolute path of the working directory */
+ if(!(workdir = get_workdir())) goto error;
+
+ /* Get the section to which the file belongs, i.e. the first subdirectory of
+ * its path relative to the working directory */
+ if(!(section = get_section(workdir, dname))) goto error;
+
+ /* Get the path to the file relatively to the section */
+ if(!(file = get_section_path(workdir, section, path))) goto error;
+
+ /* Get the relative path from file to working directory. It is used to
+ * reference the CSS file relatively to the HTML content */
+ if(!(root = get_relpath(workdir, path))) goto error;
+
+ /* Load the menu file of the working directory */
+ if(menu_load(&menu, workdir)) goto error;
+
+ /* Load the index file of the section */
+ if(index_load(&index, section)) goto error;
+
+ /* Find the menu entry corresponding to the section of the input file */
+ if(!(mentry = menu_find(&menu, section))) goto error;
+
+ /* Find the index entry corresponding to the input file. It may be NULL if the
+ * content is not indexed by the section. */
+ ientry = index_find(&index, file);
+
+ /* Print the HTML header for the input content */
+ print_head(root, mentry->label, ientry ? ientry->lang : DEFAULT_LANG);
+ print_menu1(root, &menu, mentry);
+ print_menu2(root, section, &index, ientry);
+
+ printf("<div id=\"content\">\n");
+
+exit:
+ menu_release(&menu);
+ index_release(&index);
+
+ if(path) free(path);
+ if(dname) free(dname);
+ if(bname) free(bname);
+ if(workdir) free(workdir);
+ if(root) free(root);
+ if(section) free(section);
+ if(file) free(file);
+ return res;
+error:
+ res = EXIT_FAILURE;
+ goto exit;
+}