/** txt2htm.c: Text-zu-HTML-Konverter **/

// Cop. (c) asdala.de 2015, 2018. Alle Rechte vorbehalten.

/*
1.1	2016	Kommentarfeature
1.2	2017	Option Fehler auf Standardausgabe (für diverse Editoren)
1.3	2018	HTML-Tags in [] erlaubt: [<img src=2007/a.gif>]
*/



/* Abstrahiere kompilerspezifische OS-Makros */
#if defined _WIN32 || defined __WIN32__ || defined __TOS_WIN__ || defined __WINDOWS__
#define OSWIN
#elif defined __unix || defined __unix__ || defined __FreeBSD__ || defined __linux__
#define OSUNI
#else
#error Unbekanntes Zielsystem!
#endif

#define NDEBUG // NDEBUG fuer Produktion
#define _CRT_SECURE_NO_WARNINGS

#ifdef __POCC__
#define LOCALESTR ""
#else
#define LOCALESTR ".1252"
#endif

#ifdef __LCC__
#error LCC kommt mit ctype nicht zurecht!
#endif

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <locale.h>
#ifdef OSWIN
#include <windows.h>
#endif
#ifdef OSUNI
#include <unistd.h>
#endif

#define PRGNAME "txt2htm"
#define PRGVER "1.3.1.1"
#define INDENTWIDTH 2
#define CMT '.'
enum { TP=1, TH, TUL, TOL, TBQ };

static int lnno, convct, warnct, listlevel, headerlevel, lntype, option_simpleblockquote,
  option_errs_on_stdout, option_preserve_crosses, tobreak;
static char ln1[4096], ln2[2*sizeof ln1], f1name[128], f2name[128], f3name[128];
static FILE *f1, *f2, *f3;
static unsigned short int map1252_88591[] = {
  0x20AC, // 0x80 EURO SIGN
  0x0000, // 0x81 UNDEFINED
  0x201A, // 0x82 SINGLE LOW-9 QUOTATION MARK
  0x0192, // 0x83 LATIN SMALL LETTER F WITH HOOK
  0x201E, // 0x84 DOUBLE LOW-9 QUOTATION MARK
  0x2026, // 0x85 HORIZONTAL ELLIPSIS
  0x2020, // 0x86 DAGGER
  0x2021, // 0x87 DOUBLE DAGGER
  0x02C6, // 0x88 MODIFIER LETTER CIRCUMFLEX ACCENT
  0x2030, // 0x89 PER MILLE SIGN
  0x0160, // 0x8A LATIN CAPITAL LETTER S WITH CARON
  0x2039, // 0x8B SINGLE LEFT-POINTING ANGLE QUOTATION MARK
  0x0152, // 0x8C LATIN CAPITAL LIGATURE OE
  0x0000, // 0x8D UNDEFINED
  0x017D, // 0x8E LATIN CAPITAL LETTER Z WITH CARON
  0x0000, // 0x8F UNDEFINED
  0x0000, // 0x90 UNDEFINED
  0x2018, // 0x91 LEFT SINGLE QUOTATION MARK
  0x2019, // 0x92 RIGHT SINGLE QUOTATION MARK
  0x201C, // 0x93 LEFT DOUBLE QUOTATION MARK
  0x201D, // 0x94 RIGHT DOUBLE QUOTATION MARK
  0x2022, // 0x95 BULLET
  0x2013, // 0x96 EN DASH
  0x2014, // 0x97 EM DASH
  0x02DC, // 0x98 SMALL TILDE
  0x2122, // 0x99 TRADE MARK SIGN
  0x0161, // 0x9A LATIN SMALL LETTER S WITH CARON
  0x203A, // 0x9B SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
  0x0153, // 0x9C LATIN SMALL LIGATURE OE
  0x0000, // 0x9D UNDEFINED
  0x017E, // 0x9E LATIN SMALL LETTER Z WITH CARON
  0x0178 // 0x9F LATIN CAPITAL LETTER Y WITH DIAERESIS
  };

