#!/bin/bash
# proposed rewrite of the CEUR-WS team script by Manfred Jeusfeld, CC BY-SA 4.0, see the copy of 2026-10-28 in the history of this file
#
# Check whether the index.html file in a volume has some common errors
# (c) 2020-2026 by Manfred Jeusfeld. This script is made available under the
# Creative Commons Attribution-ShareAlike CC-BY-SA 4.0 license.
#
# One script for both checks: the chair check before publication with the rules A1 to A45
# and the team check after editing with the rules 1 to 44, chosen as before:
# the team check when index.html.orig exists and the first argument is not pre.
# Every grep rule is one line: kind, chair rule, team rule, pattern, message; - means not in that check.
# Exit status: the number of errors found, at most 255, 0 when there is none.
# Same error list as the two scripts before on the 293 most recent volumes of 2026-10-28, in both checks.
#
# Call this script in the directory that contains the index.html file that you want to check.

if [[ $(uname) != "Linux" ]]; then
  echo "This script required Linux. It will not run correctly under MacOS or other operating systems."
  exit 1
fi

missing=()
command -v pdfinfo >/dev/null 2>&1 || missing+=("poppler-utils (pdfinfo)")
command -v perl >/dev/null 2>&1 || missing+=("perl")
command -v java >/dev/null 2>&1 || missing+=("java")
if [ ${#missing[@]} -ne 0 ]; then
  echo "Error: Missing required dependency/dependencies: ${missing[*]}" >&2
  exit 1
fi

if [ ! -f index.html ]; then
  echo "No file index.html in this directory. Call this script in the directory that contains your submissions files."
  exit 1
fi

MODE=chair
if [[ -f index.html.orig && "$1" != "pre" ]]; then
  MODE=team
  echo "Using check-index-errors-after for rules holding for index.html files edited by CEUR-WS team members ... "
else
  echo "check-index-errors V0.95e (2026-10-19) CC-BY-SA 4.0"
  echo ""
fi

ERRCOUNT=0

# rule id of the current mode from the chair and the team id, empty when the rule is not in this mode
rule_id() {
  local id="$2"
  [ "$MODE" = chair ] && id="$1"
  [ "$id" = "-" ] && id=""
  echo "$id"
}

# report an error of the given rule id with its message; count it unless asked not to
report() {
  local id="$1" msg="$2" count="${3:-yes}"
  echo " "
  echo " ==========> ERROR ($id) in index.html!!!!"
  echo "$msg"
  [ "$count" = yes ] && ((ERRCOUNT++))
}

# error when the basic regular expression is found; the matching lines are shown
has() {
  local id
  id=$(rule_id "$1" "$2")
  [ -n "$id" ] || return
  if grep -n -- "$3" index.html; then report "$id" "$4"; fi
}

# error when the extended regular expression is found
has_e() {
  local id
  id=$(rule_id "$1" "$2")
  [ -n "$id" ] || return
  if grep -q -E -- "$3" index.html; then report "$id" "$4"; fi
}

# error when the basic regular expression is missing
lacks() {
  local id
  id=$(rule_id "$1" "$2")
  [ -n "$id" ] || return
  if ! grep -q -- "$3" index.html; then report "$id" "$4"; fi
}

# error when the command, run on index.html, gives output; the output is shown
finds() {
  local id out
  id=$(rule_id "$1" "$2")
  [ -n "$id" ] || return
  out=$(eval "$3")
  if [ -n "$out" ]; then
    echo "$out"
    report "$id" "$4"
  fi
}

# error when the number of lines with one of the labels is not as required: one, at most one, at least one
count() {
  local id kw n bad
  id=$(rule_id "$1" "$2")
  [ -n "$id" ] || return
  for kw in $4; do
    n=$(grep -wc "$kw" index.html)
    case $3 in
      one) bad=$([ "$n" -ne 1 ] && echo y) ;;
      atmostone) bad=$([ "$n" -gt 1 ] && echo y) ;;
      atleastone) bad=$([ "$n" -eq 0 ] && echo y) ;;
    esac
    if [ -n "$bad" ]; then
      echo " ==========> ERROR ($id) in index.html!!!!"
      echo "The label $kw must have $5 value in index.html. It your case, the value is $n."
      [ "$3" = atleastone ] || grep "$kw" index.html
      ((ERRCOUNT++))
    fi
  done
}

