HTAGML(1) FreeBSD General Commands Manual HTAGML(1)

htagml
format tagged file as HTML

htagml [-ip | -x] [-f tagsfile] file

The htagml utility formats a file tagged with ctags(1) as HTML. Tags are output as fragment hyperlinks with the class “tag”.

The arguments are as follows:

tagsfile
Read the tag descriptions from a file called tagsfile. The default behavior is to read them from a file called tags.
Assume file has been pre-formatted on standard input, such as by a syntax highlighter. Only tag hyperlinks are added.
Wrap the output in a pre element.
Instead produce an index of tags ordered by their occurrence in file. The index is formatted as a ul element with the class “index”.

tags
default input tags file

ctags htagml.c && htagml htagml.c
hilex -f html htagml.c | htagml -i htagml.c

ctags(1), hilex(1)
January 12, 2021 FreeBSD 12.2-RELEASE-p6

htagml.c in git

/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <ctype.h>
#include <err.h>
#include <regex.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>

static char *nomagic(const char *pattern) {
	char *buf = malloc(2 * strlen(pattern) + 1);
	if (!buf) err(EX_OSERR, "malloc");
	char *ptr = buf;
	for (const char *ch = pattern; *ch; ++ch) {
		if (strchr(".[*", *ch)) *ptr++ = '\\';
		*ptr++ = *ch;
	}
	*ptr = '\0';
	return buf;
}

static size_t escape(bool esc, const char *ptr, size_t len) {
	if (!esc) {
		fwrite(ptr, len, 1, stdout);
		return len;
	}
	for (size_t i = 0; i < len; ++i) {
		switch (ptr[i]) {
			break; case '&': printf("&amp;");
			break; case '<': printf("&lt;");
			break; case '"': printf("&quot;");
			break; default:  putchar(ptr[i]);
		}
	}
	return len;
}

static void id(const char *tag) {
	for (const char *ch = tag; *ch; ++ch) {
		if (isalnum(*ch) || strchr("-._", *ch)) {
			putchar(*ch);
		} else {
			putchar('_');
		}
	}
}

static char *hstrstr(const char *haystack, const char *needle) {
	while (haystack) {
		char *elem = strchr(haystack, '<');
		char *match = strstr(haystack, needle);
		if (!match) return NULL;
		if (!elem || match < elem) return match;
		haystack = strchr(elem, '>');
	}
	return NULL;
}

int main(int argc, char *argv[]) {
	bool pre = false;
	bool pipe = false;
	bool index = false;
	const char *tagsFile = "tags";
	for (int opt; 0 < (opt = getopt(argc, argv, "f:ipx"));) {
		switch (opt) {
			break; case 'f': tagsFile = optarg;
			break; case 'i': pipe = true;
			break; case 'p': pre = true;
			break; case 'x': index = true;
			break; default:  return EX_USAGE;
		}
	}
	if (optind == argc) errx(EX_USAGE, "name required");
	const char *name = argv[optind];

	FILE *file = fopen(tagsFile, "r");
	if (!file) err(EX_NOINPUT, "%s", tagsFile);

	size_t len = 0;
	size_t cap = 256;
	struct Tag {
		char *tag;
		int num;
		regex_t regex;
	} *tags = malloc(cap * sizeof(*tags));
	if (!tags) err(EX_OSERR, "malloc");

	char *buf = NULL;
	size_t bufCap = 0;
	while (0 < getline(&buf, &bufCap, file)) {
		char *line = buf;
		char *tag = strsep(&line, "\t");
		char *file = strsep(&line, "\t");
		char *def = strsep(&line, "\n");
		if (!tag || !file || !def) errx(EX_DATAERR, "malformed tags file");

		if (strcmp(file, name)) continue;
		if (len == cap) {
			tags = realloc(tags, (cap *= 2) * sizeof(*tags));
			if (!tags) err(EX_OSERR, "realloc");
		}
		tags[len].tag = strdup(tag);
		if (!tags[len].tag) err(EX_OSERR, "strdup");

		tags[len].num = 0;
		if (def[0] == '/' || def[0] == '?') {
			def++;
			def[strlen(def)-1] = '\0';
			char *search = nomagic(def);
			int error = regcomp(
				&tags[len].regex, search, REG_NEWLINE | REG_NOSUB
			);
			free(search);
			if (error) {
				warnx("invalid regex for tag %s: %s", tag, def);
				continue;
			}
		} else {
			tags[len].num = strtol(def, &def, 10);
			if (*def) {
				warnx("invalid line number for tag %s: %s", tag, def);
				continue;
			}
		}
		len++;
	}
	fclose(file);

	file = fopen(name, "r");
	if (!file) err(EX_NOINPUT, "%s", name);

	int num = 0;
	printf(pre ? "<pre>" : index ? "<ul class=\"index\">\n" : "");
	while (0 < getline(&buf, &bufCap, file) && ++num) {
		struct Tag *tag = NULL;
		for (size_t i = 0; i < len; ++i) {
			if (tags[i].num) {
				if (num != tags[i].num) continue;
			} else {
				if (regexec(&tags[i].regex, buf, 0, NULL, 0)) continue;
			}
			tag = &tags[i];
			tag->num = num;
			break;
		}
		if (index) {
			if (!tag) continue;
			printf("<li><a class=\"tag\" href=\"#");
			id(tag->tag);
			printf("\">");
			escape(true, tag->tag, strlen(tag->tag));
			printf("</a></li>\n");
			continue;
		}
		if (pipe) {
			ssize_t len = getline(&buf, &bufCap, stdin);
			if (len < 0) {
				errx(EX_DATAERR, "missing line %d on standard input", num);
			}
		}
		if (!tag) {
			escape(!pipe, buf, strlen(buf));
			continue;
		}

		size_t mlen = strlen(tag->tag);
		char *match = (pipe ? hstrstr : strstr)(buf, tag->tag);
		while (match > buf && isalnum(match[-1])) {
			match = (pipe ? hstrstr : strstr)(&match[mlen], tag->tag);
		}
		if (!match && tag->tag[0] == 'M') {
			mlen = 4;
			match = (pipe ? hstrstr : strstr)(buf, "main");
		}
		if (!match) {
			mlen = strlen(buf) - 1;
			match = buf;
		}
		escape(!pipe, buf, match - buf);
		printf("<a class=\"tag\" id=\"");
		id(tag->tag);
		printf("\" href=\"#");
		id(tag->tag);
		printf("\">");
		match += escape(!pipe, match, mlen);
		printf("</a>");
		escape(!pipe, match, strlen(match));
	}
	printf(pre ? "</pre>" : index ? "</ul>\n" : "");
}