/** Drucke Programmhilfe **/
static void help(void)
  {
  puts(PRGNAME ": Text-zu-HTML-Konverter V" PRGVER ".\n"
  "\n"
  "Syntax: " PRGNAME " [/Option] <Textdatei> [HTML-Kopfdatei]\n"
  "\n"
  "Optionen:\n"
  "/?              Diese Hilfe\n"
  "/b              Formatiere Einzuege auch ohne '> ' als <BLOCKQUOTE>\n"
  "/p              Behalte Kreuze (#)\n"
  "/e              Melde Fehler auf Standard- statt auf Standardfehlerausgabe\n"
  "\n"
  "Formate:\n"
  "<H1-6>          '# ', '## ' etc.\n"
  "<OL>            '2. ', '    3.1. ' etc. Listentiefe = Einzugbreite x 2.\n"
  "<UL>            '- ', '  - ', '* ' etc. Listentiefe = Einzugbreite x 2.\n"
  "<BLOCKQUOTE>    '> '\n"
  "<P>             Jeder andere, unformatierte Absatz\n"
  "<BR>            2 Leerzeichen am Zeilenende innerhalb P oder BLOCKQUOTE\n"
  "<EM>            '_Hervorgehoben_'\n"
  "\n"
  "Text wird im Zeichensatz 1252 erwartet und nach ISO 8859-1 konvertiert.\n"
  "Der Name der Ausgabedatei entspricht der Eingabedatei, vermehrt um .html.\n"
  "Zeilen mit fuehrendem Punkt werden als Kommentare ignoriert.\n"
  "HTML-Formate koennen in eckige Klammern [] eingeschlossen werden.\n"
  "NBSP (0xA0) und SHY (0xAD) sind direkt nutzbar. Die nicht in ISO 8859-1\n"
  "enthaltenen Zeichen 0x80-0x9F werden zu HTML-Entitaeten konvertiert.\n"
  "Ist keine HTML-Kopfdatei angegeben, wird " PRGNAME ".inc verwendet.");
  exit(1);
  }

/** Melde Warnungen oder brich ab **/
static void errout(const char *s, int abort)
  {
  char *action;
  FILE *fout = option_errs_on_stdout ? stdout : stderr;
  action = abort ? "Abbruch" : "Warnung";
  if (lnno)
    fprintf(fout,PRGNAME " %s in %s:%02d: %s!\n",action,f1name,lnno,s);
  else
    fprintf(fout,PRGNAME " %s: %s!\n",action,s);
  if (abort) {
    if (f2)
      fclose(f2);
    if (f1)
      fclose(f1);
    exit(3);
    }
  else
    ++warnct;
  }

/** Konvertiere Zeichen, maskiere HTML-Steuerzeichen und generiere Zeichenformate **/
static void convert(const char *s1, char *s2)
  {
  const unsigned char *p1;
  int inEM, inHTML;

  assert(s1); assert(s2);

  p1 = (unsigned char*)s1; inEM = 0; inHTML = 0;
  while (*p1) {

    if (!inHTML) {
      // Entferne ASCII-Steuerzeichen (undef. in HTML):
      // NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, BS; SO, SI, DLE, DC1, DC2,
      // DC3, DC4, NAK, SYN, ETB, CAN, EM, SUB, ESC, FS, GS, RS, US; DEL
      if (*p1 <= 0x08u || *p1 >= 0x0eu && *p1 <= 0x1fu || *p1 == 0x7fu) {
        ++p1;
        continue;
        }
      if (*p1 == 0x0bu || *p1 == 0x0cu) { // Konvertiere ASCII-Steuerzeichen (undef. in HTML)
        *s2++ = '\n'; // VT, FF
        ++p1;
        continue;
        }
      if (*p1 >= 0x80u && *p1 <= 0x9fu) { // Konvertiere Zeichen 80-9F (undef. in ISO/IEC 8859-1)
        if (map1252_88591[*p1-0x80u] == 0)
          errout("Non-CP1252-Zeichen gefunden",1);
        s2 += sprintf(s2,"&#x%x;",map1252_88591[*p1-0x80u]);
        ++p1;
        ++convct;
        continue;
        }
      if (*p1 == '<' || *p1 == '>' || *p1 == '&') { // Maskiere HTML-Steuerzeichen
        s2 += sprintf(s2,"&#x%x;",*p1);
        ++p1;
        ++convct;
        continue;
        }
      if (*p1 == '_') { // Generiere Zeichenformate
        if (!inEM) {
          s2 += sprintf(s2,"<em>"); ++inEM; }
        else {
          s2 += sprintf(s2,"</em>"); --inEM; }
        ++p1;
        continue;
        }
      } // (!inHTML)

    if (*p1 == '[' && *(p1+1) == '<') { // NEW 201804: HTML Code [<a ...>]
      ++inHTML;
      p1 += 2;
      *s2++ = '<';
      continue;
      }
    else if (*p1 == '>' && *(p1+1) == ']') { // NEW 201804: HTML Code [<a ...>]
      --inHTML;
      p1 += 2;
      *s2++ = '>';
      continue;
      }

    *s2++ = *p1++;
    }

  *s2 = 0;
  if (inEM)
    errout("Offenes EM",1);
  if (inHTML)
    errout("Offenes HTML",1);
  }

