322 lines
9.9 KiB
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;
|
|
}
|