joeylib2/tests/gate/checkDocsCode.c

322 lines
9.9 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// checkDocsCode.c - fail when the built manual shows source code with a
// character that is not ASCII.
//
// checkDocsCode MANUAL.html
//
// Asciidoctor's replacements turn `->` into an arrow, `...` into an
// ellipsis, and a backtick next to an apostrophe into a curly quote, even
// inside a backtick code span, so code the reader copies out of the manual
// would not compile. Write such a span as `+text+`, which Asciidoctor
// leaves as it is.
//
// Every <code> and <pre> element is read with its tags dropped and its
// character references decoded (numeric ones, and amp, lt, gt, quot, apos,
// and nbsp; any other named reference counts as a character that is not
// ASCII). Exit 0 when every one is ASCII, 1 otherwise (each listed, with
// the start of its text), 2 on bad arguments.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gateArg.h"
#include "toolFile.h"
#define TOOL_NAME "checkDocsCode"
#define SAMPLE_CHARS 70u
#define ASCII_MAX 0x7Fu
#define CONTROL_END 0x20u
#define BYTE_MAX 0xFFu
#define WIDE_MAX 0xFFFFu
#define TWO_BYTE_MAX 0x7FFu
#define NBSP 0xA0u
#define GLYPHS_MAX 64u
#define TEXT_START 256u
#define UTF8_CONT 0x80u
#define UTF8_CONT_MSK 0xC0u
#define UTF8_TWO 0xC0u
#define UTF8_THREE 0xE0u
#define UTF8_FOUR 0xF0u
#define UTF8_BITS 6u
#define UTF8_PAYLOAD 0x3Fu
#define DECIMAL 10
#define HEX 16
// What an unknown named reference is counted as: not ASCII.
#define NAMED_GLYPH 0xFFFFFFFFu
// One code span or block's text: its characters, as code points.
typedef struct TextT {
uint32_t *chars;
size_t len;
size_t cap;
} TextT;
// The elements whose text is code.
static const char *kElements[] = { "code", "pre" };
static size_t decodeRef(const char *at, const char *end, uint32_t *out);
static size_t decodeUtf8(const char *at, const char *end, uint32_t *out);
static const char *elementEnd(const char *at, const char *end, const char *name, size_t nameLen);
static bool isNameChar(char c);
static void report(const TextT *text, const char *name);
static void textAdd(TextT *text, uint32_t c);
static void textOf(const char *at, const char *end, TextT *text);
static void utf8Put(uint32_t c);
// The character reference at "&": its code point in out and its length,
// or 0 when it is not one (the "&" is then itself).
static size_t decodeRef(const char *at, const char *end, uint32_t *out) {
static const struct {
const char *name;
uint32_t c;
} kNamed[] = {
{ "amp", '&' },
{ "lt", '<' },
{ "gt", '>' },
{ "quot", '"' },
{ "apos", '\'' },
{ "nbsp", NBSP }
};
const char *p;
const char *semi;
char *stop;
size_t i;
int base;
semi = memchr(at, ';', (size_t)(end - at));
if (semi == NULL || semi == at + 1) {
return 0;
}
p = at + 1;
if (*p == '#') {
p++;
base = (*p == 'x' || *p == 'X') ? HEX : DECIMAL;
p += base == HEX ? 1 : 0;
*out = (uint32_t)strtoul(p, &stop, base);
return stop == semi && stop != p ? (size_t)(semi - at) + 1u : 0u;
}
for (i = 0; i < sizeof(kNamed) / sizeof(kNamed[0]); i++) {
if ((size_t)(semi - p) == strlen(kNamed[i].name) && memcmp(p, kNamed[i].name, (size_t)(semi - p)) == 0) {
*out = kNamed[i].c;
return (size_t)(semi - at) + 1u;
}
}
for (; p < semi; p++) {
if (!isNameChar(*p)) {
return 0;
}
}
*out = NAMED_GLYPH;
return (size_t)(semi - at) + 1u;
}
// The UTF-8 character at at: its code point in out and its byte length.
static size_t decodeUtf8(const char *at, const char *end, uint32_t *out) {
uint8_t first;
size_t len;
size_t i;
first = (uint8_t)*at;
if (first <= ASCII_MAX) {
*out = first;
return 1;
}
len = first >= UTF8_FOUR ? 4u : (first >= UTF8_THREE ? 3u : (first >= UTF8_TWO ? 2u : 1u));
*out = first & (BYTE_MAX >> (len + 1u));
for (i = 1; i < len && at + i < end && ((uint8_t)at[i] & UTF8_CONT_MSK) == UTF8_CONT; i++) {
*out = (*out << UTF8_BITS) | ((uint8_t)at[i] & UTF8_PAYLOAD);
}
return i;
}
// Where the element whose content starts at at ends: its closing tag, or
// NULL when it has none.
static const char *elementEnd(const char *at, const char *end, const char *name, size_t nameLen) {
const char *p;
for (p = at; p + 2 + nameLen < end; p++) {
if (p[0] == '<' && p[1] == '/' && memcmp(p + 2, name, nameLen) == 0 && p[2 + nameLen] == '>') {
return p;
}
}
return NULL;
}
static bool isNameChar(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
}
// One element's line: its characters that are not ASCII, and the start of
// its text quoted.
static void report(const TextT *text, const char *name) {
uint32_t glyphs[GLYPHS_MAX];
size_t count;
size_t i;
size_t j;
size_t k;
uint32_t c;
count = 0;
for (i = 0; i < text->len; i++) {
c = text->chars[i];
if (c <= ASCII_MAX) {
continue;
}
for (j = 0; j < count && glyphs[j] < c; j++) {
}
if ((j < count && glyphs[j] == c) || count == GLYPHS_MAX) {
continue;
}
for (k = count; k > j; k--) {
glyphs[k] = glyphs[k - 1u];
}
glyphs[j] = c;
count++;
}
fprintf(stderr, "%s:", TOOL_NAME);
for (i = 0; i < count; i++) {
if (glyphs[i] == NAMED_GLYPH) {
fprintf(stderr, " (a named reference)");
} else {
fprintf(stderr, " U+%04X", (unsigned)glyphs[i]);
}
}
fprintf(stderr, " in <%s>: \"", name);
for (i = 0; i < text->len && i < SAMPLE_CHARS; i++) {
c = text->chars[i];
if (c == '\n') {
fputs("\\n", stderr);
} else if (c < CONTROL_END) {
fprintf(stderr, "\\x%02x", (unsigned)c);
} else {
utf8Put(c);
}
}
fputs("\"\n", stderr);
}
static void textAdd(TextT *text, uint32_t c) {
if (text->len == text->cap) {
text->cap = text->cap == 0 ? TEXT_START : text->cap * 2u;
text->chars = (uint32_t *)realloc(text->chars, text->cap * sizeof(uint32_t));
if (text->chars == NULL) {
toolFail("out of memory");
}
}
text->chars[text->len++] = c;
}
// An element's content as text: its tags dropped, its references and UTF-8
// decoded.
static void textOf(const char *at, const char *end, TextT *text) {
const char *close;
uint32_t c;
size_t used;
text->len = 0;
while (at < end) {
if (*at == '<' && (close = memchr(at, '>', (size_t)(end - at))) != NULL && close > at + 1) {
at = close + 1;
continue;
}
used = *at == '&' ? decodeRef(at, end, &c) : 0u;
if (used == 0) {
used = decodeUtf8(at, end, &c);
}
textAdd(text, c);
at += used;
}
}
// A code point to stderr as UTF-8 (an unknown named reference as "?").
static void utf8Put(uint32_t c) {
if (c == NAMED_GLYPH) {
fputc('?', stderr);
} else if (c <= ASCII_MAX) {
fputc((int)c, stderr);
} else if (c <= TWO_BYTE_MAX) {
fputc((int)(UTF8_TWO | (c >> UTF8_BITS)), stderr);
fputc((int)(UTF8_CONT | (c & UTF8_PAYLOAD)), stderr);
} else if (c <= WIDE_MAX) {
fputc((int)(UTF8_THREE | (c >> (2u * UTF8_BITS))), stderr);
fputc((int)(UTF8_CONT | ((c >> UTF8_BITS) & UTF8_PAYLOAD)), stderr);
fputc((int)(UTF8_CONT | (c & UTF8_PAYLOAD)), stderr);
} else {
fputc((int)(UTF8_FOUR | (c >> (3u * UTF8_BITS))), stderr);
fputc((int)(UTF8_CONT | ((c >> (2u * UTF8_BITS)) & UTF8_PAYLOAD)), stderr);
fputc((int)(UTF8_CONT | ((c >> UTF8_BITS) & UTF8_PAYLOAD)), stderr);
fputc((int)(UTF8_CONT | (c & UTF8_PAYLOAD)), stderr);
}
}
int main(int argc, char **argv) {
uint8_t *file;
size_t len;
const char *page;
const char *end;
const char *at;
const char *open;
const char *close;
const char *name;
size_t nameLen;
size_t i;
size_t j;
uint32_t bad;
TextT text;
toolFileName(TOOL_NAME);
if (argc != 2) {
fprintf(stderr, "Usage: %s MANUAL.html\nExit: 0 every code span and block is ASCII; 1 otherwise (each one listed).\n", TOOL_NAME);
return GATE_USAGE_EXIT;
}
if (!toolReadFile(argv[1], &file, &len)) {
toolFail("cannot read %s", argv[1]);
}
memset(&text, 0, sizeof(text));
page = (const char *)file;
end = page + len;
bad = 0;
for (at = page; at < end; at++) {
if (*at != '<') {
continue;
}
name = NULL;
nameLen = 0;
for (i = 0; i < sizeof(kElements) / sizeof(kElements[0]); i++) {
j = strlen(kElements[i]);
if (at + 1 + j < end && memcmp(at + 1, kElements[i], j) == 0 && !isNameChar(at[1 + j]) && at[1 + j] != '_') {
name = kElements[i];
nameLen = j;
}
}
if (name == NULL || (open = memchr(at, '>', (size_t)(end - at))) == NULL || (close = elementEnd(open + 1, end, name, nameLen)) == NULL) {
continue;
}
textOf(open + 1, close, &text);
for (i = 0; i < text.len && text.chars[i] <= ASCII_MAX; i++) {
}
if (i < text.len) {
bad++;
report(&text, name);
}
at = close + 2 + nameLen;
}
if (bad > 0u) {
fprintf(stderr, "%s: %u code span(s) or block(s) not ASCII; write them as `+text+`\n", TOOL_NAME, (unsigned)bad);
return 1;
}
return 0;
}