/** Melde benoetigtes Blockformat, Textstart and Grad der Ueberschrift **/
static int typeofline(const char *s, char **start, int *headerlevel)
  {
  const char *p;
  int _lntype;

  assert(s);

  *start = (char*)s;
  p = s;
  *headerlevel = 0;
  if (*s == 0)
    return 0;
  _lntype = TP; // Vorgabe <P>
  if (*p == '>') { // <BLOCKQUOTE>
    if (*(p+1) == ' ') {
      *start = (char*)p + 2; _lntype = TBQ; }
    else
      errout("Fraglich BLOCKQUOTE, formatiere als P",0);
    }
  else if (*p == '-' || *p == '*') { // <UL>
    if (*(p+1) == ' ') {
      *start = (char*)p + 2; _lntype = TUL; }
    else
      errout("Fraglich UL, formatiere als P",0);
    }
  else if (isdigit(*p)) { // <OL>
    do
      ++p;
      while (isdigit(*p) || *p == '.');
    if (*(p-1) == '.' && *p == ' ') {
     *start = (char*)p + 1; _lntype = TOL; }
    else
      errout("Fraglich OL, formatiere als P",0);
    }
  else if (*p == '#') { // <H1>
    do
      ++p;
      while (*p == '#');
    if (*p == ' ') {
      *headerlevel = (p - s > 6 ? 6 : p - s);
      if (!option_preserve_crosses)
        *start = (char*)p + 1;
      _lntype = TH;
      }
    else
      errout("Fraglich H, formatiere als P",0);
    }
  return _lntype;
  }

/** Trimme Leerraum (Leerzeilen, fuehrender/haengender/intermitt. Leerraum) und melde Einzuege **/
static void trim(char *s, int *indentleft, int *indentright)
  {
  unsigned char *p, *p1, *p2;

  assert(s);

  // Bestimme fuehrenden Leerraum
  p = p1 = (unsigned char*)s;
  while (isspace(*p1))
    ++p1; // p1 zeigt auf 1. Nonspace oder ASCIIZ

  if (*p1) { // p1 zeigt auf 1. Nonspace
    *indentleft = (int)((p1-(unsigned char*)s)/INDENTWIDTH); // 0..1 -> Einzug 0, 2..3->1 etc.

    // Bestimme hängenden Leerraum
    p2 = (unsigned char*)s + strlen(s) - 1;
    while (isspace(*p2))
      --p2; // p2 zeigt auf letzten Nonspace
    *indentright = *(p2+1) == ' ' && *(p2+2) == ' '; // BR gewuenscht?

    // Komprimiere Leerraum
    for (;p1<=p2; ++p1)
      if (!isspace(*p1) || !isspace(*(p1+1)))
        *p++ = *p1;
    }

  *p = 0;
  }

/** Lese Zeile ein und melde Blockformat, Listen- und Ueberschriftengrad **/
static int getln(void)
  {
  char *p;
  int indentleft, _lntype;

  LOOP: do {
    ++lnno;
    if (fgets(ln1,sizeof ln1,f1) == NULL) { // EOF
      listlevel = headerlevel = 0;
      return 0;
      }
    if (strlen(ln1) == sizeof(ln1)-1) // fgets() fand kein LF
      errout("Zeile zu lang",1);
    if (*ln1 == CMT) // 201606: Kommentarfeature
      goto LOOP;
    trim(ln1,&indentleft,&tobreak);
    _lntype = typeofline(ln1,&p,&headerlevel);
    if (_lntype == TP && indentleft && option_simpleblockquote) // Erlaube BLOCKQUOTE ohne '> '
      _lntype = TBQ;
    convert(p,ln2);
    } while (*ln2 == 0);
  listlevel = _lntype==TUL || _lntype==TOL ? indentleft+1 : 0; // Setze minimalen Listengrad auf 1
  return _lntype;
  }

/* Jede der folgenden Formatroutinen muss am Ende eine neue Zeile eingelesen haben! */

/** Formatiere Zeile als P **/
static void markp(void)
  {
  fputs("<p>",f2);
  fputs(ln2,f2);
  while (tobreak) {
    fputs("<br>\n",f2);
    lntype = getln();
    if (lntype != TP && lntype != TBQ) { // BR am Absatzende?
      fputs("</p>\n",f2); return; } // Dann beende stillschweigend P
    fputs(ln2,f2);
    }
  fputs("</p>\n",f2);
  lntype = getln();
  }

/** Formatiere Zeile als BLOCKQUOTE **/
static void markblockquote(void)
  {
  fputs("<blockquote>\n",f2);
  while (lntype == TBQ)
    markp();
  fputs("</blockquote>\n",f2);
  }

