// 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 and
 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 
#include 
#include 

#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;
}