regular=yes
grep -q "foaf:" index.html && regular=

if [ "$MODE" = team ]; then
  if [[ $(cd ..; pwd) == $HOME/www ]]; then
    VOLPATH=CEUR-WS.org/$(basename "$PWD")
  else
    echo "WARNING check-index-errors-after: rather call this script only in a directory like Vol-1234 on the CEUR-WS server"
    echo " "
  fi
fi

# the label of a CEUR span may not start or end with a blank
CEURBLANK='class="CEUR(?!TOC)[^"]*">(\s|[^<]*\s</span>)'
# the JSON-LD fence of pyCEURsemantify holds namespace IRIs, which are http by definition
NOFENCE="sed '/^\`\`\`json-ld/,/^\`\`\`\$/d' index.html"
DUPLINKS="grep '<a href=' index.html | grep pdf | sed 's/<a href/\n<a href/g' | sed 's/\"/\"><\/a>\n/2' | grep href | sort | uniq --count | grep -v '1 <a'"
DUPTITLES="grep CEURTITLE index.html | sed 's/<a href/\n<a href/g' | sed 's/\"/\"><\/a>\n/2' | grep href | sort | uniq --count | grep -Ev '^[[:space:]]*1 '"
DUPREFS="grep -o '\"[[:alnum:]_-]\+\.pdf\"' index.html | sort | uniq -c | awk '\$1 > 1'"