/** Formatiere Zeile als H **/
static void markh(void)
  {
  fprintf(f2,"<h%d>%s</h%d>\n",headerlevel,ln2,headerlevel);
  lntype = getln();
  }

/** Formatiere Zeile als UL oder OL **/
static void markli(int currlntype, int currlistlevel)
  {
  fputs(currlntype==TUL ? "<ul>\n" : "<ol>\n",f2);
  do {
    fputs("<li>",f2);
    if (currlistlevel == listlevel) {
      fputs(ln2,f2);
      lntype = getln();
      }
    if (currlistlevel < listlevel)
      markli(lntype,currlistlevel+1);
    fputs("</li>\n",f2);
    } while (listlevel && listlevel == currlistlevel);
  fputs(currlntype==TUL ? "</ul>\n" : "</ol>\n",f2);
  }

/** Setze Dateierweiterung **/
static void setext(char *s, const char *ext)
  {
  size_t i;
  i = strlen(s);
  if (i > 4 && s[i-4] == '.')
    i -= 4;
  strcpy(s+i,ext);
  }


/** Main **/
int main(int argc, char *argv[])
  {
  int i;

  /* Schalter */
  for (i=1; i<argc && argv[i][0]=='/'; ++i)
    switch (argv[i][1]) {
      case 'b': option_simpleblockquote = 1; break;
      case 'p': option_preserve_crosses = 1; break;
      case 'e': option_errs_on_stdout = 1; break;
      default : help();
      }
  /* f1name */
  if (i < argc)
    strcpy(f1name,argv[i]);
  else {
    fputs("Textdatei: ", stdout);
    if (fgets(f1name, sizeof f1name, stdin))
      if (*f1name)
        f1name[strlen(f1name)-1] = 0; // Entferne LF
    }
  ++i;
  /* f2name */
  strcpy(f2name,f1name); setext(f2name,".html");
  /* f3name */
  if (i < argc)
    strcpy(f3name,argv[i]);
  else { // Wir leiten HTML-Kopfdatei aus Programmdatei ab
    strcpy(f3name,PRGNAME ".inc");
    }
  if (strcmp(f1name,f2name) == 0 || strcmp(f2name,f3name) == 0)
    errout("Idente Dateinamen",1);

  /* Oeffne Dateien */
  if ((f1=fopen(f1name, "rb")) == NULL)
    errout("Textdatei nicht lesbar",1);
  if ((f2=fopen(f2name, "wb")) == NULL)
    errout("HTML-Datei nicht schreibbar",1);
  if ((f3=fopen(f3name, "rb")) == NULL)
    errout("HTML-Kopfdatei nicht lesbar, verwende Standard",0);
  #ifdef DEBUG
  setbuf(f2,NULL); // Zeitgleiche Ausgabe auf Konsole für f2 == CON
  #endif

  /* Lade ANSI-Zeichensatz */
  if (setlocale(LC_CTYPE,LOCALESTR) == NULL) // POCC unterstuetzt nur ISO-3166-Landkodierungen
    errout("Zeichensatz 1252 nicht sicher ladbar, Formatfehler moeglich",0);
  printf("Gemeldetes Gebietsschema: %s\n",setlocale(LC_ALL,NULL));

  /* Schreibe HTML-Kopf mit Zeichensatz 'iso-8859-1' (nicht 'windows-1252' oder 'utf-8') */
  printf("Konvertiere '%s' nach '%s' mit Kopfdatei '%s' ...\n", f1name, f2name,
    f3 ? f3name : "Standard");
  fputs("<!DOCTYPE html>\n\n<html lang=\"de\">\n<head>\n<meta charset=\"iso-8859-1\">\n",f2);
  fprintf(f2, "<title>%s</title>\n",f1name);
  if (f3) {
    while (fgets(ln1,sizeof ln1,f3) != NULL)
      fputs(ln1,f2);
    fclose(f3);
    }
  fputs("</head>\n<body>\n",f2);

  /* Schreibe HTML-Nutzdaten */
  lntype = getln(); // Einmalig direkter Aufruf von getln()! Weitere nur durch Formatroutinen
  while (lntype)
    switch (lntype) {
      case TUL :
      case TOL : markli(lntype,1); break;
      case TH  : markh(); break;
      case TBQ : markblockquote(); break;
      default  : markp();
      }

  /* Raeum auf */
  fputs("</body>\n</html>\n",f2);
  printf(PRGNAME ": %d Zeile(n) formatiert, %d Zeichen konvertiert, %d Warnung(en).\n",
    lnno, convct, warnct);
  fclose(f2);
  fclose(f1);

  return warnct ? 2 : 0;
  }