/** 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;
}