has     A1   1   "Workshop on Publishing Papers" "String 'Workshop on Publishing Papers' found in index.html"
has     A2   2   "OPub" "String 'OPub' found in index.html"
has_e   A3   3   "Coeditor|Carlos Nombre|Anne Foé" "String 'Coeditor' or 'Carlos Nombre' or 'Anne Foé' left over from template Vol-XXX found in index.html"
has     A4   4   "<li>" "<li> element without ID found in index.html"
has     A5   5   "workshop-website.org/loc" "wrong URL workshop-website.org/loc found in index.html"
has     -    6   "Vol-XXX" "Some references Vol-XXX to the volume number are not yet replaced in index.html"
has     A8   7   "JJJJ" "Some references JJJJ to the publication year are not yet replaced in index.html"
has     -    8   "YYYY" "Some references YYYY to the event year are not yet replaced in index.html"
has     -    9   "-C</span>" "String -C found; check whether the URN is correctly set in index.html"
has     -    10  "# Instructions" "Instruction comment lines in index.html are not removed"
has     -    11  "<pre>" "All lines containing <pre> should be removed from index.html, not just commented"
has     -    12  "Publishing Papers with CEUR-WS" "wrong proceedings title Publishing Papers with CEUR-WS"
has     A6   13  "Copying permitted for private and academic purposes" "Old non-CC-BY copyright statement; also check the footer in index.html"
has     -    14  "AUXAUTHOR" "Unsupported AUXAUTHOR found in index.html; replace by CEURAUTHOR; similar with AUXTITLE"
lacks   A7   15  "../ceur-ws.css" "wrong stylesheet; must be ../ceur-ws.css"
finds   A9   16  "file index.html | grep UTF-16" "index.html should not be encoded in UTF-16; use iconv to convert to UTF-8"
lacks   A10  17  "urn:nbn:de:0074" "Prefix urn:nbn:de:0074 missing in URN"
lacks   A11  18  "CEURTOC" "DIV section CEURTOC missing; compare to Vol-XXX/index.html"
has     A12  19  "<script" "index.html contains a script; scripts are forbidden"
lacks   -    20  "$VOLPATH" "HTML title must contain the volume path $VOLPATH"
lacks   A13  21  "CEURLOCTIME" "CEURLOCTIME element is missing; compare to Vol-XXX/index.html"
if [ -n "$regular" ]; then
finds   A14  -   "grep -o -P '(?<=CEURAUTHOR\">).*(?=</span)' index.html | grep -v ', Jr.' | grep ','" "Some CEURAUTHOR lines contain commas. Use one line per CEURAUTHOR. A CEURAUTHOR is a single person. Use multiple CEURAUTHOR fields when a paper has multiple authors."
finds   -    22  "grep -o -P '(?<=CEURAUTHOR\">).*(?=</span)' index.html | grep -v ', Jr.' | grep -v ', Sr.' | grep ','" "Some CEURAUTHOR lines contain commas. Use one line per CEURAUTHOR. A CEURAUTHOR is a single person. Use multiple CEURAUTHOR fields when a paper has multiple authors."
finds   A15  23  "grep -o -P '(?<=class=\"CEUR).*(?=</span)' index.html | grep '<' | grep -v '</span'" "Some CEUR span field contains '<' e.g. for a HTML tag. CEUR span fields may not contain HTML tags!"
fi
has     A16  24  "http://ceur-ws.org/ceur-ws.css" "index.html contains absolute path http//ceur-ws.org/ceur-ws.css; must be ../ceur-ws.css"
count   A17  -   one "CEURVERSION CEURVOLTITLE CEURFULLTITLE CEURURN CEURPUBYEAR CEURVOLACRONYM CEURTOC CEURLOCTIME CEURPUBDATE CEURVOLNR" "exactly one"
count   -    25  one "CEURVOLTITLE CEURFULLTITLE CEURURN CEURPUBYEAR CEURVOLACRONYM CEURTOC CEURLOCTIME CEURPUBDATE CEURVOLNR" "exactly one"
count   A17a 25a atmostone "CEURCOLOCATED" "exactly one"
count   A18  26  atleastone "CEURVOLEDITOR" "at least one"
lacks   A19  -   "%2FVol-XXX%2F" "The validator link at the end of index.html must initially include the parameter 'Vol-XXX'. Otherwise, our scripts cannot insert the right volume number here!"
finds   A20  27  "grep -n -P '$CEURBLANK' index.html" "index.html contains CEUR tag elements that start or end with a blank character. This creates unwanted leading or trailing spaces in metadata labels. Remove the leading and trailing blanks!"
finds   A21  28  "grep CEURAUTHOR index.html | grep ' and'" "index.html contains CEURAUTHOR elements using 'and' as separator instead ','."
has     A22  29  "Mary Y. Writter" "index.html contains a synthetic author name from Vol-XXX, here Mary Y. Writter"
finds   A23  30  "$DUPLINKS" "index.html contains multiple links to the same PDF file!"
finds   A24  31  "$DUPTITLES" "index.html contains multiple CEURTITLEs with the same label!"
if [ "$MODE" = team ]; then
  date_val=$(grep -zPo '<span class="CEURPUBDATE">\s*\K[^<]+(?=\s*</span>)' index.html | tr -d '\0' | xargs)
  [[ "$date_val" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || report A32 "CEURPUBDATE is set to \"$date_val\". Must be an ISO date like 2025-06-09."
fi
has     -    32a "MM-DD" "The date of 'submitted by' in the footer of index.html needs to be set to the day when the volume was originally submitted via osTicket"
if [ -n "$regular" ]; then
finds   A25  33  "grep -o -P '(?<=li id=).*(?=pdf)' index.html | grep '/'" "Some papers appear to be in subdirectories or use absolute URLs. They shall be in the same directory as index.html and use relative URLs"
fi
finds   A26  34  "grep CEURVOLACRONYM index.html | grep -q ' 2' || echo missing" "CEURVOLACRONYM must contain the year in which the event took place like in 'ABCD 2023'"
for kw in CEURTITLE CEURAUTHOR CEURVOLTITLE; do
finds   A27  35  "grep -n $kw index.html | grep -v 'Jr.' | grep -v 'Sr.' | grep '\.</span>'" "$kw ends with a '.'. It should be just a label not ending with a dot."
done
finds   A28  -   "grep 'submitted by' index.html | grep -q ',' || echo missing" "The name of the person after 'submitted by' in end the end of index.html must be followed by a comma ','."
lacks   A29  36  "<body>" "The index.html file must contain the tag 'body' without any parameters."
if [ "$MODE" = team ] && ! grep CEURPUBDATE index.html | grep -q "$(date +%Y)"; then
  echo " ==========> Warning (37) in index.html!!!!"
  echo "Warning: The CEURPUBDATE at the end of the index.html file does not contain the current year. This is ok if changes were made in the year after the publication of the volume."
fi
finds   A30  38  "for f in *.pdf; do grep -q \"\$f\" index.html || echo \"PDF file \$f is not linked in index.html\"; done" "Make sure all paper PDFs are linked in index.html"
finds   A31  39  "for f in \$(grep -o 'href=\"[^\"]*\.pdf\"' index.html | sed 's/href=\"//;s/\"\$//'); do [[ \$f != http* && ! -f \$f ]] && echo \"File \$f referenced in index.html does not exist in the same directory\"; done" "Make sure all references to paper PDFs in index.html also exist as file in the same directory"
finds   A32  40  "$DUPREFS" "The index.html file has multiple references to the same PDF file."
has     A33  41  "<script" "Script keyword found in index.html; we do not allow JavaScript on our pages"
finds   A34  42  "$NOFENCE | grep 'http://'" "Change occurrences of 'http://' to 'https://' if the URL is also working with https://"
has     A35  43  "0000-0000-0000-0000" "The index.html file has a dummy ORCID 0000-0000-0000-0000. Either correct or remove the tags itemscope itemtype and itemid"
lacks   A36  -   "0074-XXX-C" "Do not change the URN template from Vol-XXX in the submission index.html file; we shall assign the URN automatically"
lacks   A37  -   "\"CEURVOLNR\">Vol-XXX" "Do not change the volume number Vol-XXX in the submission index.html file; we shall assign it automatically"
finds   A38  -   "grep -nP '(?=.*<span class=\"(CEURAUTHOR|CEURVOLEDITOR)\")^(?!.*</span>)' index.html | head -n 1" "The <span> expression for CEURAUTHOR and CEURVOLEDITOR must be on a single line!"
finds   -    44  "grep -P '(?=.*<span class=\"CEURAUTHOR\")^(?!.*</span>)' index.html | head -n 1" "A line containing '<span class=\"CEURAUTHOR\"' is missing its closing '</span>' in the same line. Do not split such lines!"

if [ "$MODE" = team ]; then
  echo " "
  exit $((ERRCOUNT > 255 ? 255 : ERRCOUNT))
fi

# the chair check only: checks beyond single grep rules

# author and editor names against their ORCID profile names and aliases
normalize() {
  echo "$1" | iconv -f utf-8 -t ascii//TRANSLIT 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -s ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}
check_orcids() {
  grep -q 'itemid="https://orcid.org/' index.html || return
  echo " "
  echo "(*) Checking whether author names match their profile name on ORCID.org"
  local mismatch=no span name orcid json api_full aliases ceur_norm n n_norm n_words match_found
  while read -r span; do
    name=$(echo "$span" | sed -E 's/.*>([^<]+)<\/span>/\1/' | sed 's/&[a-zA-Z0-9#]\+;/ /g')
    orcid=$(echo "$span" | grep -oP 'itemid="https://orcid\.org/\K[0-9\-X]+')
    [[ -z "$orcid" ]] && continue
    json=$(curl -s -H "Accept: application/json" "https://pub.orcid.org/v3.0/$orcid/person")
    api_full="$(echo "$json" | grep -oP '"given-names":\s*\{"value":\s*"\K[^"]+') $(echo "$json" | grep -oP '"family-name":\s*\{"value":\s*"\K[^"]+')"
    aliases=$(echo "$json" | grep -oP '"other-name":\s*\{"content":\s*"\K[^"]+' | tr '\n' ' ')
    ceur_norm=$(normalize "$name")
    match_found=0
    for n in "$api_full" $aliases; do
      n_norm=$(normalize "$n")
      n_words=($n_norm)
      if [[ ${#n_words[@]} -ge 2 && "$ceur_norm" =~ ${n_words[0]} && "$ceur_norm" =~ ${n_words[-1]} ]]; then
        match_found=1
        break
      fi
    done
    if [[ $match_found -eq 0 ]]; then
      mismatch=yes
      echo "In index.html: $name → $orcid → On ORCID.org: $api_full (aliases: $aliases)"
    fi
  done < <(grep -oP '<span class="CEUR(?:AUTHOR|VOLEDITOR)".*?>.*?</span>' index.html)
  if [[ $mismatch == yes ]]; then
    echo " ===> Check that the CEURAUTHOR names with ORCIDs match the profile name or its aliases on ORCID.org."
    echo "      It may be that the CEURAUTHOR name in index.html has a spelling error, or the ORCID is false, or"
    echo "      the name in the ORCID profile has a spelling error. If a mismatch cannot be corrected, then remove"
    echo "      the ORCID tags from the CEURAUTHOR element in index.html; a correct profile that only lacks an alias"
    echo "      may stay, inform the author to add the alias to their ORCID profile."
    ((ERRCOUNT++))
  else
    echo "ok"
  fi
}

# the LI id of a paper must be the name of its PDF file
check_li_ids() {
  echo " "
  echo "(*) Checking mismatches of LI id tags with PDF filenames"
  local mismatches
  mismatches=$(grep -E '<li.*id=".*".*href=".*\.pdf"' index.html | awk '
    match($0, /id="([^"]*)"/, i) && match($0, /href="([^"]*\.pdf)"/, h) {
      base = substr(h[1], 1, length(h[1]) - 4)
      if (i[1] != base) printf "Error in index.html: Mismatch of filename \"%s.pdf\" with its LI id tag \"%s\".\n", base, i[1]
    }')
  if [[ -z "$mismatches" ]]; then
    echo "ok"
  else
    echo "$mismatches"
    echo " ===> Make sure that LI id tag for papers matches the PDF filename."
    echo "      For example if the filename is paper12.pdf then the LI id tag must be paper12."
    ((ERRCOUNT++))
  fi
}

# the official W3C HTML validation; requires a recent Java JRE
check_html5() {
  echo " "
  echo "(*) Running W3C HTML5 validation..."
  if [ ! -f "$HOME/bin/vnu.jar" ]; then
    echo "Download vnu.jar from https://github.com/validator/validator/releases/download/latest/vnu.jar into $HOME/bin for a full check"
    return
  fi
  local java=java
  [ -e /usr/lib/jvm/java-21-openjdk-amd64/bin/java ] && java=/usr/lib/jvm/java-21-openjdk-amd64/bin/java
  if $java -jar "$HOME/bin/vnu.jar" --errors-only index.html; then echo "ok"; else ((ERRCOUNT++)); fi
}

# consecutive page numbers (A39) and PDF page counts against CEURPAGES (A40)
check_pages() {
  echo " "
  echo "(*) Check for consecutive page numbers..."
  if perl -0777 -Mutf8 -ne '
    binmode(STDOUT, ":utf8");
    while (/<span class="CEURPAGES">\s*(\d+)\s*(?:[^0-9<]+\s*(\d+))?\s*<\/span>/gs) {
      my ($start, $end) = ($1, defined($2) ? $2 : $1);
      if (defined $last_end && $start != $last_end + 1) {
        print "Inconsistent CEURPAGES in index.html: Previous paper ended at $last_end, but current starts at $start.\n";
        $error = 1;
      }
      $last_end = $end;
    }
    exit 1 if $error;' index.html; then echo "ok"; else ((ERRCOUNT++)); fi
  echo " "
  echo "(*) Checking if PDF page counts match CEURPAGES..."
  if perl -0777 -Mutf8 -ne '
    binmode(STDOUT, ":utf8");
    while (/href="([^"]+\.pdf)"(?:(?!href=).)*?<span class="CEURPAGES">\s*(\d+)\s*(?:[^0-9<]+\s*(\d+))?\s*<\/span>/gs) {
      my ($pdf, $start, $end) = ($1, $2, defined($3) ? $3 : $2);
      my $expected = $end - $start + 1;
      next unless -f $pdf;
      if (`pdfinfo "$pdf" 2>/dev/null` =~ /^Pages:\s+(\d+)/m && $1 != $expected) {
        print "$pdf: CEURPAGES in index.html claims $expected pages ($start-$end), but PDF has $1.\n";
        $error = 1;
      }
    }
    exit 1 if $error;' index.html; then echo "ok"; else ((ERRCOUNT++)); fi
}

# diversity rule: a CEURVOLEDITOR is CEURAUTHOR of at most one research paper (A41, reported, not counted)
check_editor_overlap() {
  echo " "
  echo "(*) Checking diversity rule for editor/author overlap (A41) ..."
  local editors authors editor n overlap=no
  editors=$(grep 'class="CEURVOLEDITOR"' index.html | sed -e 's/.*class="CEURVOLEDITOR"[^>]*>//' -e 's/<\/span>.*//' -e 's/^[ \t]*//;s/[ \t]*$//' | sort -u)
  authors=$(sed -n '/<li id="preface">/,/<\/li>/!p' index.html | grep 'class="CEURAUTHOR"' | sed -e 's/.*class="CEURAUTHOR">//' -e 's/<\/span>.*//' -e 's/^[ \t]*//;s/[ \t]*$//')
  while IFS= read -r editor; do
    [ -n "$editor" ] || continue
    n=$(echo "$authors" | grep -Fxc "$editor")
    if [ "$n" -gt 1 ]; then
      overlap=yes
      echo " Editor '$editor' is an author of $n research papers."
    fi
  done <<< "$editors"
  if [[ $overlap == no ]]; then echo "ok"; else echo " ==========> ERROR (A41) in index.html!!!!"; fi
}

# at least six papers
check_paper_count() {
  echo " "
  echo "(*) Checking minimum number of papers ..."
  local n
  n=$(grep -wc CEURTITLE index.html)
  if [[ "$n" -ge 6 ]]; then
    echo "ok"
  else
    echo "The index.html file lists $n papers with a CEURTITLE. Minimum is 6."
    ((ERRCOUNT++))
  fi
}

# at least as many author ORCIDs as editor ORCIDs (A43, reported, not counted)
check_orcid_balance() {
  echo " "
  echo "(*) Checking ORCID coverage balance ..."
  local editors authors
  editors=$(grep CEURVOLEDITOR index.html | grep -c itemid)
  authors=$(grep CEURAUTHOR index.html | grep -c itemid)
  if [ "$editors" -gt "$authors" ]; then
    report A43 " There are more ORCIDs for editors ($editors) than for authors ($authors).
 We suggest that at least as many authors are semantified as editors.
 Most paper PDFs have ORCIDs for authors. So pick the data from there!" no
  else
    echo "ok"
  fi
}

# the tags of the template that the DBLP import filters rely on (A44)
check_prescribed_tags() {
  echo " "
  echo "(*) Checking prescribed tags ..."
  local tag missing_tag=no
  for tag in '"CEURCOLOCATED"' '"CEURFULLTITLE"' '"CEURTOC"'; do
    if ! grep -q "$tag" index.html; then
      echo "The index.html file does not use $tag like in our template ceur-ws.org/Vol-XXX"
      missing_tag=yes
    fi
  done
  if [[ $missing_tag == yes ]]; then
    report A44 " ==========> Make sure you use the tags like in the template ceur-ws.org/Vol-XXX/index.html"
  else
    echo "ok"
  fi
}

check_orcids
check_li_ids
check_html5
check_pages
check_editor_overlap
check_paper_count
check_orcid_balance
check_prescribed_tags
finds   A45  -   "grep -i 'li id=' index.html | grep -iq href || { echo 'no named LI element with HREF'; grep -ni 'li id=' index.html | grep -iv href; }" "A named LI element must have the HREF to the paper in the same line!"

if [ "$ERRCOUNT" -eq 0 ]; then
  echo " "
  echo "No errors in index.html found. Congrats!"
  echo " "
fi
exit $((ERRCOUNT > 255 ? 255 : ERRCOUNT))
