mirror of
https://github.com/skeeto/w64devkit.git
synced 2026-10-24 17:39:18 +03:00
A small, self-contained roff formatter for BusyBox man to run in place of groff. It covers what manual pages use: man(7), mdoc(7) and tbl, laid out like groff and mandoc respectively. Over 98% of the lines in a 295,000-line corpus of real pages render identically to those tools. -w lints a page, and it also formats pages that are not installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9122 lines
290 KiB
C
9122 lines
290 KiB
C
// nroff: format manual pages for terminals (man, mdoc, tbl)
|
|
// Chris Wellons <wellons@nullprogram.com>
|
|
//
|
|
// w64dk: $ cc -nostartfiles -O2 -s -o nroff.exe nroff.c -lmemory
|
|
// unix: $ cc -O2 -s -o nroff nroff.c
|
|
// tests: $ cc -DTEST -g3 -fsanitize=undefined -fsanitize-trap nroff.c
|
|
// fuzz: $ afl-gcc-fast -g3 -fsanitize=undefined -fsanitize-trap nroff.c
|
|
//
|
|
// Formats man(7) and mdoc(7) manual pages, including tbl(1) tables, for
|
|
// display on a terminal, as invoked by man(1). It implements the subset
|
|
// of roff that real manual pages use, including the macro definitions,
|
|
// strings, registers, and conditionals in preambles written by pod2man,
|
|
// docutils, Sphinx, and asciidoctor. Output tracks groff's terminal
|
|
// layout closely. Unsupported requests and escapes are ignored, and -w
|
|
// reports them.
|
|
//
|
|
// Emphasis uses SGR escapes: bold is bold, italic is underline. Each
|
|
// output line closes its own SGR so that a pager may display any subset
|
|
// of lines. GROFF_NO_SGR disables emphasis.
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
typedef int8_t i8;
|
|
typedef uint8_t u8;
|
|
typedef int32_t b32;
|
|
typedef int32_t i32;
|
|
typedef uint32_t u32;
|
|
typedef int64_t i64;
|
|
typedef uint64_t u64;
|
|
typedef char byte;
|
|
typedef ptrdiff_t iz;
|
|
typedef size_t uz;
|
|
|
|
#define countof(a) (iz)(sizeof(a) / sizeof(*(a)))
|
|
#define affirm(c) while (!(c)) __builtin_unreachable()
|
|
#define new(a, n, t) (t *)alloc(a, n, sizeof(t), _Alignof(t))
|
|
#define S(s) (Str){(u8 *)s, sizeof(s)-1}
|
|
#define STR(s) {(u8 *)s, sizeof(s)-1}
|
|
#define maxof(t) ((t)-1<1 ? (((t)1<<(sizeof(t)*8-2))-1)*2+1 : (t)-1)
|
|
#define xset(d, c, n) __builtin_memset(d, c, n)
|
|
#define xcpy(d, s, n) __builtin_memcpy(d, s, n)
|
|
|
|
typedef struct {
|
|
u8 *data;
|
|
iz len;
|
|
} Str;
|
|
|
|
typedef struct Plt Plt;
|
|
|
|
typedef struct {
|
|
Plt *plt;
|
|
byte *beg;
|
|
byte *end;
|
|
} Arena;
|
|
|
|
|
|
// Platform API
|
|
|
|
// Read an entire file (null path: standard input). Null data on error.
|
|
static Str plt_load(Plt *, Str path, Arena *);
|
|
// Write to standard output (1) or standard error (2), like write(2).
|
|
static b32 plt_write(Plt *, i32 fd, u8 *, i32);
|
|
// Value of an environment variable, or null if unset.
|
|
static Str plt_getenv(Plt *, Str name, Arena *);
|
|
// May standard output carry SGR escapes? False on legacy consoles.
|
|
static b32 plt_sgr(Plt *);
|
|
// Print "nroff: out of memory" and exit. Does not return.
|
|
static void plt_oom(Plt *);
|
|
// Application entry point, returns the exit status.
|
|
static i32 nroff(i32 argc, u8 **argv, Plt *, Arena);
|
|
|
|
|
|
// Basics
|
|
|
|
static uz touz(iz n)
|
|
{
|
|
affirm(n >= 0);
|
|
return (uz)n;
|
|
}
|
|
|
|
static i32 clampi32(i64 x)
|
|
{
|
|
i32 max = maxof(i32);
|
|
return x>max ? max : x<-max-1 ? -max-1 : (i32)x;
|
|
}
|
|
|
|
static void *alloc(Arena *a, iz count, iz size, iz align)
|
|
{
|
|
iz pad = (iz)-(uz)a->beg & (align - 1);
|
|
if (count >= (a->end - a->beg - pad)/size) {
|
|
plt_oom(a->plt);
|
|
}
|
|
byte *r = a->beg + pad;
|
|
a->beg += pad + count*size;
|
|
return xset(r, 0, touz(count*size));
|
|
}
|
|
|
|
// Allocate from the high end, released in LIFO order by restoring end.
|
|
static void *allochigh(Arena *a, iz count, iz size, iz align)
|
|
{
|
|
iz pad = (iz)((uz)a->end & (uz)(align - 1));
|
|
if (count >= (a->end - a->beg - pad)/size) {
|
|
plt_oom(a->plt);
|
|
}
|
|
a->end -= pad + count*size;
|
|
return xset(a->end, 0, touz(count*size));
|
|
}
|
|
|
|
static Str span(u8 *beg, u8 *end)
|
|
{
|
|
affirm(beg <= end);
|
|
return (Str){beg, end-beg};
|
|
}
|
|
|
|
static Str import(u8 *s)
|
|
{
|
|
Str r = {0};
|
|
r.data = s;
|
|
for (; r.data[r.len]; r.len++) {}
|
|
return r;
|
|
}
|
|
|
|
static b32 equals(Str a, Str b)
|
|
{
|
|
return a.len==b.len && (!a.len || !__builtin_memcmp(a.data, b.data, touz(a.len)));
|
|
}
|
|
|
|
static Str cuthead(Str s, iz len)
|
|
{
|
|
affirm(len >= 0 && len <= s.len);
|
|
if (!len) return s;
|
|
s.data += len;
|
|
s.len -= len;
|
|
return s;
|
|
}
|
|
|
|
static Str takehead(Str s, iz len)
|
|
{
|
|
affirm(len >= 0 && len <= s.len);
|
|
s.len = len;
|
|
return s;
|
|
}
|
|
|
|
static b32 startswith(Str s, Str prefix)
|
|
{
|
|
return s.len>=prefix.len && equals(takehead(s, prefix.len), prefix);
|
|
}
|
|
|
|
static Str clone(Arena *a, Str s)
|
|
{
|
|
Str r = s;
|
|
r.data = new(a, s.len, u8);
|
|
if (r.len) xcpy(r.data, s.data, touz(r.len));
|
|
return r;
|
|
}
|
|
|
|
// Extends head in place when it sits at the front of the arena.
|
|
static Str concat(Arena *a, Str head, Str tail)
|
|
{
|
|
if (!head.data || (byte *)(head.data+head.len) != a->beg) {
|
|
head = clone(a, head);
|
|
}
|
|
head.len += clone(a, tail).len;
|
|
return head;
|
|
}
|
|
|
|
typedef struct {
|
|
Str head;
|
|
Str tail;
|
|
b32 ok;
|
|
} Cut;
|
|
|
|
static Cut cut(Str s, u8 c)
|
|
{
|
|
u8 *beg = s.data;
|
|
u8 *end = s.data + s.len;
|
|
u8 *cut = beg;
|
|
for (; cut<end && *cut!=c; cut++) {}
|
|
Cut r = {0};
|
|
r.ok = cut < end;
|
|
r.head = span(beg, cut);
|
|
r.tail = span(cut+r.ok, end);
|
|
return r;
|
|
}
|
|
|
|
static b32 whitespace(u8 c)
|
|
{
|
|
return c==' ' || c=='\t';
|
|
}
|
|
|
|
static b32 digit(u8 c)
|
|
{
|
|
return c>='0' && c<='9';
|
|
}
|
|
|
|
static Str trimleft(Str s)
|
|
{
|
|
iz i = 0;
|
|
for (; i<s.len && whitespace(s.data[i]); i++) {}
|
|
return cuthead(s, i);
|
|
}
|
|
|
|
static Str trimright(Str s)
|
|
{
|
|
for (; s.len && whitespace(s.data[s.len-1]); s.len--) {}
|
|
return s;
|
|
}
|
|
|
|
static Str trim(Str s)
|
|
{
|
|
return trimright(trimleft(s));
|
|
}
|
|
|
|
static u64 hash64(Str s)
|
|
{
|
|
u64 h = 0x100;
|
|
for (iz i = 0; i < s.len; i++) {
|
|
h ^= s.data[i];
|
|
h *= 1111111111111111111u;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
// Growable byte buffer, extended in place at the front of its arena.
|
|
typedef struct {
|
|
u8 *data;
|
|
iz len;
|
|
iz cap;
|
|
} Buf;
|
|
|
|
static void bappend(Buf *b, Str s, Arena *a)
|
|
{
|
|
if (s.len > b->cap - b->len) {
|
|
iz cap = b->cap ? b->cap : 64;
|
|
for (; cap-b->len < s.len; cap *= 2) {}
|
|
if (b->data && (byte *)(b->data+b->cap)==a->beg && cap-b->cap<a->end-a->beg) {
|
|
alloc(a, cap-b->cap, 1, 1);
|
|
} else {
|
|
u8 *data = new(a, cap, u8);
|
|
if (b->len) xcpy(data, b->data, touz(b->len));
|
|
b->data = data;
|
|
}
|
|
b->cap = cap;
|
|
}
|
|
if (s.len) xcpy(b->data+b->len, s.data, touz(s.len));
|
|
b->len += s.len;
|
|
}
|
|
|
|
static void bappendu8(Buf *b, u8 c, Arena *a)
|
|
{
|
|
bappend(b, (Str){&c, 1}, a);
|
|
}
|
|
|
|
static Str bstr(Buf b)
|
|
{
|
|
return (Str){b.data, b.len};
|
|
}
|
|
|
|
// Append a zeroed element to a slice (data, len, cap), extended in
|
|
// place at the front of its arena.
|
|
#define push(a, s) \
|
|
((s)->len == (s)->cap \
|
|
? (s)->data = push_((a), (s)->data, &(s)->cap, \
|
|
sizeof(*(s)->data), _Alignof(__typeof__(*(s)->data))), \
|
|
(s)->data + (s)->len++ \
|
|
: (s)->data + (s)->len++)
|
|
|
|
static void *push_(Arena *a, void *data, iz *pcap, iz size, iz align)
|
|
{
|
|
iz cap = *pcap;
|
|
if (!data || a->beg != (byte *)data + cap*size) {
|
|
void *copy = alloc(a, cap, size, align);
|
|
if (data) xcpy(copy, data, touz(cap*size));
|
|
data = copy;
|
|
}
|
|
iz extend = cap ? cap : 4;
|
|
alloc(a, extend, size, 1); // already aligned
|
|
*pcap = cap + extend;
|
|
return data;
|
|
}
|
|
|
|
typedef struct {
|
|
Str *data;
|
|
iz len;
|
|
iz cap;
|
|
} Strs;
|
|
|
|
typedef struct {
|
|
b32 *data;
|
|
iz len;
|
|
iz cap;
|
|
} Flags;
|
|
|
|
static Str fmti64(Arena *a, i64 x)
|
|
{
|
|
u8 buf[32];
|
|
u8 *end = buf + countof(buf);
|
|
u8 *beg = end;
|
|
i64 t = x<0 ? x : -x;
|
|
do {
|
|
*--beg = (u8)('0' - t%10);
|
|
} while (t /= 10);
|
|
if (x < 0) {
|
|
*--beg = '-';
|
|
}
|
|
return clone(a, span(beg, end));
|
|
}
|
|
|
|
typedef struct {
|
|
i32 cp;
|
|
i32 len;
|
|
} Rune;
|
|
|
|
// Decode the first code point, or U+FFFD for an invalid byte.
|
|
static Rune decode(Str s)
|
|
{
|
|
Rune r = {0xfffd, 1};
|
|
if (!s.len) {
|
|
r.len = 0;
|
|
return r;
|
|
}
|
|
u8 c = s.data[0];
|
|
if (c < 0x80) {
|
|
r.cp = c;
|
|
return r;
|
|
}
|
|
i32 n = 0, cp = 0, min = 0;
|
|
if ((c&0xe0) == 0xc0) {
|
|
n = 2; cp = c&0x1f; min = 0x80;
|
|
} else if ((c&0xf0) == 0xe0) {
|
|
n = 3; cp = c&0x0f; min = 0x800;
|
|
} else if ((c&0xf8) == 0xf0) {
|
|
n = 4; cp = c&0x07; min = 0x10000;
|
|
} else {
|
|
return r;
|
|
}
|
|
if (s.len < n) {
|
|
return r;
|
|
}
|
|
for (i32 i = 1; i < n; i++) {
|
|
if ((s.data[i]&0xc0) != 0x80) {
|
|
return r;
|
|
}
|
|
cp = cp<<6 | (s.data[i]&0x3f);
|
|
}
|
|
if (cp<min || cp>0x10ffff || (cp>=0xd800 && cp<=0xdfff)) {
|
|
return r;
|
|
}
|
|
r.cp = cp;
|
|
r.len = n;
|
|
return r;
|
|
}
|
|
|
|
static Str encode(Arena *a, i32 cp)
|
|
{
|
|
u8 b[4];
|
|
i32 n = 0;
|
|
if (cp<0 || cp>0x10ffff || (cp>=0xd800 && cp<=0xdfff)) {
|
|
cp = 0xfffd;
|
|
}
|
|
if (cp < 0x80) {
|
|
b[n++] = (u8)cp;
|
|
} else if (cp < 0x800) {
|
|
b[n++] = (u8)(0xc0 | cp>>6);
|
|
b[n++] = (u8)(0x80 | (cp&0x3f));
|
|
} else if (cp < 0x10000) {
|
|
b[n++] = (u8)(0xe0 | cp>>12);
|
|
b[n++] = (u8)(0x80 | (cp>>6&0x3f));
|
|
b[n++] = (u8)(0x80 | (cp&0x3f));
|
|
} else {
|
|
b[n++] = (u8)(0xf0 | cp>>18);
|
|
b[n++] = (u8)(0x80 | (cp>>12&0x3f));
|
|
b[n++] = (u8)(0x80 | (cp>>6&0x3f));
|
|
b[n++] = (u8)(0x80 | (cp&0x3f));
|
|
}
|
|
return clone(a, (Str){b, n});
|
|
}
|
|
|
|
// Display columns occupied by a code point.
|
|
static i32 runewidth(i32 cp)
|
|
{
|
|
if ((cp>=0x300 && cp<=0x36f) || (cp>=0x200b && cp<=0x200f) ||
|
|
(cp>=0xfe00 && cp<=0xfe0f)) {
|
|
return 0;
|
|
}
|
|
if ((cp>=0x1100 && cp<=0x115f) || (cp>=0x2e80 && cp<=0xa4cf) ||
|
|
(cp>=0xac00 && cp<=0xd7a3) || (cp>=0xf900 && cp<=0xfaff) ||
|
|
(cp>=0xfe30 && cp<=0xfe4f) || (cp>=0xff00 && cp<=0xff60) ||
|
|
(cp>=0xffe0 && cp<=0xffe6) || (cp>=0x1f300 && cp<=0x1f64f) ||
|
|
(cp>=0x1f900 && cp<=0x1f9ff) || (cp>=0x20000 && cp<=0x3fffd)) {
|
|
return 2;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static i32 strwidth(Str s)
|
|
{
|
|
i32 w = 0;
|
|
while (s.len) {
|
|
Rune r = decode(s);
|
|
w += runewidth(r.cp);
|
|
s = cuthead(s, r.len);
|
|
}
|
|
return w;
|
|
}
|
|
|
|
|
|
// Hash trie mapping names to definitions
|
|
|
|
typedef struct Entry Entry;
|
|
struct Entry {
|
|
Entry *child[4];
|
|
Str key;
|
|
Str str; // string, macro body, or glyph
|
|
Str alt; // ASCII glyph
|
|
i32 num; // register value, built-in kind
|
|
i32 inc; // register auto-increment, mdoc callable flag
|
|
b32 live;
|
|
};
|
|
|
|
// Find the entry for a key, inserting if the arena is non-null.
|
|
static Entry *lookup(Entry **m, Str key, Arena *perm)
|
|
{
|
|
for (u64 h = hash64(key); *m; h <<= 2) {
|
|
if (equals(key, (*m)->key)) {
|
|
return *m;
|
|
}
|
|
m = &(*m)->child[h>>62];
|
|
}
|
|
if (!perm) {
|
|
return 0;
|
|
}
|
|
*m = new(perm, 1, Entry);
|
|
(*m)->key = clone(perm, key);
|
|
return *m;
|
|
}
|
|
|
|
static Entry *find(Entry *m, Str key)
|
|
{
|
|
Entry *e = lookup(&m, key, 0);
|
|
return e && e->live ? e : 0;
|
|
}
|
|
|
|
|
|
// Buffered output
|
|
|
|
typedef struct {
|
|
Plt *plt;
|
|
i32 len;
|
|
i32 fd;
|
|
b32 err;
|
|
u8 buf[1<<14];
|
|
} Output;
|
|
|
|
static Output *newoutput(Arena *a, Plt *plt, i32 fd)
|
|
{
|
|
Output *b = new(a, 1, Output);
|
|
b->plt = plt;
|
|
b->fd = fd;
|
|
return b;
|
|
}
|
|
|
|
static void flush(Output *b)
|
|
{
|
|
if (!b->err && b->len) {
|
|
b->err = !plt_write(b->plt, b->fd, b->buf, b->len);
|
|
}
|
|
b->len = 0;
|
|
}
|
|
|
|
static void print(Output *b, Str s)
|
|
{
|
|
for (iz off = 0; !b->err && off<s.len;) {
|
|
i32 avail = (i32)countof(b->buf) - b->len;
|
|
i32 count = s.len-off < avail ? (i32)(s.len-off) : avail;
|
|
xcpy(b->buf+b->len, s.data+off, touz(count));
|
|
off += count;
|
|
b->len += count;
|
|
if (b->len == countof(b->buf)) {
|
|
flush(b);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void printu8(Output *b, u8 c)
|
|
{
|
|
print(b, (Str){&c, 1});
|
|
}
|
|
|
|
static void printi64(Output *b, i64 x)
|
|
{
|
|
u8 buf[32];
|
|
u8 *end = buf + countof(buf);
|
|
u8 *beg = end;
|
|
i64 t = x<0 ? x : -x;
|
|
do {
|
|
*--beg = (u8)('0' - t%10);
|
|
} while (t /= 10);
|
|
if (x < 0) {
|
|
*--beg = '-';
|
|
}
|
|
print(b, span(beg, end));
|
|
}
|
|
|
|
|
|
// Special characters: name, UTF-8, ASCII fallback
|
|
|
|
static struct {
|
|
Str name;
|
|
Str utf8;
|
|
Str ascii;
|
|
} glyphs[] = {
|
|
{STR("aq"), STR("'"), STR("'")},
|
|
{STR("dq"), STR("\""), STR("\"")},
|
|
{STR("lq"), STR("\xe2\x80\x9c"), STR("\"")},
|
|
{STR("rq"), STR("\xe2\x80\x9d"), STR("\"")},
|
|
{STR("oq"), STR("\xe2\x80\x98"), STR("`")},
|
|
{STR("cq"), STR("\xe2\x80\x99"), STR("'")},
|
|
{STR("Bq"), STR("\xe2\x80\x9e"), STR(",,")},
|
|
{STR("bq"), STR("\xe2\x80\x9a"), STR(",")},
|
|
{STR("Fo"), STR("\xc2\xab"), STR("<<")},
|
|
{STR("Fc"), STR("\xc2\xbb"), STR(">>")},
|
|
{STR("fo"), STR("\xe2\x80\xb9"), STR("<")},
|
|
{STR("fc"), STR("\xe2\x80\xba"), STR(">")},
|
|
{STR("r!"), STR("\xc2\xa1"), STR("!")},
|
|
{STR("r?"), STR("\xc2\xbf"), STR("?")},
|
|
{STR("em"), STR("\xe2\x80\x94"), STR("--")},
|
|
{STR("en"), STR("\xe2\x80\x93"), STR("-")},
|
|
{STR("hy"), STR("\xe2\x80\x90"), STR("-")},
|
|
{STR("mi"), STR("\xe2\x88\x92"), STR("-")},
|
|
{STR("pl"), STR("+"), STR("+")},
|
|
{STR("eq"), STR("="), STR("=")},
|
|
{STR("bu"), STR("\xe2\x80\xa2"), STR("o")},
|
|
{STR("ci"), STR("\xe2\x97\x8b"), STR("O")},
|
|
{STR("sq"), STR("\xe2\x96\xa1"), STR("[]")},
|
|
{STR("lz"), STR("\xe2\x97\x8a"), STR("<>")},
|
|
{STR("co"), STR("\xc2\xa9"), STR("(C)")},
|
|
{STR("rg"), STR("\xc2\xae"), STR("(R)")},
|
|
{STR("tm"), STR("\xe2\x84\xa2"), STR("tm")},
|
|
{STR("de"), STR("\xc2\xb0"), STR("o")},
|
|
{STR("dg"), STR("\xe2\x80\xa0"), STR("+")},
|
|
{STR("dd"), STR("\xe2\x80\xa1"), STR("++")},
|
|
{STR("ps"), STR("\xc2\xb6"), STR("P")},
|
|
{STR("sc"), STR("\xc2\xa7"), STR("S")},
|
|
{STR("ct"), STR("\xc2\xa2"), STR("c")},
|
|
{STR("Po"), STR("\xc2\xa3"), STR("L")},
|
|
{STR("Ye"), STR("\xc2\xa5"), STR("Y")},
|
|
{STR("Eu"), STR("\xe2\x82\xac"), STR("EUR")},
|
|
{STR("eu"), STR("\xe2\x82\xac"), STR("EUR")},
|
|
{STR("Cs"), STR("\xc2\xa4"), STR("o")},
|
|
{STR("Do"), STR("$"), STR("$")},
|
|
{STR("at"), STR("@"), STR("@")},
|
|
{STR("sh"), STR("#"), STR("#")},
|
|
{STR("rs"), STR("\\"), STR("\\")},
|
|
{STR("sl"), STR("/"), STR("/")},
|
|
{STR("ha"), STR("^"), STR("^")},
|
|
{STR("ti"), STR("~"), STR("~")},
|
|
{STR("ga"), STR("`"), STR("`")},
|
|
{STR("aa"), STR("\xc2\xb4"), STR("'")},
|
|
{STR("ul"), STR("_"), STR("_")},
|
|
{STR("ru"), STR("_"), STR("_")},
|
|
{STR("rn"), STR("\xe2\x80\xbe"), STR("-")},
|
|
{STR("bv"), STR("|"), STR("|")},
|
|
{STR("ba"), STR("|"), STR("|")},
|
|
{STR("br"), STR("\xe2\x94\x82"), STR("|")},
|
|
{STR("or"), STR("|"), STR("|")},
|
|
{STR("lB"), STR("["), STR("[")},
|
|
{STR("rB"), STR("]"), STR("]")},
|
|
{STR("lC"), STR("{"), STR("{")},
|
|
{STR("rC"), STR("}"), STR("}")},
|
|
{STR("la"), STR("\xe2\x9f\xa8"), STR("<")},
|
|
{STR("ra"), STR("\xe2\x9f\xa9"), STR(">")},
|
|
{STR("fm"), STR("\xe2\x80\xb2"), STR("'")},
|
|
{STR("sd"), STR("\xe2\x80\xb3"), STR("''")},
|
|
{STR("%0"), STR("\xe2\x80\xb0"), STR("%o")},
|
|
{STR("Fn"), STR("\xc6\x92"), STR("f")},
|
|
{STR("ss"), STR("\xc3\x9f"), STR("ss")},
|
|
{STR("AE"), STR("\xc3\x86"), STR("AE")},
|
|
{STR("ae"), STR("\xc3\xa6"), STR("ae")},
|
|
{STR("OE"), STR("\xc5\x92"), STR("OE")},
|
|
{STR("oe"), STR("\xc5\x93"), STR("oe")},
|
|
{STR("/O"), STR("\xc3\x98"), STR("O")},
|
|
{STR("/o"), STR("\xc3\xb8"), STR("o")},
|
|
{STR("/L"), STR("\xc5\x81"), STR("L")},
|
|
{STR("/l"), STR("\xc5\x82"), STR("l")},
|
|
{STR("-D"), STR("\xc3\x90"), STR("D")},
|
|
{STR("Sd"), STR("\xc3\xb0"), STR("d")},
|
|
{STR("TP"), STR("\xc3\x9e"), STR("TH")},
|
|
{STR("Tp"), STR("\xc3\xbe"), STR("th")},
|
|
{STR("IJ"), STR("\xc4\xb2"), STR("IJ")},
|
|
{STR("ij"), STR("\xc4\xb3"), STR("ij")},
|
|
{STR(".i"), STR("\xc4\xb1"), STR("i")},
|
|
{STR(".j"), STR("\xc8\xb7"), STR("j")},
|
|
{STR("'A"), STR("\xc3\x81"), STR("A")},
|
|
{STR("'E"), STR("\xc3\x89"), STR("E")},
|
|
{STR("'I"), STR("\xc3\x8d"), STR("I")},
|
|
{STR("'O"), STR("\xc3\x93"), STR("O")},
|
|
{STR("'U"), STR("\xc3\x9a"), STR("U")},
|
|
{STR("'Y"), STR("\xc3\x9d"), STR("Y")},
|
|
{STR("'a"), STR("\xc3\xa1"), STR("a")},
|
|
{STR("'e"), STR("\xc3\xa9"), STR("e")},
|
|
{STR("'i"), STR("\xc3\xad"), STR("i")},
|
|
{STR("'o"), STR("\xc3\xb3"), STR("o")},
|
|
{STR("'u"), STR("\xc3\xba"), STR("u")},
|
|
{STR("'y"), STR("\xc3\xbd"), STR("y")},
|
|
{STR("'C"), STR("\xc4\x86"), STR("C")},
|
|
{STR("'c"), STR("\xc4\x87"), STR("c")},
|
|
{STR("`A"), STR("\xc3\x80"), STR("A")},
|
|
{STR("`E"), STR("\xc3\x88"), STR("E")},
|
|
{STR("`I"), STR("\xc3\x8c"), STR("I")},
|
|
{STR("`O"), STR("\xc3\x92"), STR("O")},
|
|
{STR("`U"), STR("\xc3\x99"), STR("U")},
|
|
{STR("`a"), STR("\xc3\xa0"), STR("a")},
|
|
{STR("`e"), STR("\xc3\xa8"), STR("e")},
|
|
{STR("`i"), STR("\xc3\xac"), STR("i")},
|
|
{STR("`o"), STR("\xc3\xb2"), STR("o")},
|
|
{STR("`u"), STR("\xc3\xb9"), STR("u")},
|
|
{STR("^A"), STR("\xc3\x82"), STR("A")},
|
|
{STR("^E"), STR("\xc3\x8a"), STR("E")},
|
|
{STR("^I"), STR("\xc3\x8e"), STR("I")},
|
|
{STR("^O"), STR("\xc3\x94"), STR("O")},
|
|
{STR("^U"), STR("\xc3\x9b"), STR("U")},
|
|
{STR("^a"), STR("\xc3\xa2"), STR("a")},
|
|
{STR("^e"), STR("\xc3\xaa"), STR("e")},
|
|
{STR("^i"), STR("\xc3\xae"), STR("i")},
|
|
{STR("^o"), STR("\xc3\xb4"), STR("o")},
|
|
{STR("^u"), STR("\xc3\xbb"), STR("u")},
|
|
{STR(":A"), STR("\xc3\x84"), STR("A")},
|
|
{STR(":E"), STR("\xc3\x8b"), STR("E")},
|
|
{STR(":I"), STR("\xc3\x8f"), STR("I")},
|
|
{STR(":O"), STR("\xc3\x96"), STR("O")},
|
|
{STR(":U"), STR("\xc3\x9c"), STR("U")},
|
|
{STR(":Y"), STR("\xc5\xb8"), STR("Y")},
|
|
{STR(":a"), STR("\xc3\xa4"), STR("a")},
|
|
{STR(":e"), STR("\xc3\xab"), STR("e")},
|
|
{STR(":i"), STR("\xc3\xaf"), STR("i")},
|
|
{STR(":o"), STR("\xc3\xb6"), STR("o")},
|
|
{STR(":u"), STR("\xc3\xbc"), STR("u")},
|
|
{STR(":y"), STR("\xc3\xbf"), STR("y")},
|
|
{STR("~A"), STR("\xc3\x83"), STR("A")},
|
|
{STR("~N"), STR("\xc3\x91"), STR("N")},
|
|
{STR("~O"), STR("\xc3\x95"), STR("O")},
|
|
{STR("~a"), STR("\xc3\xa3"), STR("a")},
|
|
{STR("~n"), STR("\xc3\xb1"), STR("n")},
|
|
{STR("~o"), STR("\xc3\xb5"), STR("o")},
|
|
{STR(",C"), STR("\xc3\x87"), STR("C")},
|
|
{STR(",c"), STR("\xc3\xa7"), STR("c")},
|
|
{STR("oA"), STR("\xc3\x85"), STR("A")},
|
|
{STR("oa"), STR("\xc3\xa5"), STR("a")},
|
|
{STR("vS"), STR("\xc5\xa0"), STR("S")},
|
|
{STR("vs"), STR("\xc5\xa1"), STR("s")},
|
|
{STR("vZ"), STR("\xc5\xbd"), STR("Z")},
|
|
{STR("vz"), STR("\xc5\xbe"), STR("z")},
|
|
{STR("a-"), STR("\xc2\xaf"), STR("-")},
|
|
{STR("a."), STR("\xcb\x99"), STR(".")},
|
|
{STR("a^"), STR("^"), STR("^")},
|
|
{STR("ab"), STR("\xcb\x98"), STR("u")},
|
|
{STR("ac"), STR("\xc2\xb8"), STR(",")},
|
|
{STR("ad"), STR("\xc2\xa8"), STR("\"")},
|
|
{STR("ah"), STR("\xcb\x87"), STR("v")},
|
|
{STR("ao"), STR("\xcb\x9a"), STR("o")},
|
|
{STR("a~"), STR("~"), STR("~")},
|
|
{STR("ho"), STR("\xcb\x9b"), STR(",")},
|
|
{STR("a\""), STR("\xcb\x9d"), STR("\"")},
|
|
{STR("<-"), STR("\xe2\x86\x90"), STR("<-")},
|
|
{STR("->"), STR("\xe2\x86\x92"), STR("->")},
|
|
{STR("<>"), STR("\xe2\x86\x94"), STR("<->")},
|
|
{STR("da"), STR("\xe2\x86\x93"), STR("v")},
|
|
{STR("ua"), STR("\xe2\x86\x91"), STR("^")},
|
|
{STR("va"), STR("\xe2\x86\x95"), STR("|")},
|
|
{STR("lA"), STR("\xe2\x87\x90"), STR("<=")},
|
|
{STR("rA"), STR("\xe2\x87\x92"), STR("=>")},
|
|
{STR("hA"), STR("\xe2\x87\x94"), STR("<=>")},
|
|
{STR("dA"), STR("\xe2\x87\x93"), STR("v")},
|
|
{STR("uA"), STR("\xe2\x87\x91"), STR("^")},
|
|
{STR("vA"), STR("\xe2\x87\x95"), STR("|")},
|
|
{STR("an"), STR("\xe2\x8e\xaf"), STR("-")},
|
|
{STR("-+"), STR("\xe2\x88\x93"), STR("-+")},
|
|
{STR("+-"), STR("\xc2\xb1"), STR("+-")},
|
|
{STR("pc"), STR("\xc2\xb7"), STR(".")},
|
|
{STR("md"), STR("\xe2\x8b\x85"), STR(".")},
|
|
{STR("mu"), STR("\xc3\x97"), STR("x")},
|
|
{STR("di"), STR("\xc3\xb7"), STR("/")},
|
|
{STR("f/"), STR("\xe2\x81\x84"), STR("/")},
|
|
{STR("**"), STR("\xe2\x88\x97"), STR("*")},
|
|
{STR("<="), STR("\xe2\x89\xa4"), STR("<=")},
|
|
{STR(">="), STR("\xe2\x89\xa5"), STR(">=")},
|
|
{STR("<<"), STR("\xe2\x89\xaa"), STR("<<")},
|
|
{STR(">>"), STR("\xe2\x89\xab"), STR(">>")},
|
|
{STR("!="), STR("\xe2\x89\xa0"), STR("!=")},
|
|
{STR("=="), STR("\xe2\x89\xa1"), STR("==")},
|
|
{STR("ne"), STR("\xe2\x89\xa2"), STR("!==")},
|
|
{STR("=~"), STR("\xe2\x89\x85"), STR("=~")},
|
|
{STR("|="), STR("\xe2\x89\x83"), STR("-~")},
|
|
{STR("ap"), STR("\xe2\x88\xbc"), STR("~")},
|
|
{STR("~~"), STR("\xe2\x89\x88"), STR("~~")},
|
|
{STR("~="), STR("\xe2\x89\x88"), STR("~=")},
|
|
{STR("pt"), STR("\xe2\x88\x9d"), STR("oc")},
|
|
{STR("es"), STR("\xe2\x88\x85"), STR("{}")},
|
|
{STR("mo"), STR("\xe2\x88\x88"), STR("E")},
|
|
{STR("nm"), STR("\xe2\x88\x89"), STR("!E")},
|
|
{STR("sb"), STR("\xe2\x8a\x82"), STR("(=")},
|
|
{STR("nb"), STR("\xe2\x8a\x84"), STR("(!=")},
|
|
{STR("sp"), STR("\xe2\x8a\x83"), STR("=)")},
|
|
{STR("nc"), STR("\xe2\x8a\x85"), STR("!=)")},
|
|
{STR("ib"), STR("\xe2\x8a\x86"), STR("(=")},
|
|
{STR("ip"), STR("\xe2\x8a\x87"), STR("=)")},
|
|
{STR("ca"), STR("\xe2\x88\xa9"), STR("(^)")},
|
|
{STR("cu"), STR("\xe2\x88\xaa"), STR("U")},
|
|
{STR("/_"), STR("\xe2\x88\xa0"), STR("<")},
|
|
{STR("pp"), STR("\xe2\x8a\xa5"), STR("|")},
|
|
{STR("is"), STR("\xe2\x88\xab"), STR("S")},
|
|
{STR("integral"), STR("\xe2\x88\xab"), STR("S")},
|
|
{STR("sum"), STR("\xe2\x88\x91"), STR("E")},
|
|
{STR("product"), STR("\xe2\x88\x8f"), STR("TT")},
|
|
{STR("gr"), STR("\xe2\x88\x87"), STR("V")},
|
|
{STR("sr"), STR("\xe2\x88\x9a"), STR("sqrt")},
|
|
{STR("sqrt"), STR("\xe2\x88\x9a"), STR("sqrt")},
|
|
{STR("lc"), STR("\xe2\x8c\x88"), STR("|")},
|
|
{STR("rc"), STR("\xe2\x8c\x89"), STR("|")},
|
|
{STR("lf"), STR("\xe2\x8c\x8a"), STR("|")},
|
|
{STR("rf"), STR("\xe2\x8c\x8b"), STR("|")},
|
|
{STR("if"), STR("\xe2\x88\x9e"), STR("oo")},
|
|
{STR("Ah"), STR("\xe2\x84\xb5"), STR("N")},
|
|
{STR("Im"), STR("\xe2\x84\x91"), STR("I")},
|
|
{STR("Re"), STR("\xe2\x84\x9c"), STR("R")},
|
|
{STR("wp"), STR("\xe2\x84\x98"), STR("P")},
|
|
{STR("pd"), STR("\xe2\x88\x82"), STR("d")},
|
|
{STR("-h"), STR("\xe2\x84\x8f"), STR("h")},
|
|
{STR("hbar"), STR("\xe2\x84\x8f"), STR("h")},
|
|
{STR("12"), STR("\xc2\xbd"), STR("1/2")},
|
|
{STR("14"), STR("\xc2\xbc"), STR("1/4")},
|
|
{STR("34"), STR("\xc2\xbe"), STR("3/4")},
|
|
{STR("18"), STR("\xe2\x85\x9b"), STR("1/8")},
|
|
{STR("38"), STR("\xe2\x85\x9c"), STR("3/8")},
|
|
{STR("58"), STR("\xe2\x85\x9d"), STR("5/8")},
|
|
{STR("78"), STR("\xe2\x85\x9e"), STR("7/8")},
|
|
{STR("S1"), STR("\xc2\xb9"), STR("1")},
|
|
{STR("S2"), STR("\xc2\xb2"), STR("2")},
|
|
{STR("S3"), STR("\xc2\xb3"), STR("3")},
|
|
{STR("no"), STR("\xc2\xac"), STR("~")},
|
|
{STR("tno"), STR("\xc2\xac"), STR("~")},
|
|
{STR("AN"), STR("\xe2\x88\xa7"), STR("^")},
|
|
{STR("OR"), STR("\xe2\x88\xa8"), STR("v")},
|
|
{STR("te"), STR("\xe2\x88\x83"), STR("E")},
|
|
{STR("fa"), STR("\xe2\x88\x80"), STR("A")},
|
|
{STR("st"), STR("\xe2\x88\x8b"), STR("-)")},
|
|
{STR("tf"), STR("\xe2\x88\xb4"), STR(".:.")},
|
|
{STR("3d"), STR("\xe2\x88\xb4"), STR(".:.")},
|
|
{STR("mc"), STR("\xc2\xb5"), STR("u")},
|
|
{STR("OK"), STR("\xe2\x9c\x93"), STR("\\/")},
|
|
{STR("CR"), STR("\xe2\x86\xb5"), STR("<cr>")},
|
|
{STR("rh"), STR("\xe2\x98\x9e"), STR("=>")},
|
|
{STR("lh"), STR("\xe2\x98\x9c"), STR("<=")},
|
|
{STR("ff"), STR("\xef\xac\x80"), STR("ff")},
|
|
{STR("fi"), STR("\xef\xac\x81"), STR("fi")},
|
|
{STR("fl"), STR("\xef\xac\x82"), STR("fl")},
|
|
{STR("Fi"), STR("\xef\xac\x83"), STR("ffi")},
|
|
{STR("Fl"), STR("\xef\xac\x84"), STR("ffl")},
|
|
{STR("*A"), STR("\xce\x91"), STR("A")},
|
|
{STR("*B"), STR("\xce\x92"), STR("B")},
|
|
{STR("*G"), STR("\xce\x93"), STR("G")},
|
|
{STR("*D"), STR("\xce\x94"), STR("D")},
|
|
{STR("*E"), STR("\xce\x95"), STR("E")},
|
|
{STR("*Z"), STR("\xce\x96"), STR("Z")},
|
|
{STR("*Y"), STR("\xce\x97"), STR("H")},
|
|
{STR("*H"), STR("\xce\x98"), STR("O")},
|
|
{STR("*I"), STR("\xce\x99"), STR("I")},
|
|
{STR("*K"), STR("\xce\x9a"), STR("K")},
|
|
{STR("*L"), STR("\xce\x9b"), STR("L")},
|
|
{STR("*M"), STR("\xce\x9c"), STR("M")},
|
|
{STR("*N"), STR("\xce\x9d"), STR("N")},
|
|
{STR("*C"), STR("\xce\x9e"), STR("H")},
|
|
{STR("*O"), STR("\xce\x9f"), STR("O")},
|
|
{STR("*P"), STR("\xce\xa0"), STR("TT")},
|
|
{STR("*R"), STR("\xce\xa1"), STR("P")},
|
|
{STR("*S"), STR("\xce\xa3"), STR("S")},
|
|
{STR("*T"), STR("\xce\xa4"), STR("T")},
|
|
{STR("*U"), STR("\xce\xa5"), STR("Y")},
|
|
{STR("*F"), STR("\xce\xa6"), STR("O")},
|
|
{STR("*X"), STR("\xce\xa7"), STR("X")},
|
|
{STR("*Q"), STR("\xce\xa8"), STR("Y")},
|
|
{STR("*W"), STR("\xce\xa9"), STR("O")},
|
|
{STR("*a"), STR("\xce\xb1"), STR("a")},
|
|
{STR("*b"), STR("\xce\xb2"), STR("B")},
|
|
{STR("*g"), STR("\xce\xb3"), STR("y")},
|
|
{STR("*d"), STR("\xce\xb4"), STR("d")},
|
|
{STR("*e"), STR("\xce\xb5"), STR("e")},
|
|
{STR("*z"), STR("\xce\xb6"), STR("C")},
|
|
{STR("*y"), STR("\xce\xb7"), STR("n")},
|
|
{STR("*h"), STR("\xce\xb8"), STR("0")},
|
|
{STR("*i"), STR("\xce\xb9"), STR("i")},
|
|
{STR("*k"), STR("\xce\xba"), STR("k")},
|
|
{STR("*l"), STR("\xce\xbb"), STR("\\")},
|
|
{STR("*m"), STR("\xce\xbc"), STR("u")},
|
|
{STR("*n"), STR("\xce\xbd"), STR("v")},
|
|
{STR("*c"), STR("\xce\xbe"), STR("E")},
|
|
{STR("*o"), STR("\xce\xbf"), STR("o")},
|
|
{STR("*p"), STR("\xcf\x80"), STR("n")},
|
|
{STR("*r"), STR("\xcf\x81"), STR("p")},
|
|
{STR("*s"), STR("\xcf\x83"), STR("o")},
|
|
{STR("ts"), STR("\xcf\x82"), STR("s")},
|
|
{STR("*t"), STR("\xcf\x84"), STR("t")},
|
|
{STR("*u"), STR("\xcf\x85"), STR("u")},
|
|
{STR("*f"), STR("\xcf\x86"), STR("o")},
|
|
{STR("*x"), STR("\xcf\x87"), STR("x")},
|
|
{STR("*q"), STR("\xcf\x88"), STR("u")},
|
|
{STR("*w"), STR("\xcf\x89"), STR("w")},
|
|
{STR("+h"), STR("\xcf\x91"), STR("0")},
|
|
{STR("+f"), STR("\xcf\x95"), STR("o")},
|
|
{STR("+p"), STR("\xcf\x96"), STR("w")},
|
|
{STR("+e"), STR("\xcf\xb5"), STR("e")},
|
|
};
|
|
|
|
// Transliterations of common UTF-8 input that has no glyph name.
|
|
static struct {
|
|
Str utf8;
|
|
Str ascii;
|
|
} translits[] = {
|
|
{STR("\xc2\xa0"), STR(" ")},
|
|
{STR("\xe2\x80\xa6"), STR("...")},
|
|
{STR("\xe2\x80\x8b"), STR("")},
|
|
{STR("\xe2\x80\x91"), STR("-")},
|
|
{STR("\xe2\x80\x92"), STR("-")},
|
|
{STR("\xe2\x80\x95"), STR("--")},
|
|
{STR("\xe2\x80\x82"), STR(" ")},
|
|
{STR("\xe2\x80\x83"), STR(" ")},
|
|
{STR("\xe2\x80\x89"), STR(" ")},
|
|
{STR("\xe2\x9f\xa8"), STR("<")},
|
|
{STR("\xe2\x9f\xa9"), STR(">")},
|
|
{STR("\xe2\x80\x9b"), STR("'")},
|
|
{STR("\xe2\x80\x9f"), STR("\"")},
|
|
};
|
|
|
|
|
|
// Typesetter
|
|
|
|
enum {
|
|
F_R = 0,
|
|
F_B = 1,
|
|
F_I = 2,
|
|
F_BI = 3,
|
|
};
|
|
|
|
enum {
|
|
TK_GLYPH, // printable text
|
|
TK_SPACE, // adjustable, breakable word space
|
|
TK_FIXED, // unadjustable motion, possibly negative
|
|
TK_ZERO, // zero-width marker (\&)
|
|
};
|
|
|
|
// Bounds on token widths and indents, in columns, so that widths sum
|
|
// without overflow. Nothing beyond the row width can be seen anyway.
|
|
enum {
|
|
MAXW = 1<<12,
|
|
MAXIND = 1<<10,
|
|
MAXV = 100, // blank lines in one run
|
|
};
|
|
|
|
static i32 clampw(i64 w)
|
|
{
|
|
return w<-MAXW ? -MAXW : w>MAXW ? MAXW : (i32)w;
|
|
}
|
|
|
|
enum {
|
|
TF_BREAK = 1<<0, // may break the line after this glyph
|
|
TF_ZERO = 1<<1, // glyph does not advance (\z)
|
|
TF_HYPH = 1<<2, // break point that adds a hyphen (\%)
|
|
};
|
|
|
|
typedef struct {
|
|
i32 off;
|
|
i32 len;
|
|
i32 width;
|
|
u8 type;
|
|
u8 font;
|
|
u8 flags;
|
|
} Tok;
|
|
|
|
typedef struct {
|
|
u8 *data;
|
|
i32 len;
|
|
u8 font;
|
|
} Cell;
|
|
|
|
// A captured output line (table cells)
|
|
typedef struct {
|
|
Cell *cells;
|
|
i32 len;
|
|
} Capline;
|
|
|
|
typedef struct {
|
|
Capline *data;
|
|
iz len;
|
|
iz cap;
|
|
} Caplines;
|
|
|
|
// groff's encoding: bit 0 enables adjustment, so .na and .ad toggle it
|
|
enum {
|
|
AD_L = 0,
|
|
AD_B = 1,
|
|
AD_C = 3,
|
|
AD_R = 5,
|
|
};
|
|
|
|
typedef struct {
|
|
Output *out;
|
|
i32 ll; // line length
|
|
b32 sgr; // emit SGR escapes
|
|
b32 utf8; // UTF-8 glyphs, otherwise ASCII
|
|
|
|
i32 indent;
|
|
i32 previndent; // restored by .in without an argument
|
|
i32 tindent; // temporary indent for the next line, or -1
|
|
b32 fill;
|
|
i32 adjust;
|
|
i32 center; // input lines left to center
|
|
i32 tabs[32]; // explicit tab stops (columns from indent)
|
|
i32 ntabs;
|
|
i32 vspace; // pending blank lines
|
|
b32 nospace; // no-space mode
|
|
b32 reverse; // adjustment direction, toggled per line
|
|
i32 tabw; // default tab stop interval
|
|
b32 vcollapse; // vertical spaces do not accumulate (mdoc)
|
|
b32 zerogap; // zero-width words are part of a word space (mdoc)
|
|
i32 sentspace; // extra space after sentences (.ss, 1/12 em)
|
|
i64 lines; // output line count
|
|
|
|
Tok *toks;
|
|
i32 ntoks;
|
|
i32 maxtoks;
|
|
i32 word; // first token of the current word
|
|
u8 *bytes;
|
|
i32 nbytes;
|
|
i32 maxbytes;
|
|
|
|
Cell *row;
|
|
i32 rowcap;
|
|
|
|
b32 capture; // collect lines instead of printing them
|
|
Arena *caparena;
|
|
Caplines caps;
|
|
} Typeset;
|
|
|
|
static Typeset *newtypeset(Arena *a, Output *out)
|
|
{
|
|
Typeset *ts = new(a, 1, Typeset);
|
|
ts->out = out;
|
|
ts->ll = 78;
|
|
ts->tindent = -1;
|
|
ts->fill = 1;
|
|
ts->adjust = AD_B;
|
|
ts->sentspace = 12;
|
|
ts->tabw = 5;
|
|
ts->nospace = 1; // no blank lines at the very top
|
|
ts->maxtoks = 1<<14;
|
|
ts->toks = new(a, ts->maxtoks, Tok);
|
|
ts->maxbytes = 1<<16;
|
|
ts->bytes = new(a, ts->maxbytes, u8);
|
|
ts->rowcap = 1<<12;
|
|
ts->row = new(a, ts->rowcap, Cell);
|
|
return ts;
|
|
}
|
|
|
|
static i32 tswidth(Typeset *ts, i32 beg, i32 end)
|
|
{
|
|
i32 w = 0;
|
|
for (i32 i = beg; i < end; i++) {
|
|
Tok *t = ts->toks + i;
|
|
w += t->type==TK_GLYPH && (t->flags&TF_ZERO) ? 0 : t->width;
|
|
}
|
|
return w;
|
|
}
|
|
|
|
static i32 tsstart(Typeset *ts)
|
|
{
|
|
i32 r = ts->tindent>=0 ? ts->tindent : ts->indent;
|
|
return r<0 ? 0 : r>MAXIND ? MAXIND : r;
|
|
}
|
|
|
|
static void sgrto(Output *out, u8 from, u8 to)
|
|
{
|
|
if ((from&F_B) && !(to&F_B)) print(out, S("\x1b[22m"));
|
|
if ((from&F_I) && !(to&F_I)) print(out, S("\x1b[24m"));
|
|
if (!(from&F_B) && (to&F_B)) print(out, S("\x1b[1m"));
|
|
if (!(from&F_I) && (to&F_I)) print(out, S("\x1b[4m"));
|
|
}
|
|
|
|
static void capture(Typeset *ts, i32 n)
|
|
{
|
|
Arena *a = ts->caparena;
|
|
Capline *l = push(a, &ts->caps);
|
|
l->cells = new(a, n, Cell);
|
|
l->len = n;
|
|
for (i32 i = 0; i < n; i++) {
|
|
Cell c = ts->row[i];
|
|
if (c.data) {
|
|
c.data = clone(a, (Str){c.data, c.len}).data;
|
|
}
|
|
l->cells[i] = c;
|
|
}
|
|
}
|
|
|
|
// Write the first n cells of the row as one output line.
|
|
static void emitrow(Typeset *ts, i32 n)
|
|
{
|
|
n = n<0 ? 0 : n>ts->rowcap ? ts->rowcap : n;
|
|
if (ts->capture) {
|
|
for (; ts->vspace > 0; ts->vspace--) {
|
|
capture(ts, 0);
|
|
}
|
|
for (; n>0 && !ts->row[n-1].data; n--) {}
|
|
capture(ts, n);
|
|
ts->nospace = 0;
|
|
ts->lines++;
|
|
return;
|
|
}
|
|
|
|
Output *out = ts->out;
|
|
for (; ts->vspace > 0; ts->vspace--) {
|
|
printu8(out, '\n');
|
|
}
|
|
for (; n>0 && !ts->row[n-1].data; n--) {}
|
|
u8 attr = 0;
|
|
for (i32 i = 0; i < n; i++) {
|
|
Cell *c = ts->row + i;
|
|
u8 font = c->data ? c->font : 0;
|
|
if (ts->sgr && font!=attr) {
|
|
sgrto(out, attr, font);
|
|
attr = font;
|
|
}
|
|
if (c->data) {
|
|
print(out, (Str){c->data, c->len});
|
|
} else {
|
|
printu8(out, ' ');
|
|
}
|
|
}
|
|
if (ts->sgr) {
|
|
sgrto(out, attr, 0);
|
|
}
|
|
printu8(out, '\n');
|
|
ts->nospace = 0;
|
|
ts->lines++;
|
|
}
|
|
|
|
static void clearrow(Typeset *ts, i32 n)
|
|
{
|
|
n = n<0 ? 0 : n>ts->rowcap ? ts->rowcap : n;
|
|
xset(ts->row, 0, touz(n)*sizeof(Cell));
|
|
}
|
|
|
|
// Place text into the row at a column, returning the next column.
|
|
static i32 placetext(Typeset *ts, i32 col, Str s, u8 font, b32 advance)
|
|
{
|
|
i32 start = col;
|
|
while (s.len) {
|
|
Rune r = decode(s);
|
|
i32 w = runewidth(r.cp);
|
|
if (col>=0 && col<ts->rowcap) {
|
|
Cell *c = ts->row + col;
|
|
if (w == 0 && col > 0 && ts->row[col-1].data) {
|
|
// Combining mark: append to the previous cell
|
|
c = ts->row + col - 1;
|
|
if (c->data+c->len == s.data) {
|
|
c->len += r.len;
|
|
}
|
|
} else {
|
|
c->data = s.data;
|
|
c->len = r.len;
|
|
c->font = font;
|
|
if (w==2 && col+1<ts->rowcap) {
|
|
ts->row[col+1].data = s.data;
|
|
ts->row[col+1].len = 0;
|
|
ts->row[col+1].font = font;
|
|
}
|
|
}
|
|
}
|
|
col += w;
|
|
s = cuthead(s, r.len);
|
|
}
|
|
return advance ? col : start;
|
|
}
|
|
|
|
// Lay out tokens [0, n) into the row starting at a column.
|
|
static i32 placetoks(Typeset *ts, i32 col, i32 n)
|
|
{
|
|
for (i32 i = 0; i < n; i++) {
|
|
Tok *t = ts->toks + i;
|
|
switch (t->type) {
|
|
case TK_GLYPH: {
|
|
Str s = {ts->bytes+t->off, t->len};
|
|
col = placetext(ts, col, s, t->font, !(t->flags&TF_ZERO));
|
|
} break;
|
|
case TK_SPACE:
|
|
case TK_FIXED:
|
|
col += t->width;
|
|
if (col < 0) col = 0;
|
|
break;
|
|
case TK_ZERO:
|
|
break;
|
|
}
|
|
}
|
|
return col;
|
|
}
|
|
|
|
// Distribute extra space among the word gaps of tokens [0, n), like
|
|
// groff: each gap takes the remaining space divided by the remaining
|
|
// gaps, alternating traversal direction on each adjusted line.
|
|
static void spread(Typeset *ts, i32 n, i32 extra)
|
|
{
|
|
i32 gaps = 0;
|
|
for (i32 i = 0; i < n; i++) {
|
|
if (ts->toks[i].type==TK_SPACE && (i==0 || ts->toks[i-1].type!=TK_SPACE)) {
|
|
gaps++;
|
|
}
|
|
}
|
|
if (!gaps || extra<=0) {
|
|
return;
|
|
}
|
|
b32 rtl = !ts->reverse;
|
|
ts->reverse = !ts->reverse;
|
|
for (i32 k = 0; k < n; k++) {
|
|
i32 i = rtl ? n-1-k : k;
|
|
Tok *t = ts->toks + i;
|
|
b32 first = rtl ? i==0 || t[-1].type!=TK_SPACE
|
|
: i+1==n || t[1].type!=TK_SPACE;
|
|
if (t->type!=TK_SPACE || !first) {
|
|
continue;
|
|
}
|
|
i32 add = gaps==1 ? extra : extra/gaps;
|
|
t->width += add;
|
|
extra -= add;
|
|
gaps--;
|
|
}
|
|
}
|
|
|
|
// Output tokens [0, n) as a line and shift the remainder to the front.
|
|
static void tsflushn(Typeset *ts, i32 n, b32 adjust)
|
|
{
|
|
i32 end = n;
|
|
for (; end>0 && ts->toks[end-1].type==TK_SPACE; end--) {}
|
|
|
|
// A line ending at a discretionary hyphen shows the hyphen
|
|
b32 hyph = end>0 && ts->toks[end-1].type==TK_ZERO && (ts->toks[end-1].flags&TF_HYPH);
|
|
i32 start = tsstart(ts);
|
|
i32 width = tswidth(ts, 0, end) + hyph;
|
|
if (ts->center > 0) {
|
|
i32 indent = ts->indent<0 ? 0 : ts->indent>MAXIND ? MAXIND : ts->indent;
|
|
i32 avail = ts->ll - indent;
|
|
start = indent + (avail>width ? (avail - width)/2 : 0);
|
|
} else if (ts->adjust==AD_C && ts->fill) {
|
|
i32 avail = ts->ll - start;
|
|
start += avail>width ? (avail - width)/2 : 0;
|
|
} else if (ts->adjust==AD_R && ts->fill) {
|
|
i32 avail = ts->ll - start;
|
|
start += avail>width ? avail - width : 0;
|
|
} else if (adjust && ts->adjust==AD_B && ts->fill) {
|
|
spread(ts, end, ts->ll - start - width);
|
|
}
|
|
|
|
// Columns that layout might reach: backward motions only lower it
|
|
i32 need = start + hyph + 1;
|
|
for (i32 i = 0; i < end; i++) {
|
|
Tok *t = ts->toks + i;
|
|
need += t->width>0 ? t->width : 0;
|
|
need += t->type==TK_GLYPH ? t->len : 0; // generous for \z
|
|
}
|
|
clearrow(ts, need+1);
|
|
i32 cols = placetoks(ts, start, end);
|
|
if (hyph) {
|
|
cols = placetext(ts, cols, S("-"), 0, 1);
|
|
}
|
|
for (i32 i = 0; i < ts->rowcap && i < need; i++) {
|
|
if (ts->row[i].data && i >= cols) cols = i + 1;
|
|
}
|
|
emitrow(ts, cols<ts->rowcap ? cols : ts->rowcap);
|
|
|
|
// Drop the output tokens and leading spaces of the remainder
|
|
i32 drop = n;
|
|
for (; drop<ts->ntoks && ts->toks[drop].type==TK_SPACE; drop++) {}
|
|
i32 keep = ts->ntoks - drop;
|
|
if (keep) {
|
|
// Only glyph offsets are meaningful; others may be stale
|
|
i32 boff = ts->nbytes;
|
|
for (i32 i = drop; i < ts->ntoks; i++) {
|
|
if (ts->toks[i].type==TK_GLYPH && ts->toks[i].off<boff) {
|
|
boff = ts->toks[i].off;
|
|
}
|
|
}
|
|
// Glyph bytes are appended in token order, so the remainder's
|
|
// bytes form a suffix of the byte buffer.
|
|
i32 blen = ts->nbytes - boff;
|
|
__builtin_memmove(ts->bytes, ts->bytes+boff, touz(blen));
|
|
__builtin_memmove(ts->toks, ts->toks+drop, touz(keep)*sizeof(Tok));
|
|
for (i32 i = 0; i < keep; i++) {
|
|
if (ts->toks[i].type == TK_GLYPH) {
|
|
ts->toks[i].off -= boff;
|
|
}
|
|
}
|
|
ts->nbytes = blen;
|
|
} else {
|
|
ts->nbytes = 0;
|
|
}
|
|
ts->ntoks = keep;
|
|
ts->word = ts->word>drop ? ts->word-drop : 0;
|
|
ts->tindent = -1;
|
|
if (ts->center > 0) {
|
|
ts->center--;
|
|
}
|
|
}
|
|
|
|
static b32 tsempty(Typeset *ts)
|
|
{
|
|
return ts->ntoks == 0;
|
|
}
|
|
|
|
static void tscommit(Typeset *ts);
|
|
|
|
// Break: output any pending partial line without adjustment.
|
|
static void tsbreak(Typeset *ts)
|
|
{
|
|
if (ts->fill) {
|
|
tscommit(ts); // the last word may still need to wrap
|
|
}
|
|
ts->word = ts->ntoks;
|
|
b32 glyphs = 0;
|
|
for (i32 i = 0; i < ts->ntoks; i++) {
|
|
glyphs |= ts->toks[i].type != TK_SPACE;
|
|
}
|
|
if (glyphs) {
|
|
tsflushn(ts, ts->ntoks, 0);
|
|
} else {
|
|
ts->ntoks = 0;
|
|
ts->nbytes = 0;
|
|
ts->word = 0;
|
|
}
|
|
}
|
|
|
|
static void tsvspace(Typeset *ts, i32 n)
|
|
{
|
|
tsbreak(ts);
|
|
if (!ts->nospace && n>0) {
|
|
if (ts->vcollapse) {
|
|
ts->vspace = ts->vspace>n ? ts->vspace : n; // mandoc
|
|
} else {
|
|
ts->vspace += n;
|
|
}
|
|
ts->vspace = ts->vspace>MAXV ? MAXV : ts->vspace;
|
|
}
|
|
}
|
|
|
|
static b32 hasglyphs(Typeset *ts, i32 beg, i32 end)
|
|
{
|
|
for (i32 i = beg; i < end; i++) {
|
|
if (ts->toks[i].type==TK_GLYPH || ts->toks[i].type==TK_FIXED) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static b32 isletter(Typeset *ts, i32 i)
|
|
{
|
|
if (i<0 || i>=ts->ntoks || ts->toks[i].type!=TK_GLYPH) {
|
|
return 0;
|
|
}
|
|
u8 c = ts->bytes[ts->toks[i].off];
|
|
return (c>='a' && c<='z') || (c>='A' && c<='Z') || c>=0x80;
|
|
}
|
|
|
|
// Find a break point inside the current word: the last hyphen between
|
|
// letters whose prefix fits within the given width, as groff does.
|
|
// Returns -1 if none.
|
|
static i32 hyphenbreak(Typeset *ts, i32 avail)
|
|
{
|
|
i32 best = -1;
|
|
i32 w = 0;
|
|
for (i32 i = ts->word; i < ts->ntoks-1; i++) {
|
|
Tok *t = ts->toks + i;
|
|
w += t->type==TK_GLYPH && (t->flags&TF_ZERO) ? 0 : t->width;
|
|
if (w > avail) {
|
|
break;
|
|
}
|
|
if (!(t->flags&TF_BREAK) || i==ts->word) {
|
|
continue;
|
|
}
|
|
if (t->type==TK_ZERO && (t->flags&TF_HYPH)) {
|
|
if (w+1<=avail && hasglyphs(ts, ts->word, i) && hasglyphs(ts, i+1, ts->ntoks)) {
|
|
best = i;
|
|
}
|
|
} else if (t->type==TK_ZERO ? hasglyphs(ts, i+1, ts->ntoks)
|
|
: isletter(ts, i-1) && isletter(ts, i+1)) {
|
|
best = i;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
// Finish the current word, breaking the line if it does not fit.
|
|
static void tscommit(Typeset *ts)
|
|
{
|
|
while (ts->word < ts->ntoks) {
|
|
i32 avail = ts->ll - tsstart(ts);
|
|
i32 lw = tswidth(ts, 0, ts->word);
|
|
i32 ww = tswidth(ts, ts->word, ts->ntoks);
|
|
if (lw+ww<=avail || (ts->zerogap && !hasglyphs(ts, ts->word, ts->ntoks))) {
|
|
ts->word = ts->ntoks;
|
|
return;
|
|
}
|
|
i32 brk = hyphenbreak(ts, avail - lw);
|
|
if (brk >= 0) {
|
|
tsflushn(ts, brk+1, 1);
|
|
continue;
|
|
}
|
|
if (!hasglyphs(ts, 0, ts->word)) {
|
|
ts->word = ts->ntoks; // overlong word
|
|
return;
|
|
}
|
|
tsflushn(ts, ts->word, 1);
|
|
}
|
|
}
|
|
|
|
static void tspush(Typeset *ts, Tok t, Str bytes)
|
|
{
|
|
if (ts->ntoks==ts->maxtoks || bytes.len>ts->maxbytes-ts->nbytes) {
|
|
// Pathological line: force it out
|
|
tsbreak(ts);
|
|
if (bytes.len > ts->maxbytes) {
|
|
return;
|
|
}
|
|
}
|
|
t.off = ts->nbytes;
|
|
t.len = (i32)bytes.len;
|
|
t.width = clampw(t.width);
|
|
if (bytes.len) xcpy(ts->bytes+ts->nbytes, bytes.data, touz(bytes.len));
|
|
ts->nbytes += (i32)bytes.len;
|
|
ts->toks[ts->ntoks++] = t;
|
|
}
|
|
|
|
static void tsglyph(Typeset *ts, Str s, i32 font, u8 flags)
|
|
{
|
|
Tok t = {0};
|
|
t.type = TK_GLYPH;
|
|
t.font = (u8)font;
|
|
t.flags = flags;
|
|
t.width = strwidth(s);
|
|
tspush(ts, t, s);
|
|
}
|
|
|
|
static void tsmotion(Typeset *ts, i32 n)
|
|
{
|
|
Tok t = {0};
|
|
t.type = TK_FIXED;
|
|
t.width = n;
|
|
tspush(ts, t, (Str){0});
|
|
}
|
|
|
|
static void tszero(Typeset *ts)
|
|
{
|
|
Tok t = {0};
|
|
t.type = TK_ZERO;
|
|
tspush(ts, t, (Str){0});
|
|
}
|
|
|
|
static void tsspace(Typeset *ts, i32 n)
|
|
{
|
|
if (!ts->fill) {
|
|
tsmotion(ts, n);
|
|
return;
|
|
}
|
|
tscommit(ts);
|
|
if (ts->ntoks && ts->toks[ts->ntoks-1].type==TK_SPACE) {
|
|
Tok *t = ts->toks + ts->ntoks - 1;
|
|
t->width = clampw((i64)t->width + n);
|
|
} else {
|
|
Tok t = {0};
|
|
t.type = TK_SPACE;
|
|
t.width = n;
|
|
tspush(ts, t, (Str){0});
|
|
}
|
|
ts->word = ts->ntoks;
|
|
}
|
|
|
|
// Is the text before the end a sentence ending?
|
|
static b32 sentenceend(Typeset *ts)
|
|
{
|
|
for (i32 i = ts->ntoks-1; i >= 0; i--) {
|
|
Tok *t = ts->toks + i;
|
|
if (t->type != TK_GLYPH) {
|
|
return 0;
|
|
}
|
|
Str s = {ts->bytes+t->off, t->len};
|
|
if (equals(s, S(".")) || equals(s, S("?")) || equals(s, S("!"))) {
|
|
return 1;
|
|
}
|
|
static Str transparent[] = {
|
|
STR(")"), STR("]"), STR("\""), STR("'"), STR("*"),
|
|
STR("\xe2\x80\x9d"), STR("\xe2\x80\x99"),
|
|
STR("\xe2\x80\xa0"), STR("\xe2\x80\xa1"),
|
|
};
|
|
b32 ok = 0;
|
|
for (i32 j = 0; j < countof(transparent); j++) {
|
|
ok |= equals(s, transparent[j]);
|
|
}
|
|
if (!ok) {
|
|
return 0;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// End of a filled input line: a word space, wider after a sentence.
|
|
static void tsendline(Typeset *ts)
|
|
{
|
|
tscommit(ts);
|
|
if (!hasglyphs(ts, 0, ts->ntoks)) {
|
|
return;
|
|
}
|
|
tsspace(ts, 1 + (sentenceend(ts) ? (ts->sentspace + 6)/12 : 0));
|
|
}
|
|
|
|
// Make the last word current again, so that what follows joins it.
|
|
static void tsreopen(Typeset *ts)
|
|
{
|
|
ts->word = ts->ntoks;
|
|
for (; ts->word>0 && ts->toks[ts->word-1].type!=TK_SPACE; ts->word--) {}
|
|
}
|
|
|
|
// Drop a pending end-of-line space so the next word joins the last.
|
|
static void tsunspace(Typeset *ts)
|
|
{
|
|
if (ts->ntoks && ts->toks[ts->ntoks-1].type==TK_SPACE) {
|
|
ts->ntoks--;
|
|
tsreopen(ts);
|
|
}
|
|
}
|
|
|
|
// Width of pending text, excluding trailing spaces.
|
|
static i32 tspending(Typeset *ts)
|
|
{
|
|
i32 end = ts->ntoks;
|
|
for (; end>0 && ts->toks[end-1].type==TK_SPACE; end--) {}
|
|
return tswidth(ts, 0, end);
|
|
}
|
|
|
|
// Make the pending text an unbreakable, unadjustable unit (tags).
|
|
static void tsfreeze(Typeset *ts)
|
|
{
|
|
for (; ts->ntoks && ts->toks[ts->ntoks-1].type==TK_SPACE; ts->ntoks--) {}
|
|
for (i32 i = 0; i < ts->ntoks; i++) {
|
|
if (ts->toks[i].type == TK_SPACE) {
|
|
ts->toks[i].type = TK_FIXED;
|
|
}
|
|
}
|
|
ts->word = ts->ntoks;
|
|
}
|
|
|
|
// Output the pending tokens as a standalone line of three unbreakable
|
|
// parts (titles): tokens [0, left), [left, center), and [center, ntoks).
|
|
static void tstitle(Typeset *ts, i32 left, i32 center)
|
|
{
|
|
// A forced break while tokenizing the parts may have dropped tokens
|
|
i32 n = ts->ntoks;
|
|
left = left<0 ? 0 : left>n ? n : left;
|
|
center = center<left ? left : center>n ? n : center;
|
|
for (i32 i = 0; i < n; i++) {
|
|
if (ts->toks[i].type == TK_SPACE) ts->toks[i].type = TK_FIXED;
|
|
}
|
|
i32 lw = tswidth(ts, 0, left);
|
|
i32 cw = tswidth(ts, left, center);
|
|
i32 rw = tswidth(ts, center, n);
|
|
|
|
// Motions may place text anywhere, so use the whole row
|
|
clearrow(ts, ts->rowcap);
|
|
placetoks(ts, 0, left);
|
|
i32 ccol = (ts->ll - cw + 1)/2;
|
|
if (ccol < lw+1) ccol = lw + 1;
|
|
i32 rcol = ts->ll - rw;
|
|
if (rcol < ccol+cw+1) rcol = ccol + cw + 1;
|
|
|
|
// Lay out each part from its own starting column
|
|
Tok *toks = ts->toks;
|
|
ts->toks += left;
|
|
placetoks(ts, ccol, center - left);
|
|
ts->toks += center - left;
|
|
placetoks(ts, rcol, n - center);
|
|
ts->toks = toks;
|
|
emitrow(ts, ts->rowcap);
|
|
ts->ntoks = ts->nbytes = ts->word = 0;
|
|
}
|
|
|
|
|
|
// Roff interpreter
|
|
|
|
enum {
|
|
K_NONE,
|
|
|
|
// Requests
|
|
R_IGNORE, R_AD, R_ALS, R_AM, R_AS, R_BR, R_CE, R_DE, R_DO, R_DS, R_EL,
|
|
R_FI, R_FT, R_IE, R_IF, R_IG, R_IN, R_LL, R_NA, R_NF, R_NOP, R_NR, R_NS,
|
|
R_RM, R_RN, R_RR, R_RS, R_SHIFT, R_SO, R_SP, R_TA, R_TI, R_TL, R_TR,
|
|
R_TS, R_EQ, R_PS, R_SS, R_MSO,
|
|
|
|
// man(7)
|
|
M_TH, M_SH, M_SS, M_PP, M_TP, M_TQ, M_IP, M_HP, M_RS, M_RE, M_PD, M_B,
|
|
M_I, M_SM, M_SB, M_BI, M_BR, M_IB, M_IR, M_RB, M_RI, M_UR, M_UE, M_MT,
|
|
M_ME, M_EX, M_EE, M_SY, M_YS, M_OP, M_MR, M_DT, M_IGNORE,
|
|
|
|
// mdoc(7)
|
|
D_DD, D_DT, D_OS, D_SH, D_SS, D_PP, D_D1, D_DL, D_BD, D_ED, D_BL, D_EL,
|
|
D_IT, D_RS, D_RE, D_BK, D_EK, D_BF, D_EF, D_ND, D_NM, D_AD, D_AN, D_AP,
|
|
D_AR, D_AT, D_BSX, D_BX, D_CD, D_CM, D_DV, D_DX, D_EM, D_ER, D_EV, D_EX,
|
|
D_FA, D_FD, D_FL, D_FN, D_FT, D_FX, D_IC, D_IN, D_LB, D_LI, D_LK, D_MS,
|
|
D_MT, D_NO, D_NS, D_NX, D_OX, D_PA, D_PF, D_RV, D_SM, D_ST, D_SX, D_SY,
|
|
D_TN, D_UX, D_VA, D_VT, D_XR, D_AQ, D_BQ, D_BRQ, D_DQ, D_OP, D_PQ, D_QL,
|
|
D_QQ, D_SQ, D_AO, D_AC, D_BO, D_BC, D_BRO, D_BRC, D_DO, D_DC, D_EO, D_EC,
|
|
D_FO, D_FC, D_OO, D_OC, D_PO, D_PC, D_QO, D_QC, D_SO, D_SC, D_XO, D_XC,
|
|
D_TA, D_REF, D_NOOP, D_IGNORE,
|
|
};
|
|
|
|
static struct {
|
|
Str name;
|
|
i32 kind;
|
|
} builtins[] = {
|
|
{STR("ad"), R_AD}, {STR("als"), R_ALS}, {STR("am"), R_AM},
|
|
{STR("am1"), R_AM}, {STR("ami"), R_IGNORE}, {STR("as"), R_AS},
|
|
{STR("as1"), R_AS}, {STR("br"), R_BR}, {STR("ce"), R_CE},
|
|
{STR("de"), R_DE}, {STR("de1"), R_DE}, {STR("do"), R_DO},
|
|
{STR("ds"), R_DS}, {STR("ds1"), R_DS}, {STR("el"), R_EL},
|
|
{STR("fi"), R_FI}, {STR("ft"), R_FT}, {STR("ie"), R_IE},
|
|
{STR("if"), R_IF}, {STR("ig"), R_IG}, {STR("in"), R_IN},
|
|
{STR("ll"), R_LL}, {STR("na"), R_NA}, {STR("nf"), R_NF},
|
|
{STR("nop"), R_NOP}, {STR("nr"), R_NR}, {STR("ns"), R_NS},
|
|
{STR("rm"), R_RM}, {STR("rn"), R_RN}, {STR("rr"), R_RR},
|
|
{STR("rs"), R_RS}, {STR("shift"), R_SHIFT}, {STR("so"), R_SO},
|
|
{STR("sp"), R_SP}, {STR("ta"), R_TA}, {STR("ti"), R_TI},
|
|
{STR("tl"), R_TL}, {STR("tr"), R_TR}, {STR("TS"), R_TS},
|
|
{STR("EQ"), R_EQ}, {STR("PS"), R_PS},
|
|
|
|
{STR("ab"), R_IGNORE}, {STR("af"), R_IGNORE}, {STR("aln"), R_IGNORE},
|
|
{STR("bd"), R_IGNORE}, {STR("bp"), R_IGNORE}, {STR("c2"), R_IGNORE},
|
|
{STR("cc"), R_IGNORE}, {STR("cf"), R_IGNORE}, {STR("ch"), R_IGNORE},
|
|
{STR("char"), R_IGNORE}, {STR("chop"), R_IGNORE}, {STR("color"), R_IGNORE},
|
|
{STR("cp"), R_IGNORE}, {STR("cs"), R_IGNORE}, {STR("cu"), R_IGNORE},
|
|
{STR("da"), R_IGNORE}, {STR("di"), R_IGNORE}, {STR("ec"), R_IGNORE},
|
|
{STR("em"), R_IGNORE}, {STR("eo"), R_IGNORE}, {STR("ev"), R_IGNORE},
|
|
{STR("fam"), R_IGNORE}, {STR("fc"), R_IGNORE}, {STR("fchar"), R_IGNORE},
|
|
{STR("fl"), R_IGNORE}, {STR("fp"), R_IGNORE}, {STR("fspecial"), R_IGNORE},
|
|
{STR("ftr"), R_IGNORE}, {STR("fzoom"), R_IGNORE}, {STR("hc"), R_IGNORE},
|
|
{STR("hcode"), R_IGNORE}, {STR("hla"), R_IGNORE}, {STR("hlm"), R_IGNORE},
|
|
{STR("hpf"), R_IGNORE}, {STR("hw"), R_IGNORE}, {STR("hy"), R_IGNORE},
|
|
{STR("hym"), R_IGNORE}, {STR("hys"), R_IGNORE}, {STR("it"), R_IGNORE},
|
|
{STR("itc"), R_IGNORE}, {STR("kern"), R_IGNORE}, {STR("lc"), R_IGNORE},
|
|
{STR("lf"), R_IGNORE}, {STR("lg"), R_IGNORE}, {STR("ls"), R_IGNORE},
|
|
{STR("lt"), R_IGNORE}, {STR("mc"), R_IGNORE}, {STR("mk"), R_IGNORE},
|
|
{STR("mso"), R_MSO}, {STR("ne"), R_IGNORE}, {STR("nh"), R_IGNORE},
|
|
{STR("nm"), R_IGNORE}, {STR("nn"), R_IGNORE}, {STR("nx"), R_IGNORE},
|
|
{STR("os"), R_IGNORE}, {STR("pc"), R_IGNORE}, {STR("pl"), R_IGNORE},
|
|
{STR("pm"), R_IGNORE}, {STR("pn"), R_IGNORE}, {STR("po"), R_IGNORE},
|
|
{STR("ps"), R_IGNORE}, {STR("pso"), R_IGNORE}, {STR("pvs"), R_IGNORE},
|
|
{STR("rchar"), R_IGNORE}, {STR("rd"), R_IGNORE}, {STR("return"), R_IGNORE},
|
|
{STR("rj"), R_IGNORE}, {STR("rt"), R_IGNORE}, {STR("schar"), R_IGNORE},
|
|
{STR("shc"), R_IGNORE}, {STR("ss"), R_SS}, {STR("sty"), R_IGNORE},
|
|
{STR("sv"), R_IGNORE}, {STR("sy"), R_IGNORE}, {STR("tc"), R_IGNORE},
|
|
{STR("tm"), R_IGNORE}, {STR("tm1"), R_IGNORE}, {STR("tmc"), R_IGNORE},
|
|
{STR("trf"), R_IGNORE}, {STR("trin"), R_IGNORE}, {STR("trnt"), R_IGNORE},
|
|
{STR("uf"), R_IGNORE}, {STR("ul"), R_IGNORE}, {STR("unformat"), R_IGNORE},
|
|
{STR("vpt"), R_IGNORE}, {STR("vs"), R_IGNORE}, {STR("warn"), R_IGNORE},
|
|
{STR("warnscale"), R_IGNORE}, {STR("wh"), R_IGNORE}, {STR("while"), R_IGNORE},
|
|
{STR("write"), R_IGNORE}, {STR("writec"), R_IGNORE}, {STR("writem"), R_IGNORE},
|
|
{STR("break"), R_IGNORE}, {STR("continue"), R_IGNORE},
|
|
{STR("TE"), R_IGNORE}, {STR("EN"), R_IGNORE}, {STR("PE"), R_IGNORE},
|
|
|
|
{STR("TH"), M_TH}, {STR("SH"), M_SH}, {STR("SS"), M_SS},
|
|
{STR("PP"), M_PP}, {STR("LP"), M_PP}, {STR("P"), M_PP},
|
|
{STR("TP"), M_TP}, {STR("TQ"), M_TQ}, {STR("IP"), M_IP},
|
|
{STR("HP"), M_HP}, {STR("RS"), M_RS}, {STR("RE"), M_RE},
|
|
{STR("PD"), M_PD}, {STR("B"), M_B}, {STR("I"), M_I},
|
|
{STR("SM"), M_SM}, {STR("SB"), M_SB}, {STR("BI"), M_BI},
|
|
{STR("BR"), M_BR}, {STR("IB"), M_IB}, {STR("IR"), M_IR},
|
|
{STR("RB"), M_RB}, {STR("RI"), M_RI}, {STR("UR"), M_UR},
|
|
{STR("UE"), M_UE}, {STR("MT"), M_MT}, {STR("ME"), M_ME},
|
|
{STR("EX"), M_EX}, {STR("EE"), M_EE}, {STR("SY"), M_SY},
|
|
{STR("YS"), M_YS}, {STR("OP"), M_OP}, {STR("MR"), M_MR},
|
|
{STR("DT"), M_DT}, {STR("UC"), M_IGNORE}, {STR("AT"), M_IGNORE},
|
|
{STR("PT"), M_IGNORE}, {STR("BT"), M_IGNORE}, {STR("HY"), M_IGNORE},
|
|
{STR("LINKSTYLE"), M_IGNORE},
|
|
};
|
|
|
|
// mdoc(7) macros; "callable" macros may appear in macro arguments
|
|
static struct {
|
|
Str name;
|
|
i32 kind;
|
|
b32 callable;
|
|
} mdocs[] = {
|
|
{STR("Dd"), D_DD, 0}, {STR("Dt"), D_DT, 0}, {STR("Os"), D_OS, 0},
|
|
{STR("Sh"), D_SH, 0}, {STR("Ss"), D_SS, 0}, {STR("Pp"), D_PP, 0},
|
|
{STR("Lp"), D_PP, 0}, {STR("D1"), D_D1, 0}, {STR("Dl"), D_DL, 0},
|
|
{STR("Bd"), D_BD, 0}, {STR("Ed"), D_ED, 0}, {STR("Bl"), D_BL, 0},
|
|
{STR("El"), D_EL, 0}, {STR("It"), D_IT, 0}, {STR("Rs"), D_RS, 0},
|
|
{STR("Re"), D_RE, 0}, {STR("Bk"), D_BK, 0}, {STR("Ek"), D_EK, 0},
|
|
{STR("Bf"), D_BF, 0}, {STR("Ef"), D_EF, 0}, {STR("Nd"), D_ND, 0},
|
|
{STR("Nm"), D_NM, 1}, {STR("Ad"), D_AD, 1}, {STR("An"), D_AN, 1},
|
|
{STR("Ap"), D_AP, 1}, {STR("Ar"), D_AR, 1}, {STR("At"), D_AT, 1},
|
|
{STR("Bsx"), D_BSX, 1}, {STR("Bx"), D_BX, 1}, {STR("Cd"), D_CD, 0},
|
|
{STR("Cm"), D_CM, 1}, {STR("Dv"), D_DV, 1}, {STR("Dx"), D_DX, 1},
|
|
{STR("Em"), D_EM, 1}, {STR("Er"), D_ER, 1}, {STR("Ev"), D_EV, 1},
|
|
{STR("Ex"), D_EX, 0}, {STR("Fa"), D_FA, 1}, {STR("Fd"), D_FD, 0},
|
|
{STR("Fl"), D_FL, 1}, {STR("Fn"), D_FN, 1}, {STR("Fr"), D_EM, 1},
|
|
{STR("Ft"), D_FT, 1}, {STR("Fx"), D_FX, 1}, {STR("Ic"), D_IC, 1},
|
|
{STR("In"), D_IN, 1}, {STR("Lb"), D_LB, 0}, {STR("Li"), D_LI, 1},
|
|
{STR("Lk"), D_LK, 1}, {STR("Ms"), D_MS, 1}, {STR("Mt"), D_MT, 1},
|
|
{STR("No"), D_NO, 1}, {STR("Ns"), D_NS, 1}, {STR("Nx"), D_NX, 1},
|
|
{STR("Ox"), D_OX, 1}, {STR("Pa"), D_PA, 1}, {STR("Pf"), D_PF, 1},
|
|
{STR("Rv"), D_RV, 0}, {STR("Sm"), D_SM, 0}, {STR("St"), D_ST, 1},
|
|
{STR("Sx"), D_SX, 1}, {STR("Sy"), D_SY, 1}, {STR("Tn"), D_TN, 1},
|
|
{STR("Ux"), D_UX, 1}, {STR("Va"), D_VA, 1}, {STR("Vt"), D_VT, 1},
|
|
{STR("Xr"), D_XR, 1}, {STR("Aq"), D_AQ, 1}, {STR("Bq"), D_BQ, 1},
|
|
{STR("Brq"), D_BRQ, 1}, {STR("Dq"), D_DQ, 1}, {STR("Op"), D_OP, 1},
|
|
{STR("Pq"), D_PQ, 1}, {STR("Ql"), D_QL, 1}, {STR("Qq"), D_QQ, 1},
|
|
{STR("Sq"), D_SQ, 1}, {STR("Ao"), D_AO, 1}, {STR("Ac"), D_AC, 1},
|
|
{STR("Bo"), D_BO, 1}, {STR("Bc"), D_BC, 1}, {STR("Bro"), D_BRO, 1},
|
|
{STR("Brc"), D_BRC, 1}, {STR("Do"), D_DO, 1}, {STR("Dc"), D_DC, 1},
|
|
{STR("Eo"), D_EO, 1}, {STR("Ec"), D_EC, 1}, {STR("Fo"), D_FO, 0},
|
|
{STR("Fc"), D_FC, 1}, {STR("Oo"), D_OO, 1}, {STR("Oc"), D_OC, 1},
|
|
{STR("Po"), D_PO, 1}, {STR("Pc"), D_PC, 1}, {STR("Qo"), D_QO, 1},
|
|
{STR("Qc"), D_QC, 1}, {STR("So"), D_SO, 1}, {STR("Sc"), D_SC, 1},
|
|
{STR("Xo"), D_XO, 1}, {STR("Xc"), D_XC, 1}, {STR("Ta"), D_TA, 1},
|
|
{STR("%A"), D_REF, 0}, {STR("%B"), D_REF, 0}, {STR("%C"), D_REF, 0},
|
|
{STR("%D"), D_REF, 0}, {STR("%I"), D_REF, 0}, {STR("%J"), D_REF, 0},
|
|
{STR("%N"), D_REF, 0}, {STR("%O"), D_REF, 0}, {STR("%P"), D_REF, 0},
|
|
{STR("%Q"), D_REF, 0}, {STR("%R"), D_REF, 0}, {STR("%T"), D_REF, 0},
|
|
{STR("%U"), D_REF, 0}, {STR("%V"), D_REF, 0}, {STR("Ot"), D_FT, 1},
|
|
{STR("Hf"), D_IGNORE, 0}, {STR("Tg"), D_NOOP, 0},
|
|
{STR("Es"), D_IGNORE, 1}, {STR("En"), D_IGNORE, 1},
|
|
{STR("Bt"), D_IGNORE, 0}, {STR("Ud"), D_IGNORE, 0},
|
|
};
|
|
|
|
enum {
|
|
IN = 7*24, // default indent (man), basic units
|
|
SN = 3, // subsection heading indent
|
|
|
|
U_PER_N = 24,
|
|
U_PER_V = 40,
|
|
|
|
MAXDEPTH = 64,
|
|
MAXRS = 32,
|
|
};
|
|
|
|
// An open mdoc block: list, display, or reference
|
|
typedef struct {
|
|
i32 kind; // D_BL, D_BD, D_RS
|
|
i32 type; // list type (LT_*)
|
|
i32 width; // tag width, columns
|
|
i32 indent; // indent of the block's content
|
|
i32 saveindent;
|
|
b32 savefill;
|
|
b32 compact;
|
|
i32 count; // -enum counter
|
|
i32 ncols; // -column
|
|
i32 colw[16];
|
|
i64 lines; // output line count when the tag or body began
|
|
i32 body; // pending tokens when the body began
|
|
} MdBlock;
|
|
|
|
// One part of an .Rs reference, like %A or %T
|
|
typedef struct {
|
|
Str v;
|
|
u8 part; // letter after the '%'
|
|
} MdRef;
|
|
|
|
typedef struct Frame Frame;
|
|
struct Frame {
|
|
Frame *next;
|
|
Str src;
|
|
Str *args; // args[0] is the macro name
|
|
i32 nargs;
|
|
i32 line;
|
|
Str name;
|
|
b32 macro;
|
|
byte *end; // perm.end before args, restored on pop
|
|
i32 sonest; // .so nesting level of a file frame
|
|
};
|
|
|
|
// Input trap actions, run after the next text line (an-input-trap)
|
|
enum {
|
|
TRAP_FONT = 1<<0, // return to roman
|
|
TRAP_HEAD = 1<<1, // end a heading: break, no-space mode
|
|
TRAP_TAG = 1<<2, // write the paragraph tag
|
|
};
|
|
|
|
typedef struct {
|
|
Plt *plt;
|
|
Arena perm;
|
|
Output *err;
|
|
Typeset *ts;
|
|
b32 warn;
|
|
i32 status;
|
|
|
|
Frame *frame;
|
|
Frame *freeframes;
|
|
i32 depth;
|
|
i64 budget;
|
|
|
|
Entry *strings;
|
|
Entry *macros;
|
|
Entry *regs;
|
|
Entry *kinds;
|
|
Entry *glyphmap;
|
|
Entry *asciimap;
|
|
Entry *trmap;
|
|
|
|
u64 iestack;
|
|
i32 iedepth;
|
|
i32 skip; // >0 while skipping a false block
|
|
|
|
Entry *defining; // capturing a macro body
|
|
Str defend;
|
|
Buf defbody;
|
|
b32 ignoring; // inside .ig
|
|
Str igend;
|
|
|
|
i32 font;
|
|
i32 prevfont;
|
|
b32 cont; // previous text line ended with \c
|
|
|
|
// man(7) state
|
|
b32 page;
|
|
Str title;
|
|
Str section;
|
|
Str date;
|
|
Str source;
|
|
Str manual;
|
|
i32 margin; // an-margin
|
|
i32 pindent; // an-prevailing-indent
|
|
i32 rsmargin[MAXRS];
|
|
i32 rspindent[MAXRS];
|
|
i32 rsdepth;
|
|
i32 pd; // paragraph distance (lines)
|
|
i32 trap;
|
|
i64 taglines; // output line count when a tag began
|
|
b32 example;
|
|
i32 expd;
|
|
i32 exfont;
|
|
Str url;
|
|
b32 inurl;
|
|
b32 sy;
|
|
i32 syindent;
|
|
i32 syadjust;
|
|
|
|
b32 tblactive;
|
|
Buf tblsrc;
|
|
|
|
// mdoc(7) state
|
|
b32 mdoc;
|
|
Str mdtitle;
|
|
Str mdsec;
|
|
Str mdvol;
|
|
Str mddate;
|
|
Str mdos;
|
|
Str mdname; // first .Nm argument
|
|
i32 mdsection; // MS_* of the current section
|
|
b32 mdnospace; // no space before the next word
|
|
b32 mdsmoff; // .Sm off
|
|
b32 mdsplit; // .An -split
|
|
b32 mdsplitset;
|
|
b32 mdauthors; // seen an author in AUTHORS
|
|
i32 mdsynprev; // kind of the previous SYNOPSIS line
|
|
i32 mdnmbody; // indent of a SYNOPSIS .Nm block, or 0
|
|
MdBlock mdblocks[32];
|
|
i32 mdnblocks;
|
|
i32 mddepth; // nested callable macros
|
|
b32 mdintag; // .It tag continues to .Xc
|
|
MdRef mdrefs[32]; // .Rs parts in mandoc's order
|
|
i32 mdrefn;
|
|
b32 mdfo; // inside .Fo/.Fc
|
|
i32 mdfoargs;
|
|
b32 mdeos; // last word ended a sentence
|
|
b32 mdwrote; // this macro line has emitted a word
|
|
b32 nobreak; // hyphens in words do not allow breaks
|
|
} Ctx;
|
|
|
|
static void warnf(Ctx *c, Str msg, Str detail)
|
|
{
|
|
if (!c->warn) {
|
|
return;
|
|
}
|
|
Frame *f = c->frame;
|
|
for (; f && f->macro; f = f->next) {}
|
|
Output *err = c->err;
|
|
print(err, S("nroff: "));
|
|
if (f) {
|
|
print(err, f->name);
|
|
printu8(err, ':');
|
|
printi64(err, f->line);
|
|
print(err, S(": "));
|
|
}
|
|
print(err, msg);
|
|
if (detail.data) {
|
|
print(err, S(": "));
|
|
print(err, detail);
|
|
}
|
|
printu8(err, '\n');
|
|
flush(err);
|
|
}
|
|
|
|
static Str getstring(Ctx *c, Str name)
|
|
{
|
|
Entry *e = find(c->strings, name);
|
|
return e ? e->str : (Str){0};
|
|
}
|
|
|
|
static void setstring(Ctx *c, Str name, Str value)
|
|
{
|
|
Entry *e = lookup(&c->strings, name, &c->perm);
|
|
e->str = clone(&c->perm, value);
|
|
e->live = 1;
|
|
}
|
|
|
|
|
|
// Input
|
|
|
|
static Frame *newframe(Ctx *c)
|
|
{
|
|
Frame *f = c->freeframes;
|
|
if (f) {
|
|
c->freeframes = f->next;
|
|
xset(f, 0, sizeof(*f));
|
|
} else {
|
|
f = new(&c->perm, 1, Frame);
|
|
}
|
|
return f;
|
|
}
|
|
|
|
static void pushfile(Ctx *c, Str name, Str src)
|
|
{
|
|
Frame *f = newframe(c);
|
|
f->src = src;
|
|
f->name = name;
|
|
f->next = c->frame;
|
|
c->frame = f;
|
|
}
|
|
|
|
static void popframe(Ctx *c)
|
|
{
|
|
Frame *f = c->frame;
|
|
c->frame = f->next;
|
|
if (f->macro) {
|
|
c->depth--;
|
|
c->perm.end = f->end; // frames pop in LIFO order
|
|
}
|
|
f->next = c->freeframes;
|
|
c->freeframes = f;
|
|
}
|
|
|
|
static Frame *argframe(Ctx *c)
|
|
{
|
|
Frame *f = c->frame;
|
|
for (; f && !f->macro; f = f->next) {}
|
|
return f;
|
|
}
|
|
|
|
static b32 oddbackslashes(Str s)
|
|
{
|
|
i32 n = 0;
|
|
for (iz i = s.len-1; i>=0 && s.data[i]=='\\'; i--) {
|
|
n++;
|
|
}
|
|
return n & 1;
|
|
}
|
|
|
|
static Str stripcr(Str s)
|
|
{
|
|
if (s.len && s.data[s.len-1]=='\r') s.len--;
|
|
return s;
|
|
}
|
|
|
|
// Read the next logical input line, joining escaped newlines. Returns
|
|
// a null string at the end of input.
|
|
static Str nextline(Ctx *c, Arena *scratch)
|
|
{
|
|
for (;;) {
|
|
Frame *f = c->frame;
|
|
if (!f) {
|
|
return (Str){0};
|
|
}
|
|
if (!f->src.len || c->budget<=0) {
|
|
if (c->budget <= 0) {
|
|
f->src = (Str){0};
|
|
}
|
|
popframe(c);
|
|
continue;
|
|
}
|
|
c->budget--;
|
|
Cut l = cut(f->src, '\n');
|
|
f->src = l.tail;
|
|
f->line++;
|
|
Str line = stripcr(l.head);
|
|
if (!line.data) {
|
|
line.data = l.tail.data;
|
|
}
|
|
// Drop each escaping backslash before joining, so that concat
|
|
// extends the line in place. Joined runs of backslashes are then
|
|
// even, and the next segment alone decides whether to continue.
|
|
b32 more = oddbackslashes(line);
|
|
if (more) line = takehead(line, line.len-1);
|
|
while (more && f->src.len) {
|
|
Cut n = cut(f->src, '\n');
|
|
f->src = n.tail;
|
|
f->line++;
|
|
Str next = stripcr(n.head);
|
|
more = oddbackslashes(next);
|
|
if (more) next = takehead(next, next.len-1);
|
|
line = concat(scratch, line, next);
|
|
}
|
|
return line;
|
|
}
|
|
}
|
|
|
|
|
|
// Escapes
|
|
|
|
typedef struct {
|
|
Str name;
|
|
Str rest;
|
|
b32 ok;
|
|
} Arg;
|
|
|
|
// End of a [bracketed] name, honoring nested brackets.
|
|
static iz bracketend(Str s)
|
|
{
|
|
i32 depth = 0;
|
|
for (iz i = 0; i < s.len; i++) {
|
|
if (s.data[i] == '[') {
|
|
depth++;
|
|
} else if (s.data[i]==']' && !--depth) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Parse an escape's name: x, (xx, or [name].
|
|
static Arg escname(Str s)
|
|
{
|
|
Arg r = {0};
|
|
if (!s.len) {
|
|
return r;
|
|
}
|
|
if (s.data[0] == '(') {
|
|
if (s.len < 3) {
|
|
return r;
|
|
}
|
|
r.name = span(s.data+1, s.data+3);
|
|
r.rest = cuthead(s, 3);
|
|
} else if (s.data[0] == '[') {
|
|
iz end = bracketend(s);
|
|
if (end < 0) {
|
|
return r;
|
|
}
|
|
r.name = span(s.data+1, s.data+end);
|
|
r.rest = cuthead(s, end+1);
|
|
} else {
|
|
r.name = takehead(s, 1);
|
|
r.rest = cuthead(s, 1);
|
|
}
|
|
r.ok = 1;
|
|
return r;
|
|
}
|
|
|
|
// Parse a delimited escape argument: 'text'.
|
|
static Arg escdelim(Str s)
|
|
{
|
|
Arg r = {0};
|
|
if (!s.len) {
|
|
return r;
|
|
}
|
|
u8 d = s.data[0];
|
|
for (iz i = 1; i < s.len; i++) {
|
|
if (s.data[i] == '\\') {
|
|
i++;
|
|
} else if (s.data[i] == d) {
|
|
r.name = span(s.data+1, s.data+i);
|
|
r.rest = cuthead(s, i+1);
|
|
r.ok = 1;
|
|
return r;
|
|
}
|
|
}
|
|
// Unterminated: take the rest
|
|
r.name = cuthead(s, 1);
|
|
r.rest = (Str){s.data+s.len, 0};
|
|
r.ok = 1;
|
|
return r;
|
|
}
|
|
|
|
static i32 getreg(Ctx *c, Str name);
|
|
static i32 textwidth(Ctx *c, Str s, Arena scratch);
|
|
|
|
// Interpolate strings, registers, macro arguments, and widths. Other
|
|
// escapes pass through unchanged. Truncates at comments.
|
|
static Str expand(Ctx *c, Str s, Arena *a, i32 depth)
|
|
{
|
|
iz i = 0;
|
|
for (; i<s.len && s.data[i]!='\\'; i++) {}
|
|
if (i == s.len) {
|
|
return s;
|
|
}
|
|
|
|
Buf b = {0};
|
|
bappend(&b, takehead(s, i), a);
|
|
while (i < s.len) {
|
|
u8 ch = s.data[i];
|
|
if (ch!='\\' || i+1==s.len) {
|
|
bappendu8(&b, ch, a);
|
|
i++;
|
|
continue;
|
|
}
|
|
u8 e = s.data[i+1];
|
|
Str rest = cuthead(s, i+2);
|
|
switch (e) {
|
|
case '\\':
|
|
bappend(&b, S("\\\\"), a);
|
|
i += 2;
|
|
continue;
|
|
|
|
case '"':
|
|
return bstr(b);
|
|
|
|
case '#':
|
|
return bstr(b);
|
|
|
|
case '*': {
|
|
Arg n = escname(rest);
|
|
if (!n.ok) {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
Str name = n.name;
|
|
if (rest.data[0] == '[') {
|
|
// \*[name args]: arguments are ignored
|
|
if (depth < 32) {
|
|
name = expand(c, name, a, depth+1);
|
|
}
|
|
name = cut(trim(name), ' ').head;
|
|
}
|
|
Str v = getstring(c, name);
|
|
if (!v.data) {
|
|
warnf(c, S("undefined string"), name);
|
|
} else if (depth<32 && c->budget>0 && b.len<1<<20) {
|
|
// Bound nested string bombs by work and by size
|
|
c->budget--;
|
|
bappend(&b, expand(c, v, a, depth+1), a);
|
|
}
|
|
i = n.rest.data - s.data;
|
|
} continue;
|
|
|
|
case 'n': {
|
|
i32 inc = 0;
|
|
if (rest.len && (rest.data[0]=='+' || rest.data[0]=='-')) {
|
|
inc = rest.data[0]=='+' ? 1 : -1;
|
|
rest = cuthead(rest, 1);
|
|
}
|
|
Arg n = escname(rest);
|
|
if (!n.ok) {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
Str name = n.name;
|
|
if (rest.data[0]=='[' && depth<32) {
|
|
name = expand(c, name, a, depth+1);
|
|
}
|
|
if (inc) {
|
|
Entry *r = lookup(&c->regs, name, &c->perm);
|
|
r->live = 1;
|
|
r->num = clampi32((i64)r->num + (i64)inc*r->inc);
|
|
}
|
|
bappend(&b, fmti64(a, getreg(c, name)), a);
|
|
i = n.rest.data - s.data;
|
|
} continue;
|
|
|
|
case '$': {
|
|
Frame *f = argframe(c);
|
|
Arg n = escname(rest);
|
|
if (!n.ok) {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
Str name = n.name;
|
|
Str v = {0};
|
|
if (f && equals(name, S("*"))) {
|
|
for (i32 j = 1; j < f->nargs; j++) {
|
|
v = concat(a, v, j>1 ? S(" ") : S(""));
|
|
v = concat(a, v, f->args[j]);
|
|
}
|
|
} else if (f && equals(name, S("@"))) {
|
|
for (i32 j = 1; j < f->nargs; j++) {
|
|
v = concat(a, v, j>1 ? S(" \"") : S("\""));
|
|
v = concat(a, v, f->args[j]);
|
|
v = concat(a, v, S("\""));
|
|
}
|
|
} else if (f) {
|
|
i32 k = 0;
|
|
for (iz j = 0; j < name.len; j++) {
|
|
if (!digit(name.data[j]) || k>1000) {
|
|
k = -1;
|
|
break;
|
|
}
|
|
k = k*10 + name.data[j] - '0';
|
|
}
|
|
if (k>=0 && k<f->nargs) {
|
|
v = f->args[k];
|
|
}
|
|
}
|
|
if (depth < 32) {
|
|
bappend(&b, expand(c, v, a, depth+1), a);
|
|
}
|
|
i = n.rest.data - s.data;
|
|
} continue;
|
|
|
|
case 'w': {
|
|
Arg d = escdelim(rest);
|
|
if (!d.ok) {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
Str t = depth<32 ? expand(c, d.name, a, depth+1) : d.name;
|
|
i32 w = textwidth(c, t, *a);
|
|
bappend(&b, fmti64(a, (i64)w*U_PER_N), a);
|
|
i = d.rest.data - s.data;
|
|
} continue;
|
|
|
|
default:
|
|
bappendu8(&b, '\\', a);
|
|
bappendu8(&b, e, a);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
return bstr(b);
|
|
}
|
|
|
|
// Copy mode: reduce \\ to \.
|
|
static Str copymode(Str s, Arena *a)
|
|
{
|
|
iz i = 0;
|
|
for (; i+1<s.len && !(s.data[i]=='\\' && s.data[i+1]=='\\'); i++) {}
|
|
if (i+1 >= s.len) {
|
|
return s;
|
|
}
|
|
Buf b = {0};
|
|
for (i = 0; i < s.len; i++) {
|
|
bappendu8(&b, s.data[i], a);
|
|
if (s.data[i]=='\\' && i+1<s.len) {
|
|
if (s.data[i+1] != '\\') {
|
|
bappendu8(&b, s.data[i+1], a);
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
return bstr(b);
|
|
}
|
|
|
|
|
|
// Registers and expressions
|
|
|
|
static i32 getreg(Ctx *c, Str name)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
if (name.len && name.data[0]=='.') {
|
|
static struct { Str name; i32 value; } fixed[] = {
|
|
{STR(".g"), 1}, {STR(".H"), 24}, {STR(".V"), 40},
|
|
{STR(".T"), 1}, {STR(".A"), 0}, {STR(".s"), 10},
|
|
{STR(".v"), 40}, {STR(".w"), 24}, {STR(".x"), 1},
|
|
{STR(".y"), 23}, {STR(".Y"), 0}, {STR(".ss"), 12},
|
|
{STR(".C"), 0}, {STR(".cp"), 0},
|
|
{STR(".P"), 1}, {STR(".R"), 10000}, {STR(".t"), 1<<24},
|
|
};
|
|
for (i32 i = 0; i < countof(fixed); i++) {
|
|
if (equals(name, fixed[i].name)) {
|
|
return fixed[i].value;
|
|
}
|
|
}
|
|
if (equals(name, S(".sss"))) return ts->sentspace;
|
|
if (equals(name, S(".l"))) return ts->ll * U_PER_N;
|
|
if (equals(name, S(".i"))) return ts->indent * U_PER_N;
|
|
if (equals(name, S(".u"))) return ts->fill;
|
|
if (equals(name, S(".j"))) return ts->adjust;
|
|
if (equals(name, S(".f"))) return c->font + 1;
|
|
if (equals(name, S(".c"))) return c->frame ? c->frame->line : 0;
|
|
if (equals(name, S(".$"))) {
|
|
Frame *f = argframe(c);
|
|
return f ? f->nargs-1 : 0;
|
|
}
|
|
}
|
|
if (equals(name, S("an-margin"))) {
|
|
return c->margin;
|
|
}
|
|
Entry *e = find(c->regs, name);
|
|
return e ? e->num : 0;
|
|
}
|
|
|
|
static void setreg(Ctx *c, Str name, i32 value)
|
|
{
|
|
Entry *e = lookup(&c->regs, name, &c->perm);
|
|
e->num = value;
|
|
e->live = 1;
|
|
}
|
|
|
|
typedef struct {
|
|
Str rest;
|
|
i64 value;
|
|
b32 ok;
|
|
} Num;
|
|
|
|
static i64 unitscale(u8 u)
|
|
{
|
|
switch (u) {
|
|
case 'i': return 240;
|
|
case 'c': return 94;
|
|
case 'p': return 3;
|
|
case 'P': return 40;
|
|
case 'm': return 24;
|
|
case 'n': return 24;
|
|
case 'v': return 40;
|
|
case 'u': return 1;
|
|
case 'M': return 0; // hundredths of an em, handled below
|
|
case 's': return 1;
|
|
case 'z': return 1;
|
|
case 'f': return 1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static i64 clampnum(i64 x)
|
|
{
|
|
return x>(i64)1<<40 ? (i64)1<<40 : x<-((i64)1<<40) ? -((i64)1<<40) : x;
|
|
}
|
|
|
|
// Multiply clamped numbers, saturating instead of overflowing.
|
|
static i64 mulnum(i64 x, i64 y)
|
|
{
|
|
i64 lim = (i64)1<<40;
|
|
if (y && (x>lim/(y<0?-y:y) || x<-lim/(y<0?-y:y))) {
|
|
return (x<0) == (y<0) ? lim : -lim;
|
|
}
|
|
return x * y;
|
|
}
|
|
|
|
static Num evalexpr(Str s, u8 unit, i32 depth);
|
|
|
|
static Num evalterm(Str s, u8 unit, i32 depth)
|
|
{
|
|
Num r = {0};
|
|
s = trimleft(s);
|
|
if (!s.len || depth>32) {
|
|
r.rest = s;
|
|
return r;
|
|
}
|
|
u8 ch = s.data[0];
|
|
if (ch == '(') {
|
|
r = evalexpr(cuthead(s, 1), unit, depth+1);
|
|
if (r.ok) {
|
|
Str t = trimleft(r.rest);
|
|
if (t.len && t.data[0]==')') {
|
|
t = cuthead(t, 1);
|
|
}
|
|
r.rest = t;
|
|
}
|
|
return r;
|
|
}
|
|
if (ch=='-' || ch=='+') {
|
|
r = evalterm(cuthead(s, 1), unit, depth+1);
|
|
if (ch == '-') r.value = -r.value;
|
|
return r;
|
|
}
|
|
if (ch == '|') {
|
|
return evalterm(cuthead(s, 1), unit, depth+1);
|
|
}
|
|
|
|
// Number with optional fraction and unit
|
|
i64 whole = 0, frac = 0, scale = 1;
|
|
iz i = 0;
|
|
b32 any = 0;
|
|
for (; i<s.len && digit(s.data[i]); i++) {
|
|
whole = clampnum(whole*10 + s.data[i] - '0');
|
|
any = 1;
|
|
}
|
|
if (i<s.len && s.data[i]=='.') {
|
|
for (i++; i<s.len && digit(s.data[i]); i++) {
|
|
if (scale < 1000000) {
|
|
frac = frac*10 + s.data[i] - '0';
|
|
scale *= 10;
|
|
}
|
|
any = 1;
|
|
}
|
|
}
|
|
if (!any) {
|
|
r.rest = s;
|
|
return r;
|
|
}
|
|
u8 u = unit;
|
|
if (i<s.len && unitscale(s.data[i])>=0) {
|
|
u = s.data[i++];
|
|
}
|
|
i64 m = unitscale(u);
|
|
if (u == 'M') {
|
|
r.value = (whole*24 + frac*24/scale) / 100;
|
|
} else if (u == 'p') {
|
|
r.value = (whole*240 + frac*240/scale) / 72;
|
|
} else if (u == 'c') {
|
|
r.value = (whole*24000 + frac*24000/scale) / 254;
|
|
} else {
|
|
r.value = whole*m + frac*m/scale;
|
|
}
|
|
r.value = clampnum(r.value);
|
|
r.rest = cuthead(s, i);
|
|
r.ok = 1;
|
|
return r;
|
|
}
|
|
|
|
// Evaluate left to right without precedence, as roff does.
|
|
static Num evalexpr(Str s, u8 unit, i32 depth)
|
|
{
|
|
Num r = evalterm(s, unit, depth);
|
|
while (r.ok) {
|
|
Str t = r.rest;
|
|
if (!t.len) {
|
|
break;
|
|
}
|
|
static Str ops[] = {
|
|
STR("<="), STR(">="), STR("=="), STR("<>"), STR(">?"), STR("<?"),
|
|
STR("+"), STR("-"), STR("*"), STR("/"), STR("%"), STR("<"),
|
|
STR(">"), STR("="), STR("&"), STR(":"),
|
|
};
|
|
i32 op = -1;
|
|
for (i32 i = 0; i < countof(ops); i++) {
|
|
if (startswith(t, ops[i])) {
|
|
op = i;
|
|
break;
|
|
}
|
|
}
|
|
if (op < 0) {
|
|
break;
|
|
}
|
|
Num b = evalterm(cuthead(t, ops[op].len), unit, depth);
|
|
if (!b.ok) {
|
|
break;
|
|
}
|
|
i64 x = r.value, y = b.value;
|
|
switch (op) {
|
|
case 0: x = x <= y; break;
|
|
case 1: x = x >= y; break;
|
|
case 2: x = x == y; break;
|
|
case 3: x = x != y; break;
|
|
case 4: x = x>y ? x : y; break;
|
|
case 5: x = x<y ? x : y; break;
|
|
case 6: x = x + y; break;
|
|
case 7: x = x - y; break;
|
|
case 8: x = mulnum(x, y); break;
|
|
case 9: x = y ? x / y : 0; break;
|
|
case 10: x = y ? x % y : 0; break;
|
|
case 11: x = x < y; break;
|
|
case 12: x = x > y; break;
|
|
case 13: x = x == y; break;
|
|
case 14: x = x>0 && y>0; break;
|
|
case 15: x = x>0 || y>0; break;
|
|
}
|
|
r.value = clampnum(x);
|
|
r.rest = b.rest;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
typedef struct {
|
|
i32 value;
|
|
b32 rel; // relative: -1, 0, +1
|
|
b32 ok;
|
|
} Scaled;
|
|
|
|
// Parse a request argument with an optional leading sign.
|
|
static Scaled scaled(Str s, u8 unit)
|
|
{
|
|
Scaled r = {0};
|
|
s = trim(s);
|
|
if (!s.len) {
|
|
return r;
|
|
}
|
|
if (s.data[0]=='+' || s.data[0]=='-') {
|
|
r.rel = s.data[0]=='+' ? 1 : -1;
|
|
s = cuthead(s, 1);
|
|
}
|
|
Num n = evalexpr(s, unit, 0);
|
|
if (!n.ok) {
|
|
return r;
|
|
}
|
|
i64 v = n.value * (r.rel<0 ? -1 : 1);
|
|
r.value = (i32)(v<-(1<<24) ? -(1<<24) : v>1<<24 ? 1<<24 : v);
|
|
r.ok = 1;
|
|
return r;
|
|
}
|
|
|
|
// Horizontal units to columns, rounded like groff.
|
|
static i32 hcols(i64 u)
|
|
{
|
|
i64 r = u<0 ? -((-u + 11)/U_PER_N) : (u + 11)/U_PER_N;
|
|
return (i32)(r<-(1<<20) ? -(1<<20) : r>1<<20 ? 1<<20 : r);
|
|
}
|
|
|
|
// Vertical units to lines, rounded like groff. Huge spaces are capped.
|
|
static i32 vlines(i64 u)
|
|
{
|
|
i64 r = u<0 ? -((-u + 19)/U_PER_V) : (u + 19)/U_PER_V;
|
|
return (i32)(r<-MAXV ? -MAXV : r>MAXV ? MAXV : r);
|
|
}
|
|
|
|
// Parse a man(7) indentation argument (default unit: n). Like groff's
|
|
// an-margin and an-prevailing-indent, the result stays in basic units,
|
|
// unrounded, so that fractional indents accumulate exactly.
|
|
static i32 manunits(Str s, i32 dflt)
|
|
{
|
|
Scaled v = scaled(s, 'n');
|
|
if (!v.ok) {
|
|
return dflt;
|
|
}
|
|
i32 max = 200*U_PER_N;
|
|
return v.value<0 ? 0 : v.value>max ? max : v.value;
|
|
}
|
|
|
|
|
|
// Text tokenizer
|
|
|
|
static i32 parsefont(Ctx *c, Str name)
|
|
{
|
|
if (!name.len || equals(name, S("P"))) {
|
|
return c->prevfont;
|
|
}
|
|
if (name.len==1 && digit(name.data[0])) {
|
|
i32 n = name.data[0] - '0';
|
|
return n==2 ? F_I : n==3 ? F_B : n==4 ? F_BI : F_R;
|
|
}
|
|
b32 b = 0, i = 0;
|
|
for (iz k = 0; k < name.len; k++) {
|
|
b |= name.data[k] == 'B';
|
|
i |= name.data[k] == 'I';
|
|
}
|
|
if (equals(name, S("BI")) || (b && i)) return F_BI;
|
|
if (b) return F_B;
|
|
if (i) return F_I;
|
|
return F_R;
|
|
}
|
|
|
|
static void setfont(Ctx *c, i32 font)
|
|
{
|
|
c->prevfont = c->font;
|
|
c->font = font;
|
|
}
|
|
|
|
static Str glyphtext(Ctx *c, Str name, Arena *a)
|
|
{
|
|
Entry *e = find(c->glyphmap, name);
|
|
if (e) {
|
|
return c->ts->utf8 ? e->str : e->alt;
|
|
}
|
|
// \[uXXXX] and \[uXXXX_YYYY], with 4 to 6 hex digits
|
|
if (name.len>=5 && name.data[0]=='u') {
|
|
i32 cp = 0;
|
|
iz i = 1;
|
|
for (; i<name.len && i<7; i++) {
|
|
u8 h = name.data[i];
|
|
i32 v = digit(h) ? h-'0' : h>='A'&&h<='F' ? h-'A'+10 :
|
|
h>='a'&&h<='f' ? h-'a'+10 : -1;
|
|
if (v < 0) break;
|
|
cp = cp<<4 | v;
|
|
}
|
|
if (i>=5 && (i==name.len || name.data[i]=='_')) {
|
|
Str s = encode(a, cp);
|
|
if (!c->ts->utf8 && cp>=0x80) {
|
|
Entry *t = find(c->asciimap, s);
|
|
return t ? t->str : S("?");
|
|
}
|
|
return s;
|
|
}
|
|
}
|
|
if (startswith(name, S("char"))) {
|
|
i32 cp = 0;
|
|
for (iz i = 4; i<name.len && digit(name.data[i]) && cp<1000; i++) {
|
|
cp = cp*10 + name.data[i] - '0';
|
|
}
|
|
if (cp>=32 && cp<127) {
|
|
return encode(a, cp);
|
|
}
|
|
}
|
|
warnf(c, S("unknown glyph"), name);
|
|
return (Str){0};
|
|
}
|
|
|
|
typedef struct {
|
|
Ctx *c;
|
|
i32 width;
|
|
b32 measure;
|
|
i32 part; // first token of a title part, where its columns start
|
|
} Sink;
|
|
|
|
// Accumulate a measured width, saturating so that it cannot overflow.
|
|
static i32 measured(i32 w, i64 n)
|
|
{
|
|
i64 r = w + n;
|
|
return (i32)(r<-(1<<20) ? -(1<<20) : r>1<<20 ? 1<<20 : r);
|
|
}
|
|
|
|
static void sinkglyph(Sink *k, Str s, u8 flags)
|
|
{
|
|
if (k->measure) {
|
|
k->width = measured(k->width, strwidth(s));
|
|
return;
|
|
}
|
|
Ctx *c = k->c;
|
|
if (c->trmap) {
|
|
Entry *e = find(c->trmap, s);
|
|
if (e) s = e->str;
|
|
}
|
|
tsglyph(c->ts, s, c->font, flags);
|
|
}
|
|
|
|
static void sinkspace(Sink *k, i32 n)
|
|
{
|
|
if (k->measure) {
|
|
k->width = measured(k->width, n);
|
|
} else {
|
|
tsspace(k->c->ts, n);
|
|
}
|
|
}
|
|
|
|
static void sinkmotion(Sink *k, i32 n)
|
|
{
|
|
if (k->measure) {
|
|
k->width = measured(k->width, n);
|
|
} else {
|
|
tsmotion(k->c->ts, n);
|
|
}
|
|
}
|
|
|
|
// Current output column, for tabs.
|
|
static i32 sinkcol(Sink *k)
|
|
{
|
|
if (k->measure) {
|
|
return k->width;
|
|
}
|
|
Typeset *ts = k->c->ts;
|
|
return tsstart(ts) + tswidth(ts, k->part, ts->ntoks);
|
|
}
|
|
|
|
static void sinktab(Sink *k)
|
|
{
|
|
Typeset *ts = k->c->ts;
|
|
i32 col = sinkcol(k);
|
|
i32 rel = col - (k->measure ? 0 : ts->indent);
|
|
i32 next = -1;
|
|
for (i32 i = 0; i < ts->ntabs; i++) {
|
|
if (ts->tabs[i] > rel) {
|
|
next = ts->tabs[i];
|
|
break;
|
|
}
|
|
}
|
|
if (next < 0) {
|
|
i32 last = ts->ntabs ? ts->tabs[ts->ntabs-1] : 0;
|
|
if (ts->ntabs && rel>=last) {
|
|
next = rel + 1; // past explicit stops
|
|
} else {
|
|
next = (rel/ts->tabw + 1) * ts->tabw;
|
|
}
|
|
}
|
|
sinkmotion(k, next - rel);
|
|
}
|
|
|
|
static b32 isbreakglyph(Str s)
|
|
{
|
|
return equals(s, S("-")) || equals(s, S("\xe2\x80\x90")) ||
|
|
equals(s, S("\xe2\x80\x94"));
|
|
}
|
|
|
|
// Interpret escapes in a text line and send glyphs to the sink.
|
|
// Returns true if the line ends with \c (continuation).
|
|
static b32 tokenize(Ctx *c, Str s, Sink *k, Arena scratch)
|
|
{
|
|
b32 contin = 0;
|
|
i32 i = 0;
|
|
while (i < s.len) {
|
|
contin = 0;
|
|
u8 ch = s.data[i];
|
|
if (ch == ' ') {
|
|
i32 n = 0;
|
|
for (; i<s.len && s.data[i]==' '; i++, n++) {}
|
|
sinkspace(k, n);
|
|
continue;
|
|
}
|
|
if (ch == '\t') {
|
|
i++;
|
|
sinktab(k);
|
|
continue;
|
|
}
|
|
if (ch != '\\') {
|
|
Rune r = decode(cuthead(s, i));
|
|
Str g = {s.data+i, r.len};
|
|
if (r.cp==0xfffd && r.len==1 && s.data[i]!=0xef) {
|
|
g = S("?");
|
|
}
|
|
if (!c->ts->utf8 && r.cp>=0x80) {
|
|
Entry *e = find(c->asciimap, g);
|
|
g = e ? e->str : S("?");
|
|
}
|
|
if (r.cp >= 0x20) {
|
|
sinkglyph(k, g, ch=='-' && !c->nobreak ? TF_BREAK : 0);
|
|
}
|
|
i += r.len;
|
|
continue;
|
|
}
|
|
|
|
// Escapes
|
|
if (i+1 == s.len) {
|
|
i++;
|
|
continue;
|
|
}
|
|
u8 e = s.data[i+1];
|
|
Str rest = cuthead(s, i+2);
|
|
i += 2;
|
|
switch (e) {
|
|
case '\\':
|
|
case 'e':
|
|
case 'E':
|
|
sinkglyph(k, S("\\"), 0);
|
|
break;
|
|
case '-':
|
|
sinkglyph(k, S("-"), 0);
|
|
break;
|
|
case '.':
|
|
sinkglyph(k, S("."), 0);
|
|
break;
|
|
case '\'':
|
|
sinkglyph(k, c->ts->utf8 ? S("\xc2\xb4") : S("'"), 0);
|
|
break;
|
|
case '`':
|
|
sinkglyph(k, S("`"), 0);
|
|
break;
|
|
case '&':
|
|
case ')':
|
|
if (!k->measure) tszero(c->ts);
|
|
break;
|
|
case '%':
|
|
// Discretionary hyphen, except at the start of a word
|
|
if (!k->measure) {
|
|
Typeset *ts = c->ts;
|
|
if (hasglyphs(ts, ts->word, ts->ntoks)) {
|
|
tszero(ts);
|
|
ts->toks[ts->ntoks-1].flags = TF_BREAK | TF_HYPH;
|
|
}
|
|
}
|
|
break;
|
|
case '|': case '^': case ',': case '/':
|
|
case '{': case '}':
|
|
break;
|
|
case ':':
|
|
// Break point without a hyphen
|
|
if (!k->measure) {
|
|
tszero(c->ts);
|
|
c->ts->toks[c->ts->ntoks-1].flags = TF_BREAK;
|
|
}
|
|
break;
|
|
case ' ': case '~': case '0':
|
|
if (k->measure) {
|
|
k->width = measured(k->width, 1);
|
|
} else {
|
|
tsglyph(c->ts, S(" "), c->font, 0);
|
|
}
|
|
break;
|
|
case 'c':
|
|
contin = i == s.len;
|
|
break;
|
|
case 't':
|
|
case 'a':
|
|
sinktab(k);
|
|
break;
|
|
case 'p':
|
|
break;
|
|
|
|
case 'f': {
|
|
Arg n = escname(rest);
|
|
if (n.ok) {
|
|
if (n.name.len==0 && rest.len && rest.data[0]=='[') {
|
|
setfont(c, c->prevfont);
|
|
} else {
|
|
setfont(c, parsefont(c, n.name));
|
|
}
|
|
i = (i32)(n.rest.data - s.data);
|
|
}
|
|
} break;
|
|
|
|
case '(':
|
|
case '[': {
|
|
Arg n = escname(cuthead(s, i-1));
|
|
if (n.ok) {
|
|
Str g = glyphtext(c, n.name, &scratch);
|
|
if (g.len) {
|
|
sinkglyph(k, g, isbreakglyph(g) ? TF_BREAK : 0);
|
|
}
|
|
i = (i32)(n.rest.data - s.data);
|
|
}
|
|
} break;
|
|
|
|
case 'C':
|
|
case 'N': {
|
|
Arg d = escdelim(rest);
|
|
if (d.ok) {
|
|
Str g = {0};
|
|
if (e == 'C') {
|
|
g = glyphtext(c, d.name, &scratch);
|
|
} else {
|
|
i32 cp = 0;
|
|
for (iz j = 0; j<d.name.len && digit(d.name.data[j]) && cp<0x110000; j++) {
|
|
cp = cp*10 + d.name.data[j] - '0';
|
|
}
|
|
if (cp >= 32) {
|
|
Str t = encode(&scratch, cp);
|
|
g = c->ts->utf8||cp<0x80 ? t : S("?");
|
|
}
|
|
}
|
|
if (g.len) {
|
|
sinkglyph(k, g, 0);
|
|
}
|
|
i = (i32)(d.rest.data - s.data);
|
|
}
|
|
} break;
|
|
|
|
case 'h': {
|
|
Arg d = escdelim(rest);
|
|
if (d.ok) {
|
|
Str arg = d.name;
|
|
b32 abs = arg.len && arg.data[0]=='|';
|
|
Num n = evalexpr(abs ? cuthead(arg, 1) : arg, 'm', 0);
|
|
if (n.ok) {
|
|
i32 cols = hcols(n.value);
|
|
if (abs) {
|
|
cols -= sinkcol(k) - (k->measure ? 0 : tsstart(c->ts));
|
|
}
|
|
sinkmotion(k, cols);
|
|
}
|
|
i = (i32)(d.rest.data - s.data);
|
|
}
|
|
} break;
|
|
|
|
case 'z': {
|
|
// Zero-width glyph: the next character does not advance
|
|
if (i < s.len && s.data[i] != '\\') {
|
|
Rune r = decode(cuthead(s, i));
|
|
if (!k->measure) {
|
|
tsglyph(c->ts, (Str){s.data+i, r.len}, c->font, TF_ZERO);
|
|
}
|
|
i += r.len;
|
|
}
|
|
} break;
|
|
|
|
case 'o': {
|
|
// Overstrike: approximate with the last character
|
|
Arg d = escdelim(rest);
|
|
if (d.ok) {
|
|
Str t = d.name;
|
|
if (t.len) {
|
|
iz j = t.len - 1;
|
|
for (; j>0 && (t.data[j]&0xc0)==0x80; j--) {}
|
|
sinkglyph(k, cuthead(t, j), 0);
|
|
}
|
|
i = (i32)(d.rest.data - s.data);
|
|
}
|
|
} break;
|
|
|
|
case 's': {
|
|
// Size changes are ignored: \s-1 \s+2 \s0 \s(12 \s[12] \s'12'
|
|
iz j = i;
|
|
if (j<s.len && (s.data[j]=='+' || s.data[j]=='-')) j++;
|
|
if (j < s.len) {
|
|
u8 d = s.data[j];
|
|
if (d == '(') {
|
|
j += 3;
|
|
} else if (d == '[') {
|
|
for (; j<s.len && s.data[j]!=']'; j++) {}
|
|
j++;
|
|
} else if (d=='\'' || d=='"') {
|
|
Arg a = escdelim(cuthead(s, j));
|
|
j = a.rest.data - s.data;
|
|
} else if (digit(d)) {
|
|
j++;
|
|
if (j<s.len && digit(s.data[j]) && s.data[j-1]>='1' && s.data[j-1]<='3') {
|
|
j++;
|
|
}
|
|
}
|
|
}
|
|
i = (i32)(j<s.len ? j : s.len);
|
|
} break;
|
|
|
|
case 'k': {
|
|
Arg n = escname(rest);
|
|
if (n.ok) {
|
|
if (!k->measure) {
|
|
Typeset *ts = c->ts;
|
|
setreg(c, n.name, clampi32((i64)tswidth(ts, 0, ts->ntoks)*U_PER_N));
|
|
}
|
|
i = (i32)(n.rest.data - s.data);
|
|
}
|
|
} break;
|
|
|
|
case 'v': case 'X': case 'Z': case 'D': case 'l': case 'L':
|
|
case 'b': case 'x': case 'R': case 'A': case 'B': case 'S':
|
|
case 'H': {
|
|
// Not representable on a terminal: dropped
|
|
Arg d = escdelim(rest);
|
|
if (d.ok) {
|
|
i = (i32)(d.rest.data - s.data);
|
|
}
|
|
} break;
|
|
|
|
case 'm': case 'M': case 'g': case 'V': case 'Y': case 'F': case 'j': {
|
|
Arg n = escname(rest);
|
|
if (n.ok) {
|
|
i = (i32)(n.rest.data - s.data);
|
|
}
|
|
} break;
|
|
|
|
case 'u': case 'd': case 'r':
|
|
break;
|
|
|
|
case '*': case 'n': case '$': case 'w': case '"': case '#':
|
|
// Already expanded; leftovers are malformed
|
|
break;
|
|
|
|
default: {
|
|
// Unknown escape: print the character
|
|
Rune r = decode(cuthead(s, i-1));
|
|
warnf(c, S("unknown escape"), (Str){s.data+i-1, r.len});
|
|
sinkglyph(k, (Str){s.data+i-1, r.len}, 0);
|
|
i += r.len - 1;
|
|
} break;
|
|
}
|
|
}
|
|
return contin;
|
|
}
|
|
|
|
static i32 textwidth(Ctx *c, Str s, Arena scratch)
|
|
{
|
|
Sink k = {0};
|
|
k.c = c;
|
|
k.measure = 1;
|
|
i32 font = c->font, prev = c->prevfont;
|
|
tokenize(c, s, &k, scratch);
|
|
c->font = font;
|
|
c->prevfont = prev;
|
|
return k.width;
|
|
}
|
|
|
|
|
|
// man(7)
|
|
|
|
static void processline(Ctx *c, Str line, Arena scratch, b32 expanded, i32 depth);
|
|
static void firetrap(Ctx *c);
|
|
static void resetadjust(Ctx *c);
|
|
static Str joinargs(Str *args, i32 nargs, Arena *a);
|
|
static void mdheader(Ctx *c, Arena scratch);
|
|
static void mdfooter(Ctx *c, Arena scratch);
|
|
|
|
static void textline(Ctx *c, Str s, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
if (c->mdoc && !c->page) {
|
|
mdheader(c, scratch);
|
|
}
|
|
if (c->mdnospace && !c->cont) {
|
|
tsunspace(ts); // .Sm off joins text lines too
|
|
}
|
|
c->mdnospace = c->mdsmoff; // a text word uses up a no-space request
|
|
c->mdeos = 0;
|
|
c->mdsynprev = D_NO; // in SYNOPSIS, text is like a .No line
|
|
if (!s.len && !c->cont) {
|
|
tsvspace(ts, 1); // blank line
|
|
return;
|
|
}
|
|
|
|
// Trailing spaces are not significant, unless escaped
|
|
iz end = s.len;
|
|
for (; end>0 && s.data[end-1]==' '; end--) {}
|
|
if (end<s.len && oddbackslashes(takehead(s, end))) {
|
|
end++;
|
|
}
|
|
s = takehead(s, end);
|
|
|
|
b32 brk = ts->center>0 || !ts->fill;
|
|
if (ts->fill && !c->cont && (s.data[0]==' ' || s.data[0]=='\t')) {
|
|
tsbreak(ts);
|
|
iz n = 0;
|
|
for (; n<s.len && s.data[n]==' '; n++) {}
|
|
if (n) {
|
|
tsmotion(ts, (i32)n);
|
|
s = cuthead(s, n);
|
|
}
|
|
}
|
|
|
|
Sink k = {0};
|
|
k.c = c;
|
|
b32 contin = tokenize(c, s, &k, scratch);
|
|
c->cont = contin;
|
|
if (contin) {
|
|
return;
|
|
}
|
|
|
|
if (brk) {
|
|
b32 empty = tsempty(ts);
|
|
tsbreak(ts);
|
|
if (empty) {
|
|
// An empty no-fill line still produces a blank line
|
|
ts->vspace++;
|
|
}
|
|
} else {
|
|
tsendline(ts);
|
|
}
|
|
firetrap(c);
|
|
}
|
|
|
|
static void setroman(Ctx *c)
|
|
{
|
|
c->font = F_R;
|
|
c->prevfont = F_R;
|
|
}
|
|
|
|
// Write a paragraph tag (an-write-paragraph-tag): on the same line as
|
|
// the body if it fits within the prevailing indent, else on its own.
|
|
static void finishtag(Ctx *c)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
tscommit(ts);
|
|
i32 w = tspending(ts);
|
|
b32 any = hasglyphs(ts, 0, ts->ntoks);
|
|
b32 fits = any && ts->lines==c->taglines && (i64)(w+1)*U_PER_N<=c->pindent;
|
|
if (fits) {
|
|
tsfreeze(ts);
|
|
tsmotion(ts, hcols(c->margin + c->pindent) - hcols(c->margin) - w);
|
|
ts->word = ts->ntoks;
|
|
} else if (any) {
|
|
tsbreak(ts);
|
|
} else {
|
|
// An empty tag that does not fit still occupies a line
|
|
ts->ntoks = ts->nbytes = ts->word = 0;
|
|
if (c->pindent < U_PER_N) {
|
|
clearrow(ts, 1);
|
|
emitrow(ts, 0);
|
|
}
|
|
}
|
|
ts->indent = hcols(c->margin + c->pindent);
|
|
ts->tindent = fits ? hcols(c->margin) : -1;
|
|
resetadjust(c);
|
|
}
|
|
|
|
static void firetrap(Ctx *c)
|
|
{
|
|
i32 trap = c->trap;
|
|
if (!trap) {
|
|
return;
|
|
}
|
|
c->trap = 0;
|
|
setroman(c);
|
|
if (trap & TRAP_HEAD) {
|
|
tsbreak(c->ts);
|
|
c->ts->nospace = 1;
|
|
}
|
|
if (trap & TRAP_TAG) {
|
|
finishtag(c);
|
|
}
|
|
}
|
|
|
|
// Paragraph spacing, then no-space mode (an-break-paragraph).
|
|
static void breakpara(Ctx *c)
|
|
{
|
|
setroman(c);
|
|
tsvspace(c->ts, c->pd);
|
|
c->ts->nospace = 1;
|
|
}
|
|
|
|
static void resetmargin(Ctx *c)
|
|
{
|
|
c->rsdepth = 0;
|
|
c->margin = IN;
|
|
c->pindent = IN;
|
|
}
|
|
|
|
static void endpage(Ctx *c, Arena scratch)
|
|
{
|
|
if (!c->page) {
|
|
return;
|
|
}
|
|
if (c->mdoc) {
|
|
mdfooter(c, scratch);
|
|
return;
|
|
}
|
|
Typeset *ts = c->ts;
|
|
tsbreak(ts);
|
|
ts->vspace = 1;
|
|
ts->nospace = 0;
|
|
ts->ntabs = 0; // titles use the default tab stops
|
|
|
|
// Footer: source, date, title(section)
|
|
i32 font = c->font, prev = c->prevfont;
|
|
b32 fill = ts->fill;
|
|
ts->fill = 0; // titles never wrap
|
|
Sink k = {0};
|
|
k.c = c;
|
|
c->font = F_R;
|
|
tokenize(c, c->source, &k, scratch);
|
|
i32 left = k.part = ts->ntoks;
|
|
tokenize(c, c->date, &k, scratch);
|
|
i32 center = k.part = ts->ntoks;
|
|
c->font = F_I;
|
|
tokenize(c, c->title, &k, scratch);
|
|
c->font = F_R;
|
|
tokenize(c, S("("), &k, scratch);
|
|
tokenize(c, c->section, &k, scratch);
|
|
tokenize(c, S(")"), &k, scratch);
|
|
tstitle(ts, left, center);
|
|
ts->fill = fill;
|
|
c->font = font;
|
|
c->prevfont = prev;
|
|
c->page = 0;
|
|
}
|
|
|
|
static Str defaultmanual(Str section)
|
|
{
|
|
if (section.len != 1) return S("");
|
|
switch (section.data[0]) {
|
|
case '1': return S("General Commands Manual");
|
|
case '2': return S("System Calls Manual");
|
|
case '3': return S("Library Functions Manual");
|
|
case '4': return S("Kernel Interfaces Manual");
|
|
case '5': return S("File Formats Manual");
|
|
case '6': return S("Games Manual");
|
|
case '7': return S("Miscellaneous Information Manual");
|
|
case '8': return S("System Manager's Manual");
|
|
case '9': return S("Kernel Developer's Manual");
|
|
}
|
|
return S("");
|
|
}
|
|
|
|
static b32 hasbackslash(Str s)
|
|
{
|
|
for (iz i = 0; i < s.len; i++) {
|
|
if (s.data[i] == '\\') return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Length of the copy-mode character at the start of a string, where an
|
|
// escape like \- is one character but \fB is three.
|
|
static iz charlen(Str s)
|
|
{
|
|
b32 one = s.len>1 && s.data[0]=='\\' &&
|
|
cut(S("-&e ~|^:%'`_)c\\.!?{}Eat"), s.data[1]).ok;
|
|
return one ? 2 : 1;
|
|
}
|
|
|
|
// Offset of a copy-mode character in a string.
|
|
static iz charoff(Str s, iz n)
|
|
{
|
|
iz i = 0;
|
|
for (; n>0 && i<s.len; n--) {
|
|
i += charlen(cuthead(s, i));
|
|
}
|
|
return i;
|
|
}
|
|
|
|
// Shorten an overlong page topic by removing characters from its
|
|
// middle, like groff's an*abbreviate-page-topic, including its left to
|
|
// right evaluation of "m2 <? len - 1". It counts copy-mode characters
|
|
// and measures the formatted header, where tabs depend on position.
|
|
// Each round keeps a head and tail of the previous round's, so the
|
|
// result is a head and tail of the topic joined by "...".
|
|
static Str abbrevtopic(Ctx *c, Str topic, Arena scratch)
|
|
{
|
|
iz n = 0;
|
|
for (iz i = 0; i < topic.len; n++) {
|
|
i += charlen(cuthead(topic, i));
|
|
}
|
|
|
|
Str sect = concat(&scratch, S("("), c->section);
|
|
sect = concat(&scratch, sect, S(")"));
|
|
i32 rest = 2*textwidth(c, sect, scratch) + textwidth(c, c->manual, scratch);
|
|
b32 tabs = cut(topic, '\t').ok || cut(sect, '\t').ok || cut(c->manual, '\t').ok;
|
|
b32 plain = (!tabs && !hasbackslash(topic)) || n>MAXW; // width is length
|
|
iz len = n; // without the "..."
|
|
iz head = n;
|
|
iz tail = 0;
|
|
iz dots = 0;
|
|
for (;;) {
|
|
i64 w = 2*(head + dots + tail) + rest;
|
|
if (!plain) {
|
|
Arena tmp = scratch;
|
|
Str r = concat(&tmp, takehead(topic, charoff(topic, head)), dots ? S("...") : S(""));
|
|
r = concat(&tmp, r, cuthead(topic, charoff(topic, n-tail)));
|
|
r = concat(&tmp, r, sect);
|
|
w = textwidth(c, concat(&tmp, concat(&tmp, r, c->manual), r), tmp);
|
|
}
|
|
if (w < c->ts->ll) break;
|
|
iz m1 = len/2 - 2;
|
|
iz m2 = len/2 + 2;
|
|
iz pend = m1>1 ? m1 : 1;
|
|
iz sbeg = (m2<len ? m2 : len) - 1;
|
|
if (pend+1>len || sbeg<0 || sbeg>len) break;
|
|
head = pend + 1;
|
|
tail = len - sbeg;
|
|
dots = 3;
|
|
if (head+tail >= len) break;
|
|
len = head + tail;
|
|
}
|
|
if (!dots) {
|
|
return topic;
|
|
}
|
|
Str r = concat(&c->perm, takehead(topic, charoff(topic, head)), S("..."));
|
|
return concat(&c->perm, r, cuthead(topic, charoff(topic, n-tail)));
|
|
}
|
|
|
|
// Truncate an overlong inner footer with an ellipsis, like groff.
|
|
static Str abbrevfooter(Ctx *c, Str foot, Arena scratch)
|
|
{
|
|
i64 halftitle = c->ts->ll*U_PER_N/2;
|
|
i64 halfcfoot = (i64)textwidth(c, c->date, scratch)*U_PER_N/2;
|
|
i64 width = (i64)textwidth(c, foot, scratch)*U_PER_N + halfcfoot;
|
|
if (width<halftitle || hasbackslash(foot)) {
|
|
return foot;
|
|
}
|
|
width = ((i64)foot.len + 3)*U_PER_N + halfcfoot;
|
|
Str r = foot;
|
|
for (iz end = foot.len-1; width>=halftitle && end>0; end--) {
|
|
i64 nw = ((i64)end + 3)*U_PER_N + halfcfoot;
|
|
if (nw >= width) break;
|
|
width = nw;
|
|
r = takehead(foot, end);
|
|
}
|
|
return concat(&c->perm, r, S("..."));
|
|
}
|
|
|
|
static void manth(Ctx *c, Str *args, i32 nargs, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
endpage(c, scratch);
|
|
tsbreak(ts);
|
|
Arena *perm = &c->perm;
|
|
c->title = clone(perm, nargs>0 ? args[0] : S(""));
|
|
c->section = clone(perm, nargs>1 ? args[1] : S(""));
|
|
c->date = clone(perm, nargs>2 ? args[2] : S(""));
|
|
c->source = clone(perm, nargs>3 ? args[3] : S(""));
|
|
c->manual = clone(perm, nargs>4 ? args[4] : defaultmanual(c->section));
|
|
c->page = 1;
|
|
ts->ntabs = 0; // titles use the default tab stops
|
|
c->title = abbrevtopic(c, c->title, scratch);
|
|
c->source = abbrevfooter(c, c->source, scratch);
|
|
|
|
// Header: title(section), manual, title(section)
|
|
ts->fill = 0; // titles never wrap
|
|
Sink k = {0};
|
|
k.c = c;
|
|
c->font = F_I;
|
|
tokenize(c, c->title, &k, scratch);
|
|
c->font = F_R;
|
|
tokenize(c, S("("), &k, scratch);
|
|
tokenize(c, c->section, &k, scratch);
|
|
tokenize(c, S(")"), &k, scratch);
|
|
i32 left = k.part = ts->ntoks;
|
|
tokenize(c, c->manual, &k, scratch);
|
|
i32 center = k.part = ts->ntoks;
|
|
c->font = F_I;
|
|
tokenize(c, c->title, &k, scratch);
|
|
c->font = F_R;
|
|
tokenize(c, S("("), &k, scratch);
|
|
tokenize(c, c->section, &k, scratch);
|
|
tokenize(c, S(")"), &k, scratch);
|
|
tstitle(ts, left, center);
|
|
printu8(ts->out, '\n');
|
|
|
|
resetmargin(c);
|
|
setroman(c);
|
|
c->pd = 1;
|
|
c->trap = 0;
|
|
c->example = 0;
|
|
c->sy = 0;
|
|
ts->fill = 1;
|
|
ts->indent = hcols(c->margin);
|
|
ts->tindent = -1;
|
|
resetadjust(c);
|
|
ts->nospace = 1;
|
|
ts->vspace = 0;
|
|
}
|
|
|
|
static void manheading(Ctx *c, i32 kind, Str *args, i32 nargs, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
breakpara(c);
|
|
resetmargin(c);
|
|
ts->fill = 1;
|
|
ts->indent = hcols(c->margin);
|
|
ts->tindent = kind==M_SH ? 0 : SN;
|
|
c->trap |= TRAP_HEAD;
|
|
setfont(c, F_B);
|
|
if (nargs) {
|
|
Str text = concat(&scratch, S("\\&"), joinargs(args, nargs, &scratch));
|
|
textline(c, text, scratch);
|
|
}
|
|
}
|
|
|
|
static Str joinargs(Str *args, i32 nargs, Arena *a)
|
|
{
|
|
Str r = {0};
|
|
for (i32 i = 0; i < nargs; i++) {
|
|
if (i) r = concat(a, r, S(" "));
|
|
r = concat(a, r, args[i]);
|
|
}
|
|
if (!r.data) r = S("");
|
|
return r;
|
|
}
|
|
|
|
static Str fontesc(i32 font)
|
|
{
|
|
switch (font) {
|
|
case F_B: return S("\\fB");
|
|
case F_I: return S("\\fI");
|
|
case F_BI: return S("\\f(BI");
|
|
}
|
|
return S("\\fR");
|
|
}
|
|
|
|
static void manfont(Ctx *c, i32 kind, Str *args, i32 nargs, Arena scratch)
|
|
{
|
|
i32 a = F_R, b = F_R;
|
|
switch (kind) {
|
|
case M_B: case M_SB: a = F_B; break;
|
|
case M_I: a = F_I; break;
|
|
case M_SM: a = F_R; break;
|
|
case M_BI: a = F_B; b = F_I; break;
|
|
case M_BR: a = F_B; b = F_R; break;
|
|
case M_IB: a = F_I; b = F_B; break;
|
|
case M_IR: a = F_I; b = F_R; break;
|
|
case M_RB: a = F_R; b = F_B; break;
|
|
case M_RI: a = F_R; b = F_I; break;
|
|
}
|
|
|
|
b32 alternating = kind!=M_B && kind!=M_I && kind!=M_SM && kind!=M_SB;
|
|
if (!alternating) {
|
|
// Arguments, or else the next text line, then back to roman
|
|
c->trap |= TRAP_FONT;
|
|
if (kind != M_SM) {
|
|
setfont(c, a);
|
|
}
|
|
if (nargs) {
|
|
Str text = concat(&scratch, S("\\&"), joinargs(args, nargs, &scratch));
|
|
textline(c, text, scratch);
|
|
}
|
|
return;
|
|
}
|
|
if (!nargs) {
|
|
return;
|
|
}
|
|
|
|
Str text = S("\\&");
|
|
for (i32 i = 0; i < nargs; i++) {
|
|
text = concat(&scratch, text, fontesc(i&1 ? b : a));
|
|
text = concat(&scratch, text, args[i]);
|
|
}
|
|
textline(c, text, scratch);
|
|
setroman(c);
|
|
}
|
|
|
|
// The AD string sets adjustment at .TH and after each paragraph tag.
|
|
static void resetadjust(Ctx *c)
|
|
{
|
|
Str ad = getstring(c, S("AD"));
|
|
c->ts->adjust = ad.len && ad.data[0]=='l' ? AD_L :
|
|
ad.len && ad.data[0]=='c' ? AD_C :
|
|
ad.len && ad.data[0]=='r' ? AD_R : AD_B;
|
|
}
|
|
|
|
// Begin a tagged paragraph: the next text line is the tag.
|
|
static void mantag(Ctx *c, Str width)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
breakpara(c);
|
|
if (width.data) {
|
|
c->pindent = manunits(width, c->pindent);
|
|
}
|
|
ts->indent = hcols(c->margin);
|
|
ts->tindent = -1;
|
|
ts->adjust = AD_L; // tags are not adjusted
|
|
c->taglines = ts->lines;
|
|
c->trap |= TRAP_TAG;
|
|
}
|
|
|
|
static void manmacro(Ctx *c, i32 kind, Str *args, i32 nargs, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
switch (kind) {
|
|
case M_TH:
|
|
manth(c, args, nargs, scratch);
|
|
break;
|
|
|
|
case M_SH:
|
|
case M_SS:
|
|
manheading(c, kind, args, nargs, scratch);
|
|
break;
|
|
|
|
case M_PP:
|
|
breakpara(c);
|
|
ts->indent = hcols(c->margin);
|
|
ts->tindent = -1;
|
|
c->pindent = IN;
|
|
break;
|
|
|
|
case M_TP:
|
|
mantag(c, nargs ? args[0] : (Str){0});
|
|
break;
|
|
|
|
case M_TQ:
|
|
tsbreak(ts);
|
|
ts->nospace = 1;
|
|
mantag(c, nargs ? args[0] : (Str){0});
|
|
break;
|
|
|
|
case M_IP:
|
|
if (!nargs) {
|
|
breakpara(c);
|
|
ts->indent = hcols(c->margin + c->pindent);
|
|
ts->tindent = -1;
|
|
break;
|
|
}
|
|
mantag(c, nargs>1 ? args[1] : (Str){0});
|
|
textline(c, concat(&scratch, S("\\&"), args[0]), scratch);
|
|
break;
|
|
|
|
case M_HP:
|
|
breakpara(c);
|
|
if (nargs) c->pindent = manunits(args[0], c->pindent);
|
|
ts->indent = hcols(c->margin + c->pindent);
|
|
ts->tindent = hcols(c->margin);
|
|
break;
|
|
|
|
case M_RS: {
|
|
tsbreak(ts);
|
|
if (c->rsdepth < MAXRS) {
|
|
c->rsmargin[c->rsdepth] = c->margin;
|
|
c->rspindent[c->rsdepth] = c->pindent;
|
|
c->rsdepth++;
|
|
}
|
|
// Signed, like groff's ".nr an-margin +(n;\$1)": ".RS -4" outdents
|
|
i64 m = c->margin + (i64)c->pindent;
|
|
if (nargs) {
|
|
Num v = evalexpr(args[0], 'n', 0);
|
|
m = c->margin + (v.ok ? v.value : 0);
|
|
}
|
|
c->margin = (i32)(m<0 ? 0 : m>MAXIND*U_PER_N ? MAXIND*U_PER_N : m);
|
|
c->pindent = IN;
|
|
ts->indent = hcols(c->margin);
|
|
ts->tindent = -1;
|
|
} break;
|
|
|
|
case M_RE: {
|
|
// Inset levels count from 1; level 1 has no saved state
|
|
tsbreak(ts);
|
|
i32 level = c->rsdepth; // target level, 0-based
|
|
if (nargs) {
|
|
Scaled v = scaled(args[0], 'u');
|
|
if (v.ok && v.value-1 < level) level = v.value - 1;
|
|
} else {
|
|
level--;
|
|
}
|
|
if (level>=0 && level<c->rsdepth) {
|
|
c->margin = c->rsmargin[level];
|
|
c->pindent = c->rspindent[level];
|
|
c->rsdepth = level;
|
|
}
|
|
ts->indent = hcols(c->margin);
|
|
ts->tindent = -1;
|
|
} break;
|
|
|
|
case M_PD:
|
|
if (nargs) {
|
|
Scaled v = scaled(args[0], 'v');
|
|
c->pd = v.ok ? vlines(v.value) : 1;
|
|
if (c->pd < 0) c->pd = 0;
|
|
} else {
|
|
c->pd = 1;
|
|
}
|
|
break;
|
|
|
|
case M_B: case M_I: case M_SM: case M_SB: case M_BI: case M_BR:
|
|
case M_IB: case M_IR: case M_RB: case M_RI:
|
|
manfont(c, kind, args, nargs, scratch);
|
|
break;
|
|
|
|
case M_UR:
|
|
case M_MT:
|
|
c->url = clone(&c->perm, nargs ? args[0] : S(""));
|
|
c->inurl = 1;
|
|
break;
|
|
|
|
case M_UE:
|
|
case M_ME: {
|
|
// Show the address in angle brackets after any link text
|
|
Str text = {0};
|
|
if (c->inurl) {
|
|
text = concat(&scratch, S("\\(la"), c->url);
|
|
text = concat(&scratch, text, S("\\(ra"));
|
|
}
|
|
if (nargs) {
|
|
text = concat(&scratch, text, args[0]);
|
|
}
|
|
c->inurl = 0;
|
|
if (text.len) {
|
|
textline(c, text, scratch);
|
|
}
|
|
} break;
|
|
|
|
case M_EX:
|
|
tsbreak(ts);
|
|
if (!c->example) {
|
|
c->example = 1;
|
|
c->expd = c->pd;
|
|
c->exfont = c->font;
|
|
c->pd = 1;
|
|
ts->fill = 0;
|
|
setroman(c);
|
|
}
|
|
break;
|
|
|
|
case M_EE:
|
|
tsbreak(ts);
|
|
if (c->example) {
|
|
c->example = 0;
|
|
c->pd = c->expd;
|
|
c->font = c->exfont;
|
|
ts->fill = 1;
|
|
}
|
|
break;
|
|
|
|
case M_SY: {
|
|
if (!c->sy) {
|
|
breakpara(c);
|
|
c->sy = 1;
|
|
c->syindent = ts->indent;
|
|
c->syadjust = ts->adjust;
|
|
ts->adjust = AD_L;
|
|
} else {
|
|
tsbreak(ts);
|
|
ts->nospace = 1;
|
|
}
|
|
Str cmd = nargs ? args[0] : S("");
|
|
i32 w = textwidth(c, cmd, scratch) + 1;
|
|
ts->indent = hcols(c->margin + w*U_PER_N);
|
|
ts->tindent = hcols(c->margin);
|
|
manfont(c, M_B, &cmd, 1, scratch);
|
|
} break;
|
|
|
|
case M_YS:
|
|
tsbreak(ts);
|
|
if (c->sy) {
|
|
ts->indent = c->syindent;
|
|
ts->adjust = c->syadjust;
|
|
c->sy = 0;
|
|
}
|
|
break;
|
|
|
|
case M_OP: {
|
|
Str text = S("\\&[\\fB");
|
|
if (nargs) text = concat(&scratch, text, args[0]);
|
|
text = concat(&scratch, text, S("\\fR"));
|
|
if (nargs > 1) {
|
|
text = concat(&scratch, text, S("\\fI\\~"));
|
|
text = concat(&scratch, text, args[1]);
|
|
text = concat(&scratch, text, S("\\fR"));
|
|
}
|
|
text = concat(&scratch, text, S("]"));
|
|
textline(c, text, scratch);
|
|
} break;
|
|
|
|
case M_MR: {
|
|
Str text = S("\\fI");
|
|
if (nargs) text = concat(&scratch, text, args[0]);
|
|
text = concat(&scratch, text, S("\\fR("));
|
|
if (nargs > 1) text = concat(&scratch, text, args[1]);
|
|
text = concat(&scratch, text, S(")"));
|
|
if (nargs > 2) text = concat(&scratch, text, args[2]);
|
|
textline(c, text, scratch);
|
|
} break;
|
|
|
|
case M_DT:
|
|
ts->ntabs = 0;
|
|
break;
|
|
|
|
case M_IGNORE:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Requests
|
|
|
|
typedef struct {
|
|
Strs args; // args.data[0] is the macro name
|
|
Flags quoted; // per argument
|
|
} MacroArgs;
|
|
|
|
// Split macro arguments, honoring quotes. With cells, each tab is an
|
|
// argument of its own: "\t", or "\t " when spaces follow it, and a word
|
|
// directly beside a tab is escaped as text.
|
|
static MacroArgs macroargs(Str name, Str s, b32 cells, Arena *a)
|
|
{
|
|
MacroArgs r = {0};
|
|
*push(a, &r.args) = name;
|
|
*push(a, &r.quoted) = 0;
|
|
b32 tab = 0; // directly after a tab
|
|
for (;;) {
|
|
iz skip = 0;
|
|
for (; skip<s.len && whitespace(s.data[skip]); skip++) {
|
|
if (cells && s.data[skip]=='\t') break;
|
|
}
|
|
s = cuthead(s, skip);
|
|
tab &= !skip;
|
|
if (!s.len) {
|
|
break;
|
|
}
|
|
Str arg = {0};
|
|
b32 isq = s.data[0] == '"';
|
|
if (s.data[0] == '\t') {
|
|
arg = takehead(s, s.len>1 && s.data[1]==' ' ? 2 : 1);
|
|
s = cuthead(s, 1);
|
|
} else if (isq) {
|
|
Buf b = {0};
|
|
iz i = 1;
|
|
for (; i < s.len; i++) {
|
|
if (s.data[i] == '"') {
|
|
if (i+1<s.len && s.data[i+1]=='"') {
|
|
bappendu8(&b, '"', a);
|
|
i++;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
bappendu8(&b, s.data[i], a);
|
|
if (s.data[i]=='\\' && i+1<s.len) {
|
|
bappendu8(&b, s.data[++i], a);
|
|
}
|
|
}
|
|
arg = bstr(b);
|
|
if (!arg.data) arg = S("");
|
|
s = cuthead(s, i<s.len ? i+1 : i);
|
|
} else {
|
|
iz i = 0;
|
|
for (; i<s.len && !whitespace(s.data[i]); i++) {
|
|
if (s.data[i]=='\\' && i+1<s.len) i++;
|
|
}
|
|
arg = takehead(s, i);
|
|
s = cuthead(s, i);
|
|
if (cells && arg.len>1 && (tab || (s.len && s.data[0]=='\t'))) {
|
|
arg = concat(a, S("\\&"), arg); // like mandoc, never a macro
|
|
}
|
|
}
|
|
tab = !isq && arg.data[0]=='\t';
|
|
*push(a, &r.args) = arg;
|
|
*push(a, &r.quoted) = isq;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
static Str nextword(Str *s)
|
|
{
|
|
Str t = trimleft(*s);
|
|
iz i = 0;
|
|
for (; i<t.len && !whitespace(t.data[i]); i++) {}
|
|
*s = cuthead(t, i);
|
|
return takehead(t, i);
|
|
}
|
|
|
|
static i32 countbraces(Str s)
|
|
{
|
|
i32 n = 0;
|
|
for (iz i = 0; i+1 < s.len; i++) {
|
|
if (s.data[i] == '\\') {
|
|
if (s.data[i+1] == '{') n++;
|
|
if (s.data[i+1] == '}') n--;
|
|
i++;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
typedef struct {
|
|
Str rest;
|
|
b32 value;
|
|
} Cond;
|
|
|
|
// Prepare a string comparison operand. As in groff on a terminal, a
|
|
// change to a font that does not exist (e.g. CB) vanishes, but a real
|
|
// font change makes the strings differ.
|
|
static Str plaintext(Str s, Arena *a)
|
|
{
|
|
Buf b = {0};
|
|
for (iz i = 0; i < s.len; i++) {
|
|
if (s.data[i]=='\\' && i+1<s.len) {
|
|
u8 e = s.data[i+1];
|
|
if (e == 'f') {
|
|
Arg n = escname(cuthead(s, i+2));
|
|
Str f = n.name;
|
|
b32 real = !f.len || equals(f, S("R")) || equals(f, S("I")) ||
|
|
equals(f, S("B")) || equals(f, S("BI")) ||
|
|
equals(f, S("P")) || (f.len==1 && digit(f.data[0]));
|
|
if (n.ok && !real) {
|
|
i = n.rest.data - s.data - 1;
|
|
continue;
|
|
}
|
|
} else if (e=='&' || e=='|' || e=='^') {
|
|
i++;
|
|
continue;
|
|
}
|
|
}
|
|
bappendu8(&b, s.data[i], a);
|
|
}
|
|
Str r = bstr(b);
|
|
return r.data ? r : S("");
|
|
}
|
|
|
|
static Cond evalcond(Ctx *c, Str s, Arena *a)
|
|
{
|
|
Cond r = {0};
|
|
s = trimleft(s);
|
|
b32 neg = 0;
|
|
for (; s.len && s.data[0]=='!'; s = cuthead(s, 1)) {
|
|
neg = !neg;
|
|
}
|
|
if (!s.len) {
|
|
r.rest = s;
|
|
return r;
|
|
}
|
|
|
|
u8 ch = s.data[0];
|
|
b32 term = s.len==1 || whitespace(s.data[1]) || s.data[1]=='\\';
|
|
if (term && (ch=='n' || ch=='o')) {
|
|
r.value = 1;
|
|
r.rest = cuthead(s, 1);
|
|
} else if (term && (ch=='t' || ch=='e' || ch=='v' || ch=='h')) {
|
|
r.value = 0;
|
|
r.rest = cuthead(s, 1);
|
|
} else if ((ch=='d' || ch=='r' || ch=='c' || ch=='m' || ch=='F' || ch=='S') &&
|
|
s.len>1 && whitespace(s.data[1])) {
|
|
Str rest = cuthead(s, 1);
|
|
Str name = nextword(&rest);
|
|
r.rest = rest;
|
|
if (ch == 'd') {
|
|
r.value = find(c->strings, name) || find(c->macros, name) ||
|
|
find(c->kinds, name);
|
|
} else if (ch == 'r') {
|
|
r.value = find(c->regs, name) || equals(name, S("an-margin")) ||
|
|
(name.len && name.data[0]=='.');
|
|
} else if (ch == 'c') {
|
|
r.value = 1;
|
|
} else {
|
|
r.value = 0;
|
|
}
|
|
} else if (digit(ch) || ch=='(' || ch=='+' || ch=='-' || ch=='|' || ch=='.') {
|
|
Num n = evalexpr(s, 'u', 0);
|
|
r.value = n.ok && n.value>0;
|
|
r.rest = n.ok ? n.rest : s;
|
|
// Skip any unparsed remainder of the expression
|
|
for (; r.rest.len && !whitespace(r.rest.data[0]); r.rest = cuthead(r.rest, 1)) {
|
|
if (r.rest.data[0] == '\\') break;
|
|
}
|
|
} else {
|
|
// String comparison: 'a'b'
|
|
u8 d = ch;
|
|
iz i = 1, j = -1, k = -1;
|
|
for (; i < s.len; i++) {
|
|
if (s.data[i]=='\\' && i+1<s.len) {
|
|
i++;
|
|
} else if (s.data[i] == d) {
|
|
if (j < 0) {
|
|
j = i;
|
|
} else {
|
|
k = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (k < 0) {
|
|
r.rest = (Str){s.data+s.len, 0};
|
|
r.value = 0;
|
|
} else {
|
|
Str x = plaintext(span(s.data+1, s.data+j), a);
|
|
Str y = plaintext(span(s.data+j+1, s.data+k), a);
|
|
r.value = equals(x, y);
|
|
r.rest = cuthead(s, k+1);
|
|
}
|
|
}
|
|
r.value ^= neg;
|
|
return r;
|
|
}
|
|
|
|
static void pushie(Ctx *c, b32 value)
|
|
{
|
|
c->iestack = c->iestack<<1 | (u64)!!value;
|
|
if (c->iedepth < 64) c->iedepth++;
|
|
}
|
|
|
|
static b32 popie(Ctx *c)
|
|
{
|
|
if (!c->iedepth) {
|
|
return 1;
|
|
}
|
|
b32 r = (b32)(c->iestack & 1);
|
|
c->iestack >>= 1;
|
|
c->iedepth--;
|
|
return r;
|
|
}
|
|
|
|
static void doif(Ctx *c, i32 kind, Str args, Arena scratch, i32 depth)
|
|
{
|
|
b32 t;
|
|
Str rest;
|
|
if (kind == R_EL) {
|
|
t = !popie(c);
|
|
rest = args;
|
|
} else {
|
|
Cond cd = evalcond(c, args, &scratch);
|
|
t = cd.value;
|
|
rest = cd.rest;
|
|
if (kind == R_IE) {
|
|
pushie(c, t);
|
|
}
|
|
}
|
|
rest = trimleft(rest);
|
|
if (!t) {
|
|
i32 d = countbraces(rest);
|
|
if (d > 0) {
|
|
c->skip = d;
|
|
}
|
|
return;
|
|
}
|
|
while (startswith(rest, S("\\{"))) {
|
|
rest = trimleft(cuthead(rest, 2));
|
|
}
|
|
if (rest.len && depth<MAXDEPTH) {
|
|
processline(c, rest, scratch, 1, depth+1);
|
|
}
|
|
}
|
|
|
|
static b32 isterminator(Str line, Str end)
|
|
{
|
|
if (!line.len || (line.data[0]!='.' && line.data[0]!='\'')) {
|
|
return 0;
|
|
}
|
|
Str t = trimleft(cuthead(line, 1)); // ". ." also ends a macro
|
|
if (!startswith(t, end)) {
|
|
return 0;
|
|
}
|
|
t = cuthead(t, end.len);
|
|
return !t.len || whitespace(t.data[0]) || t.data[0]=='\\';
|
|
}
|
|
|
|
static void invoke(Ctx *c, Str name, Str body, Str argstr, Arena scratch)
|
|
{
|
|
if (c->depth >= MAXDEPTH) {
|
|
warnf(c, S("macro nesting too deep"), name);
|
|
return;
|
|
}
|
|
// Arguments may view the input line, which does not outlive the
|
|
// call, so copy them to the high end of perm until the frame pops
|
|
byte *end = c->perm.end;
|
|
Strs raw = macroargs(name, argstr, 0, &scratch).args;
|
|
Str *args = allochigh(&c->perm, raw.len, sizeof(Str), _Alignof(Str));
|
|
args[0] = name;
|
|
for (iz i = 1; i < raw.len; i++) {
|
|
Str s = copymode(raw.data[i], &scratch);
|
|
args[i].data = allochigh(&c->perm, s.len, 1, 1);
|
|
args[i].len = s.len;
|
|
if (s.len) xcpy(args[i].data, s.data, touz(s.len));
|
|
}
|
|
Frame *f = newframe(c);
|
|
f->src = body;
|
|
f->args = args;
|
|
f->nargs = (i32)raw.len;
|
|
f->name = name;
|
|
f->macro = 1;
|
|
f->end = end;
|
|
f->next = c->frame;
|
|
c->frame = f;
|
|
c->depth++;
|
|
}
|
|
|
|
static void mdocline(Ctx *c, i32 kind, Str name, Str args, Arena scratch);
|
|
|
|
static void request(Ctx *c, i32 kind, Str args, b32 nobreak, Arena scratch, i32 depth)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
switch (kind) {
|
|
case R_IGNORE:
|
|
break;
|
|
|
|
case R_BR:
|
|
if (!nobreak) tsbreak(ts);
|
|
break;
|
|
|
|
case R_SP: {
|
|
Scaled v = scaled(args, 'v');
|
|
i32 n = v.ok ? vlines(v.value) : 1;
|
|
if (nobreak) {
|
|
if (n > 0 && !ts->nospace) {
|
|
ts->vspace = ts->vspace+n>MAXV ? MAXV : ts->vspace+n;
|
|
}
|
|
} else {
|
|
tsvspace(ts, n<0 ? 0 : n);
|
|
}
|
|
} break;
|
|
|
|
case R_NF:
|
|
if (!nobreak) tsbreak(ts);
|
|
ts->fill = 0;
|
|
break;
|
|
|
|
case R_FI:
|
|
if (!nobreak) tsbreak(ts);
|
|
ts->fill = 1;
|
|
break;
|
|
|
|
case R_AD: {
|
|
// Adjustment applies at the next output line
|
|
Str a = trim(args);
|
|
ts->adjust |= 1;
|
|
if (a.len) {
|
|
switch (a.data[0]) {
|
|
case 'l': ts->adjust = AD_L; break;
|
|
case 'r': ts->adjust = AD_R; break;
|
|
case 'c': ts->adjust = AD_C; break;
|
|
case 'b': case 'n': ts->adjust = AD_B; break;
|
|
default: {
|
|
Num n = evalexpr(a, 'u', 0);
|
|
if (n.ok && n.value>=0 && n.value<=AD_R) {
|
|
ts->adjust = (i32)n.value;
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case R_NA:
|
|
ts->adjust &= ~1;
|
|
break;
|
|
|
|
case R_CE: {
|
|
if (!nobreak) tsbreak(ts);
|
|
Scaled v = scaled(args, 'u');
|
|
ts->center = v.ok ? v.value : 1;
|
|
if (ts->center < 0) ts->center = 0;
|
|
} break;
|
|
|
|
case R_IN: {
|
|
if (!nobreak) tsbreak(ts);
|
|
Scaled v = scaled(args, 'm');
|
|
i32 previous = ts->indent;
|
|
if (!v.ok) {
|
|
ts->indent = ts->previndent;
|
|
} else {
|
|
ts->indent = hcols(v.rel ? ts->indent*U_PER_N + v.value : v.value);
|
|
}
|
|
ts->previndent = previous;
|
|
if (ts->indent < 0) ts->indent = 0;
|
|
if (ts->indent > 200) ts->indent = 200;
|
|
} break;
|
|
|
|
case R_TI: {
|
|
if (!nobreak) tsbreak(ts);
|
|
Scaled v = scaled(args, 'm');
|
|
if (v.ok) {
|
|
ts->tindent = hcols(v.rel ? ts->indent*U_PER_N + v.value : v.value);
|
|
if (ts->tindent < 0) ts->tindent = 0;
|
|
if (ts->tindent > 200) ts->tindent = 200;
|
|
}
|
|
} break;
|
|
|
|
case R_LL: {
|
|
Scaled v = scaled(args, 'm');
|
|
if (v.ok) {
|
|
i32 cols = hcols(v.value);
|
|
i32 ll = v.rel ? ts->ll + cols : cols;
|
|
if (ll>=10 && ll<=1000) ts->ll = ll;
|
|
}
|
|
} break;
|
|
|
|
case R_TA: {
|
|
ts->ntabs = 0;
|
|
i32 prev = 0;
|
|
Str rest = args;
|
|
while (ts->ntabs < countof(ts->tabs)) {
|
|
Str w = nextword(&rest);
|
|
if (!w.len) break;
|
|
if (w.data[w.len-1]=='L' || w.data[w.len-1]=='R' || w.data[w.len-1]=='C') {
|
|
w.len--;
|
|
}
|
|
if (equals(w, S("T"))) continue;
|
|
Scaled v = scaled(w, 'm');
|
|
if (!v.ok) continue;
|
|
i32 col = hcols(v.value);
|
|
col = v.rel>0 ? prev + col : col;
|
|
if (col <= prev && ts->ntabs) continue;
|
|
ts->tabs[ts->ntabs++] = col;
|
|
prev = col;
|
|
}
|
|
} break;
|
|
|
|
case R_FT: {
|
|
Str f = trim(args);
|
|
setfont(c, parsefont(c, nextword(&f)));
|
|
} break;
|
|
|
|
case R_NS:
|
|
ts->nospace = 1;
|
|
break;
|
|
|
|
case R_SS: {
|
|
// Only the additional sentence space matters on a terminal
|
|
Str rest = args;
|
|
nextword(&rest);
|
|
Str m = nextword(&rest);
|
|
Scaled v = scaled(m, 'u');
|
|
if (v.ok && v.value>=0 && !v.rel) ts->sentspace = v.value;
|
|
} break;
|
|
|
|
case R_RS:
|
|
ts->nospace = 0;
|
|
break;
|
|
|
|
case R_DS: {
|
|
Str rest = args;
|
|
Str key = nextword(&rest);
|
|
if (!key.len) break;
|
|
Str v = trimleft(rest);
|
|
if (v.len && v.data[0]=='"') v = cuthead(v, 1);
|
|
setstring(c, key, copymode(v, &scratch));
|
|
} break;
|
|
|
|
case R_AS: {
|
|
Str rest = args;
|
|
Str key = nextword(&rest);
|
|
if (!key.len) break;
|
|
Str v = trimleft(rest);
|
|
if (v.len && v.data[0]=='"') v = cuthead(v, 1);
|
|
Str old = getstring(c, key);
|
|
Str nv = concat(&scratch, old.data ? old : S(""), copymode(v, &scratch));
|
|
setstring(c, key, nv);
|
|
} break;
|
|
|
|
case R_DE:
|
|
case R_AM: {
|
|
Str rest = args;
|
|
Str key = nextword(&rest);
|
|
Str end = nextword(&rest);
|
|
if (!key.len) {
|
|
c->ignoring = 1;
|
|
c->igend = S(".");
|
|
break;
|
|
}
|
|
Entry *e = lookup(&c->macros, key, &c->perm);
|
|
c->defining = e;
|
|
c->defend = clone(&c->perm, end.len ? end : S("."));
|
|
c->defbody = (Buf){0};
|
|
if (kind==R_AM && e->live) {
|
|
bappend(&c->defbody, e->str, &c->perm);
|
|
}
|
|
} break;
|
|
|
|
case R_IG: {
|
|
Str rest = args;
|
|
Str end = nextword(&rest);
|
|
c->ignoring = 1;
|
|
c->igend = clone(&c->perm, end.len ? end : S("."));
|
|
} break;
|
|
|
|
case R_RM: {
|
|
Str rest = args;
|
|
for (Str w; (w = nextword(&rest)).len;) {
|
|
Entry *e = find(c->macros, w);
|
|
if (e) e->live = 0;
|
|
e = find(c->strings, w);
|
|
if (e) e->live = 0;
|
|
}
|
|
} break;
|
|
|
|
case R_RR: {
|
|
Str rest = args;
|
|
for (Str w; (w = nextword(&rest)).len;) {
|
|
Entry *e = find(c->regs, w);
|
|
if (e) e->live = 0;
|
|
}
|
|
} break;
|
|
|
|
case R_RN:
|
|
case R_ALS: {
|
|
Str rest = args;
|
|
Str a = nextword(&rest);
|
|
Str b = nextword(&rest);
|
|
Str from = kind==R_RN ? a : b;
|
|
Str to = kind==R_RN ? b : a;
|
|
Entry *src = find(c->macros, from);
|
|
Entry **map = &c->macros;
|
|
if (!src) {
|
|
src = find(c->strings, from);
|
|
map = &c->strings;
|
|
}
|
|
if (!src || !to.len) break;
|
|
Entry *dst = lookup(map, to, &c->perm);
|
|
dst->str = src->str;
|
|
dst->live = 1;
|
|
if (kind == R_RN) src->live = 0;
|
|
} break;
|
|
|
|
case R_NR: {
|
|
Str rest = args;
|
|
Str key = nextword(&rest);
|
|
Str val = nextword(&rest);
|
|
Str inc = nextword(&rest);
|
|
if (!key.len) break;
|
|
Scaled v = scaled(val, 'u');
|
|
if (!v.ok) break;
|
|
Entry *e = lookup(&c->regs, key, &c->perm);
|
|
if (!e->live) e->num = 0;
|
|
e->num = v.rel ? clampi32((i64)e->num + v.value) : v.value;
|
|
e->live = 1;
|
|
if (inc.len) {
|
|
Scaled iv = scaled(inc, 'u');
|
|
if (iv.ok) e->inc = iv.value;
|
|
}
|
|
} break;
|
|
|
|
case R_IF:
|
|
case R_IE:
|
|
case R_EL:
|
|
doif(c, kind, args, scratch, depth);
|
|
break;
|
|
|
|
case R_NOP:
|
|
if (depth < MAXDEPTH) {
|
|
processline(c, trimleft(args), scratch, 1, depth+1);
|
|
}
|
|
break;
|
|
|
|
case R_DO:
|
|
if (depth < MAXDEPTH) {
|
|
Str line = concat(&scratch, S("."), trimleft(args));
|
|
processline(c, line, scratch, 1, depth+1);
|
|
}
|
|
break;
|
|
|
|
case R_SHIFT: {
|
|
Frame *f = argframe(c);
|
|
Scaled v = scaled(args, 'u');
|
|
i32 n = v.ok ? v.value : 1;
|
|
if (f && n>0) {
|
|
if (n > f->nargs-1) n = f->nargs - 1;
|
|
for (i32 i = 1; i+n < f->nargs; i++) {
|
|
f->args[i] = f->args[i+n];
|
|
}
|
|
f->nargs -= n;
|
|
}
|
|
} break;
|
|
|
|
case R_SO: {
|
|
Str path = trim(args);
|
|
if (!path.len) break;
|
|
Frame *file = c->frame; // the file running this request
|
|
for (; file && file->macro; file = file->next) {}
|
|
i32 nest = file ? file->sonest+1 : 1;
|
|
if (nest > MAXDEPTH) {
|
|
warnf(c, S(".so nested too deeply"), path);
|
|
break;
|
|
}
|
|
Str src = plt_load(c->plt, path, &c->perm);
|
|
if (!src.data) {
|
|
warnf(c, S("cannot open"), path);
|
|
c->status = 1;
|
|
break;
|
|
}
|
|
pushfile(c, clone(&c->perm, path), src);
|
|
c->frame->sonest = nest;
|
|
} break;
|
|
|
|
case R_TR: {
|
|
Str s = trim(args);
|
|
while (s.len) {
|
|
Str from = {0}, to = {0};
|
|
for (i32 side = 0; side < 2; side++) {
|
|
Str g = {0};
|
|
if (!s.len) break;
|
|
if (s.data[0]=='\\' && s.len>1 && (s.data[1]=='(' || s.data[1]=='[')) {
|
|
Arg n = escname(cuthead(s, 1));
|
|
if (!n.ok) { s = (Str){0}; break; }
|
|
g = glyphtext(c, n.name, &c->perm);
|
|
s = n.rest;
|
|
} else if (s.data[0]=='\\' && s.len>1) {
|
|
g = takehead(cuthead(s, 1), 1);
|
|
s = cuthead(s, 2);
|
|
} else {
|
|
Rune r = decode(s);
|
|
g = takehead(s, r.len);
|
|
s = cuthead(s, r.len);
|
|
}
|
|
if (side) to = g; else from = g;
|
|
}
|
|
if (!from.len) break;
|
|
Entry *e = lookup(&c->trmap, from, &c->perm);
|
|
e->str = clone(&c->perm, to.len ? to : S(" "));
|
|
e->live = 1;
|
|
}
|
|
} break;
|
|
|
|
case R_TL: {
|
|
if (!nobreak) tsbreak(ts);
|
|
Str s = trim(args);
|
|
if (!s.len) break;
|
|
u8 d = s.data[0];
|
|
Str parts[3] = {0};
|
|
s = cuthead(s, 1);
|
|
for (i32 i = 0; i < 3; i++) {
|
|
Cut p = cut(s, d);
|
|
parts[i] = p.head;
|
|
s = p.tail;
|
|
}
|
|
b32 fill = ts->fill;
|
|
ts->fill = 0; // titles never wrap
|
|
Sink k = {0};
|
|
k.c = c;
|
|
i32 marks[3];
|
|
for (i32 i = 0; i < 3; i++) {
|
|
k.part = ts->ntoks;
|
|
tokenize(c, parts[i], &k, scratch);
|
|
marks[i] = ts->ntoks;
|
|
}
|
|
tstitle(ts, marks[0], marks[1]);
|
|
ts->fill = fill;
|
|
} break;
|
|
|
|
case R_TS:
|
|
tsbreak(ts);
|
|
c->tblactive = 1;
|
|
c->tblsrc = (Buf){0};
|
|
break;
|
|
|
|
case R_MSO:
|
|
// Only www.tmac matters to manual pages: its link macros
|
|
if (equals(trim(args), S("www.tmac"))) {
|
|
static Str body = STR(
|
|
".ie '\\$2'' \\&\\$1\\$3\n"
|
|
".el \\&\\$2 <\\$1>\\$3\n"
|
|
);
|
|
Str names[] = {S("URL"), S("MTO")};
|
|
for (i32 i = 0; i < countof(names); i++) {
|
|
Entry *e = lookup(&c->macros, names[i], &c->perm);
|
|
e->str = body;
|
|
e->live = 1;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case R_EQ:
|
|
c->ignoring = 1;
|
|
c->igend = S("EN");
|
|
break;
|
|
|
|
case R_PS:
|
|
c->ignoring = 1;
|
|
c->igend = S("PE");
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void tblline(Ctx *c, Str line, Arena scratch);
|
|
|
|
static void controlline(Ctx *c, Str line, Arena scratch, i32 depth)
|
|
{
|
|
b32 nobreak = line.data[0] == '\'';
|
|
Str s = cuthead(line, 1);
|
|
s = trimleft(s);
|
|
iz i = 0;
|
|
for (; i<s.len && !whitespace(s.data[i]) && s.data[i]!='\\'; i++) {}
|
|
Str name = takehead(s, i);
|
|
Str args = cuthead(s, i);
|
|
if (!name.len || name.data[0]=='"') {
|
|
// Possibly ".\}", or a malformed comment
|
|
return;
|
|
}
|
|
args = trimleft(args);
|
|
|
|
Entry *m = find(c->macros, name);
|
|
if (m) {
|
|
invoke(c, m->key, m->str, args, scratch);
|
|
return;
|
|
}
|
|
|
|
Entry *k = find(c->kinds, name);
|
|
i32 kind = k ? k->num : K_NONE;
|
|
if (kind>=M_TH && kind<=M_IGNORE) {
|
|
Strs argv = macroargs(name, args, 0, &scratch).args;
|
|
manmacro(c, kind, argv.data+1, (i32)argv.len-1, scratch);
|
|
return;
|
|
}
|
|
if (kind>=D_DD && kind<=D_IGNORE) {
|
|
if (!c->mdoc && (kind==D_DD || kind==D_DT || kind==D_OS) && !c->page) {
|
|
c->mdoc = 1;
|
|
}
|
|
if (c->mdoc) {
|
|
mdocline(c, kind, name, args, scratch);
|
|
return;
|
|
}
|
|
kind = K_NONE;
|
|
}
|
|
if (kind == K_NONE) {
|
|
warnf(c, S("unknown macro"), name);
|
|
return;
|
|
}
|
|
request(c, kind, args, nobreak, scratch, depth);
|
|
}
|
|
|
|
static void processline(Ctx *c, Str line, Arena scratch, b32 expanded, i32 depth)
|
|
{
|
|
if (c->ignoring) {
|
|
if (isterminator(line, c->igend) || (equals(c->igend, S(".")) && isterminator(line, S(".")))) {
|
|
c->ignoring = 0;
|
|
}
|
|
return;
|
|
}
|
|
if (c->defining) {
|
|
if (isterminator(line, c->defend)) {
|
|
Entry *e = c->defining;
|
|
e->str = bstr(c->defbody);
|
|
if (!e->str.data) e->str = S("");
|
|
e->live = 1;
|
|
c->defining = 0;
|
|
} else {
|
|
bappend(&c->defbody, copymode(line, &scratch), &c->perm);
|
|
bappendu8(&c->defbody, '\n', &c->perm);
|
|
}
|
|
return;
|
|
}
|
|
if (c->skip > 0) {
|
|
c->skip += countbraces(line);
|
|
if (c->skip < 0) c->skip = 0;
|
|
return;
|
|
}
|
|
if (c->tblactive) {
|
|
tblline(c, line, scratch);
|
|
return;
|
|
}
|
|
|
|
if (!expanded) {
|
|
line = expand(c, line, &scratch, 0);
|
|
}
|
|
if (line.len && (line.data[0]=='.' || line.data[0]=='\'')) {
|
|
controlline(c, line, scratch, depth);
|
|
return;
|
|
}
|
|
|
|
// A line holding only a block close is not text
|
|
Str t = trim(line);
|
|
while (startswith(t, S("\\}"))) {
|
|
t = trimleft(cuthead(t, 2));
|
|
}
|
|
if (!t.len && line.len) {
|
|
return;
|
|
}
|
|
textline(c, line, scratch);
|
|
}
|
|
|
|
|
|
// tbl: lines between .TS and .TE are collected, then laid out as a
|
|
// whole. Cells are formatted by the typesetter in capture mode, so
|
|
// escapes, fonts, and requests inside text blocks work as usual.
|
|
|
|
enum {
|
|
TBL_MAXCOLS = 64,
|
|
TBL_MAXFMTS = 256,
|
|
};
|
|
|
|
typedef struct {
|
|
u8 key; // l r c n a s ^ _ =
|
|
i8 font; // -1 to inherit
|
|
b32 top; // t: align multi-line rows to the top
|
|
b32 equal; // e: equal-width column
|
|
i32 minw; // w(): minimum width, columns, or -1
|
|
i32 sep; // separation after the column
|
|
i32 vline; // vertical lines after the column
|
|
b32 expand; // x: take the remaining line width
|
|
} TblSpec;
|
|
|
|
typedef struct {
|
|
TblSpec spec[TBL_MAXCOLS];
|
|
i32 n;
|
|
i32 vleft; // vertical lines before the first column
|
|
} TblFmt;
|
|
|
|
typedef struct {
|
|
Capline *lines;
|
|
i32 nlines;
|
|
i32 width;
|
|
i32 left; // width before the numeric alignment point
|
|
u8 rule; // '_' or '=' for a ruled cell
|
|
b32 vspan; // \^
|
|
b32 block; // T{ text block
|
|
Str text;
|
|
} TblCell;
|
|
|
|
typedef struct {
|
|
TblCell *cells;
|
|
i32 ncells;
|
|
i32 fmt;
|
|
u8 rule; // full-width rule row
|
|
b32 keyed; // the rule is a format row: a row of its own
|
|
i32 space; // blank lines requested before the row
|
|
} TblRow;
|
|
|
|
typedef struct {
|
|
TblFmt *fmts;
|
|
i32 nfmts;
|
|
TblRow *rows;
|
|
i32 nrows;
|
|
i32 ncols;
|
|
u8 tab;
|
|
b32 box;
|
|
b32 allbox;
|
|
b32 center;
|
|
b32 expand;
|
|
} Tbl;
|
|
|
|
static b32 tblkey(u8 ch)
|
|
{
|
|
switch (ch) {
|
|
case 'l': case 'r': case 'c': case 'n': case 'a': case 's': case '^':
|
|
case '_': case '-': case '=':
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static u8 lower(u8 ch)
|
|
{
|
|
return ch>='A' && ch<='Z' ? (u8)(ch + 32) : ch;
|
|
}
|
|
|
|
// Parse format rows from a line, appending to the table. Sets *done at
|
|
// the terminating period. Returns false on a syntax error.
|
|
static b32 tblparsefmt(Tbl *t, Str line, b32 *done, Arena *a)
|
|
{
|
|
TblFmt *f = 0;
|
|
for (iz i = 0; i < line.len; i++) {
|
|
u8 ch = line.data[i];
|
|
if (!f) {
|
|
if (t->nfmts == TBL_MAXFMTS) return 0;
|
|
if (!t->fmts) t->fmts = new(a, TBL_MAXFMTS, TblFmt);
|
|
f = t->fmts + t->nfmts++;
|
|
for (i32 j = 0; j < TBL_MAXCOLS; j++) {
|
|
f->spec[j].font = -1;
|
|
f->spec[j].minw = -1;
|
|
f->spec[j].sep = 3;
|
|
}
|
|
}
|
|
if (whitespace(ch)) {
|
|
continue;
|
|
} else if (ch == ',') {
|
|
f = 0;
|
|
} else if (ch == '.') {
|
|
*done = 1;
|
|
return f->n > 0;
|
|
} else if (ch == '|') {
|
|
if (f->n) f->spec[f->n-1].vline++; else f->vleft++;
|
|
} else if (tblkey(lower(ch))) {
|
|
if (f->n == TBL_MAXCOLS) return 0;
|
|
u8 k = lower(ch);
|
|
f->spec[f->n++].key = k=='-' ? '_' : k;
|
|
} else if (!f->n) {
|
|
return 0;
|
|
} else {
|
|
// Modifiers of the most recent key letter
|
|
TblSpec *s = f->spec + f->n - 1;
|
|
switch (lower(ch)) {
|
|
case 'b': s->font = (i8)(s->font<0 ? F_B : s->font|F_B); break;
|
|
case 'i': s->font = (i8)(s->font<0 ? F_I : s->font|F_I); break;
|
|
case 't': s->top = 1; break;
|
|
// x excludes e and w, and the later modifier wins
|
|
case 'e': s->equal = 1; s->expand = 0; break;
|
|
case 'x': s->expand = 1; s->equal = 0; s->minw = -1; break;
|
|
case 'u': case 'z': case 'd': break;
|
|
case 'f': {
|
|
iz j = i + 1;
|
|
Str name = {0};
|
|
if (j<line.len && line.data[j]=='(') {
|
|
name = span(line.data+j+1, line.data+(j+3<=line.len ? j+3 : line.len));
|
|
j += 3;
|
|
} else if (j<line.len && line.data[j]=='[') {
|
|
iz k = j;
|
|
for (; k<line.len && line.data[k]!=']'; k++) {}
|
|
name = span(line.data+j+1, line.data+k);
|
|
j = k + 1;
|
|
} else {
|
|
iz k = j;
|
|
for (; k<line.len && k<j+2 && ((line.data[k]>='A' && line.data[k]<='Z') || digit(line.data[k])); k++) {}
|
|
name = span(line.data+j, line.data+k);
|
|
j = k;
|
|
}
|
|
b32 bold = 0, ital = 0;
|
|
for (iz k = 0; k < name.len; k++) {
|
|
bold |= name.data[k]=='B' || name.data[k]=='3';
|
|
ital |= name.data[k]=='I' || name.data[k]=='2';
|
|
}
|
|
s->font = (i8)((bold ? F_B : 0) | (ital ? F_I : 0));
|
|
i = j - 1;
|
|
} break;
|
|
case 'w': {
|
|
iz j = i + 1;
|
|
Str w = {0};
|
|
if (j<line.len && line.data[j]=='(') {
|
|
iz k = j;
|
|
for (; k<line.len && line.data[k]!=')'; k++) {}
|
|
w = span(line.data+j+1, line.data+k);
|
|
j = k + 1;
|
|
} else {
|
|
iz k = j;
|
|
for (; k<line.len && (digit(line.data[k]) || line.data[k]=='.'); k++) {}
|
|
w = span(line.data+j, line.data+k);
|
|
j = k;
|
|
}
|
|
Scaled v = scaled(w, 'n');
|
|
i32 cols = hcols(v.value);
|
|
if (v.ok) s->minw = cols<0 ? 0 : cols>MAXW ? MAXW : cols;
|
|
s->expand = 0;
|
|
i = j - 1;
|
|
} break;
|
|
case 'p': case 'v': {
|
|
iz j = i + 1;
|
|
if (j<line.len && (line.data[j]=='+' || line.data[j]=='-')) j++;
|
|
for (; j<line.len && digit(line.data[j]); j++) {}
|
|
i = j - 1;
|
|
} break;
|
|
default:
|
|
if (digit(ch)) {
|
|
i32 n = 0;
|
|
iz j = i;
|
|
for (; j<line.len && digit(line.data[j]) && n<100; j++) {
|
|
n = n*10 + line.data[j] - '0';
|
|
}
|
|
s->sep = n;
|
|
i = j - 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void tbloptions(Tbl *t, Str line)
|
|
{
|
|
for (iz i = 0; i < line.len; i++) {
|
|
Str rest = cuthead(line, i);
|
|
if (whitespace(rest.data[0]) || rest.data[0]==',' || rest.data[0]==';') {
|
|
continue;
|
|
}
|
|
iz n = 0;
|
|
for (; n<rest.len && ((rest.data[n]>='a' && rest.data[n]<='z') || (rest.data[n]>='A' && rest.data[n]<='Z')); n++) {}
|
|
Str word = takehead(rest, n);
|
|
iz end = n;
|
|
if (n<rest.len && rest.data[n]=='(') {
|
|
iz k = n;
|
|
for (; k<rest.len && rest.data[k]!=')'; k++) {}
|
|
Str arg = span(rest.data+n+1, rest.data+k);
|
|
if (equals(word, S("tab")) && arg.len) {
|
|
t->tab = arg.data[0];
|
|
}
|
|
end = k + 1;
|
|
}
|
|
for (iz k = 0; k < word.len; k++) {
|
|
word.data[k] = lower(word.data[k]); // safe: our own copy
|
|
}
|
|
if (equals(word, S("box")) || equals(word, S("frame")) ||
|
|
equals(word, S("doublebox")) || equals(word, S("doubleframe"))) {
|
|
t->box = 1;
|
|
} else if (equals(word, S("allbox"))) {
|
|
t->box = t->allbox = 1;
|
|
} else if (equals(word, S("center")) || equals(word, S("centre"))) {
|
|
t->center = 1;
|
|
} else if (equals(word, S("expand"))) {
|
|
t->expand = 1;
|
|
}
|
|
i += end>0 ? end-1 : 0;
|
|
}
|
|
}
|
|
|
|
// Typesetter and context state saved around a table cell
|
|
typedef struct {
|
|
i32 indent;
|
|
i32 previndent;
|
|
i32 tindent;
|
|
b32 fill;
|
|
i32 adjust;
|
|
i32 center;
|
|
i32 ll;
|
|
i32 vspace;
|
|
b32 nospace;
|
|
i32 font;
|
|
i32 prevfont;
|
|
i32 trap;
|
|
b32 cont;
|
|
} TsSave;
|
|
|
|
// Begin capturing typesetter output for a table cell into the arena.
|
|
static TsSave tsbegin(Ctx *c, Arena *a)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
TsSave s = {0};
|
|
s.indent = ts->indent;
|
|
s.previndent = ts->previndent;
|
|
s.tindent = ts->tindent;
|
|
s.fill = ts->fill;
|
|
s.adjust = ts->adjust;
|
|
s.center = ts->center;
|
|
s.ll = ts->ll;
|
|
s.vspace = ts->vspace;
|
|
s.nospace = ts->nospace;
|
|
s.font = c->font;
|
|
s.prevfont = c->prevfont;
|
|
s.trap = c->trap;
|
|
s.cont = c->cont;
|
|
|
|
ts->capture = 1;
|
|
ts->caparena = a;
|
|
ts->caps = (Caplines){0};
|
|
ts->indent = 0;
|
|
ts->tindent = -1;
|
|
ts->vspace = 0;
|
|
ts->nospace = 0;
|
|
ts->center = 0;
|
|
c->trap = 0;
|
|
c->cont = 0;
|
|
return s;
|
|
}
|
|
|
|
// End a capture started by tsbegin, restoring the saved state.
|
|
static void tsend(Ctx *c, TsSave s)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
tsbreak(ts);
|
|
ts->capture = 0;
|
|
ts->indent = s.indent;
|
|
ts->previndent = s.previndent;
|
|
ts->tindent = s.tindent;
|
|
ts->fill = s.fill;
|
|
ts->adjust = s.adjust;
|
|
ts->center = s.center;
|
|
ts->ll = s.ll;
|
|
ts->vspace = s.vspace;
|
|
ts->nospace = s.nospace;
|
|
c->font = s.font;
|
|
c->prevfont = s.prevfont;
|
|
c->trap = s.trap;
|
|
c->cont = s.cont;
|
|
}
|
|
|
|
static i32 caplinewidth(Capline *l)
|
|
{
|
|
i32 n = l->len;
|
|
for (; n>0 && !l->cells[n-1].data; n--) {}
|
|
return n;
|
|
}
|
|
|
|
// Format one cell, either a single line or a filled text block.
|
|
static void tblrender(Ctx *c, TblCell *cell, TblSpec *spec, i32 width, Arena *perm, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
TsSave save = tsbegin(c, perm);
|
|
if (spec->font >= 0) {
|
|
setfont(c, spec->font);
|
|
}
|
|
if (cell->block) {
|
|
ts->fill = 1;
|
|
ts->ll = width>0 ? width : 1;
|
|
Str src = cell->text;
|
|
while (src.len) {
|
|
Cut l = cut(src, '\n');
|
|
src = l.tail;
|
|
processline(c, l.head, scratch, 0, 1);
|
|
}
|
|
} else {
|
|
ts->fill = 0;
|
|
ts->ll = 10000;
|
|
Str text = expand(c, cell->text, &scratch, 0);
|
|
Sink k = {0};
|
|
k.c = c;
|
|
tokenize(c, text, &k, scratch);
|
|
}
|
|
tsend(c, save);
|
|
cell->lines = ts->caps.data;
|
|
cell->nlines = (i32)ts->caps.len;
|
|
for (; cell->nlines>0 && !caplinewidth(cell->lines+cell->nlines-1); cell->nlines--) {}
|
|
cell->width = 0;
|
|
for (i32 i = 0; i < cell->nlines; i++) {
|
|
i32 w = caplinewidth(cell->lines + i);
|
|
cell->width = w>cell->width ? w : cell->width;
|
|
}
|
|
|
|
// Numeric alignment point: after the last digit, or at a decimal
|
|
// point adjacent to a digit. Items without digits are centered (-1).
|
|
cell->left = -1;
|
|
if (spec->key=='n' && cell->nlines==1) {
|
|
Capline *l = cell->lines;
|
|
i32 point = -1;
|
|
for (i32 i = 0; i < l->len; i++) {
|
|
Cell *x = l->cells + i;
|
|
if (!x->data || x->len!=1) continue;
|
|
if (digit(x->data[0])) {
|
|
point = i + 1;
|
|
}
|
|
}
|
|
for (i32 i = 0; i < l->len; i++) {
|
|
Cell *x = l->cells + i;
|
|
if (x->data && x->len==1 && x->data[0]=='.') {
|
|
b32 before = i>0 && l->cells[i-1].data && digit(l->cells[i-1].data[0]);
|
|
b32 after = i+1<l->len && l->cells[i+1].data && digit(l->cells[i+1].data[0]);
|
|
if (before || after) point = i;
|
|
}
|
|
}
|
|
if (point >= 0) cell->left = point;
|
|
}
|
|
}
|
|
|
|
static TblSpec *tblspec(Tbl *t, TblRow *row, i32 j)
|
|
{
|
|
static TblSpec dflt = {'l', -1, 0, 0, -1, 3, 0, 0};
|
|
if (!t->nfmts) return &dflt;
|
|
TblFmt *f = t->fmts + row->fmt;
|
|
return j<f->n ? f->spec+j : &dflt;
|
|
}
|
|
|
|
// Split one data line into cells, reading text blocks from the source.
|
|
// Like tbl, entries skip the columns of an s format. The row keeps only
|
|
// the cells it has, so that huge tables fit.
|
|
static void tblrow(Tbl *t, Str *src, Str line, Arena *a)
|
|
{
|
|
TblRow *row = t->rows + t->nrows++;
|
|
TblCell cells[TBL_MAXCOLS] = {0};
|
|
for (;;) {
|
|
for (; row->ncells<TBL_MAXCOLS && tblspec(t, row, row->ncells)->key=='s'; row->ncells++) {}
|
|
if (row->ncells == TBL_MAXCOLS) break;
|
|
TblCell *cell = cells + row->ncells++;
|
|
Cut f = cut(line, t->tab);
|
|
Str text = f.head;
|
|
if (startswith(text, S("T{")) && !trim(cuthead(text, 2)).len) {
|
|
// Text block: lines until one starting with T}
|
|
Buf b = {0};
|
|
Str rest = {0};
|
|
while (src->len) {
|
|
Cut l = cut(*src, '\n');
|
|
*src = l.tail;
|
|
Str bl = stripcr(l.head);
|
|
if (startswith(bl, S("T}"))) {
|
|
rest = cuthead(bl, 2);
|
|
break;
|
|
}
|
|
bappend(&b, bl, a);
|
|
bappendu8(&b, '\n', a);
|
|
}
|
|
cell->block = 1;
|
|
cell->text = bstr(b);
|
|
if (rest.len && rest.data[0]==t->tab) {
|
|
line = cuthead(rest, 1);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
Str tt = trim(text);
|
|
if (equals(tt, S("_")) || equals(tt, S("="))) {
|
|
cell->rule = tt.data[0];
|
|
} else if (equals(tt, S("\\_"))) {
|
|
cell->rule = '_';
|
|
} else if (equals(tt, S("\\^"))) {
|
|
cell->vspan = 1;
|
|
} else if (startswith(tt, S("\\R")) && tt.len>2) {
|
|
cell->rule = tt.data[2];
|
|
} else {
|
|
cell->text = text;
|
|
}
|
|
if (!f.ok) break;
|
|
line = f.tail;
|
|
}
|
|
row->cells = new(a, row->ncells, TblCell);
|
|
xcpy(row->cells, cells, touz(row->ncells)*sizeof(TblCell));
|
|
}
|
|
|
|
// The rule drawn by a format row of only _ and = keys over every column,
|
|
// or zero if it takes data
|
|
static u8 tblfmtrule(Tbl *t, i32 i)
|
|
{
|
|
TblFmt *f = t->fmts + i;
|
|
for (i32 j = 0; j < f->n; j++) {
|
|
if (f->spec[j].key!='_' && f->spec[j].key!='=') return 0;
|
|
}
|
|
for (i32 k = 0; k < t->nfmts; k++) {
|
|
if (t->fmts[k].n > f->n) return 0;
|
|
}
|
|
return f->spec[0].key;
|
|
}
|
|
|
|
// Columns spanned by the cell in column j, counting the s after it
|
|
static i32 tblspan(Tbl *t, TblRow *row, i32 j)
|
|
{
|
|
i32 span = 1;
|
|
for (; j+span<t->ncols && tblspec(t, row, j+span)->key=='s'; span++) {}
|
|
return span;
|
|
}
|
|
|
|
// Width of columns [j, j+span) and the separation between them
|
|
static i32 tblspanw(i32 *colw, i32 *sep, i32 j, i32 span)
|
|
{
|
|
i32 w = 0;
|
|
for (i32 k = j; k < j+span; k++) {
|
|
w += colw[k] + (k<j+span-1 ? sep[k]*U_PER_N : 0);
|
|
}
|
|
return w;
|
|
}
|
|
|
|
static void tblequal(i32 *colw, b32 *ecol, i32 n)
|
|
{
|
|
i32 w = 0;
|
|
for (i32 j = 0; j < n; j++) {
|
|
w = ecol[j] && colw[j]>w ? colw[j] : w;
|
|
}
|
|
for (i32 j = 0; j < n; j++) {
|
|
colw[j] = ecol[j] ? w : colw[j];
|
|
}
|
|
}
|
|
|
|
// Widen columns evenly for the widest cell of each span, ordered by last
|
|
// column then by first column descending, like tbl. A span over an
|
|
// equal-width or expanding column widens every column, like tbl.
|
|
static void tbldivide(Tbl *t, i32 *colw, i32 *sep, b32 *ecol, b32 *xcol, Arena scratch)
|
|
{
|
|
i32 n = t->ncols;
|
|
i32 *spanw = new(&scratch, n*n, i32);
|
|
for (i32 r = 0; r < t->nrows; r++) {
|
|
TblRow *row = t->rows + r;
|
|
for (i32 j = 0; j < row->ncells && j < n; j++) {
|
|
i32 key = tblspec(t, row, j)->key;
|
|
i32 span = tblspan(t, row, j);
|
|
if (span==1 || key=='s' || key=='^') continue;
|
|
i32 *w = spanw + j*n + j+span-1;
|
|
i32 cw = row->cells[j].width * U_PER_N;
|
|
*w = cw>*w ? cw : *w;
|
|
}
|
|
}
|
|
for (i32 e = 1; e < n; e++) {
|
|
for (i32 s = e-1; s >= 0; s--) {
|
|
i32 span = e - s + 1;
|
|
i32 need = (spanw[s*n+e] - tblspanw(colw, sep, s, span))/span;
|
|
b32 eq = 0;
|
|
for (i32 k = s; k <= e; k++) {
|
|
eq |= ecol[k] || xcol[k];
|
|
}
|
|
for (i32 k = 0; need>0 && k<n; k++) {
|
|
colw[k] += eq || (k>=s && k<=e) ? need : 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void tblplace(Typeset *ts, i32 x, Str s)
|
|
{
|
|
if (x>=0 && x<ts->rowcap) {
|
|
ts->row[x].data = s.data;
|
|
ts->row[x].len = (i32)s.len;
|
|
ts->row[x].font = 0;
|
|
}
|
|
}
|
|
|
|
// Draw a horizontal rule over [x0, xend) into a cleared row, with joints
|
|
// at the vertical lines. It leaves out the columns a vertical span holds
|
|
// open, and above and below mark the lines that spanning cells of the
|
|
// adjacent rows cut, null at the top or bottom of the table.
|
|
static void tblrule(Typeset *ts, Str ch, i32 x0, i32 xend, i32 *vline, i32 *vx, i32 n, b32 *open, b32 *above, b32 *below)
|
|
{
|
|
// By the directions joined: up 8, down 4, left 2, right 1
|
|
static Str joints[16] = {
|
|
STR(""), STR("\xe2\x94\x80"), STR("\xe2\x94\x80"), STR("\xe2\x94\x80"),
|
|
STR("\xe2\x94\x82"), STR("\xe2\x94\x8c"), STR("\xe2\x94\x90"), STR("\xe2\x94\xac"),
|
|
STR("\xe2\x94\x82"), STR("\xe2\x94\x94"), STR("\xe2\x94\x98"), STR("\xe2\x94\xb4"),
|
|
STR("\xe2\x94\x82"), STR("\xe2\x94\x9c"), STR("\xe2\x94\xa4"), STR("\xe2\x94\xbc"),
|
|
};
|
|
clearrow(ts, xend+1);
|
|
for (i32 x = x0; x < xend; x++) {
|
|
tblplace(ts, x, ch);
|
|
}
|
|
for (i32 j = 0; open && j < n; j++) {
|
|
if (open[j]) {
|
|
i32 lo = vx[j] + (j || vline[0]) - (j && open[j-1] && !vline[j]);
|
|
i32 hi = j<n-1 || vline[n] ? vx[j+1] : xend;
|
|
for (i32 x = lo; x < hi; x++) {
|
|
tblplace(ts, x, (Str){0});
|
|
}
|
|
}
|
|
}
|
|
for (i32 j = 0; j <= n; j++) {
|
|
if (vline[j]) {
|
|
i32 d = (above && !above[j])<<3 | (below && !below[j])<<2 |
|
|
(j>0 && !(open && open[j-1]))<<1 | (j<n && !(open && open[j]));
|
|
Str s = joints[d];
|
|
if (!ts->utf8) {
|
|
s = d&3 && d&12 ? S("+") : d&3 ? S("-") : S("|");
|
|
}
|
|
tblplace(ts, vx[j], d ? s : (Str){0});
|
|
}
|
|
}
|
|
}
|
|
|
|
static void tbllayout(Ctx *c, Str src, Arena scratch)
|
|
{
|
|
// The table lives in scratch, and tblrender gets the scratch past it
|
|
// by value. Only the lines of rendered cells are kept in perm.
|
|
Typeset *ts = c->ts;
|
|
Arena *a = &scratch;
|
|
Arena *perm = &c->perm;
|
|
Tbl t = {0};
|
|
t.tab = '\t';
|
|
|
|
// Options and formats
|
|
Str s = src;
|
|
Cut first = cut(s, '\n');
|
|
Str fl = trim(stripcr(first.head));
|
|
if (fl.len && fl.data[fl.len-1]==';') {
|
|
tbloptions(&t, clone(a, fl));
|
|
s = first.tail;
|
|
}
|
|
Str fmtstart = s;
|
|
b32 done = 0, ok = 1;
|
|
while (s.len && !done && ok) {
|
|
Cut l = cut(s, '\n');
|
|
s = l.tail;
|
|
ok = tblparsefmt(&t, stripcr(l.head), &done, a);
|
|
}
|
|
if (!ok || !done) {
|
|
// Malformed format: treat everything as left-aligned data
|
|
t.nfmts = 0;
|
|
s = fmtstart;
|
|
}
|
|
i32 nfmt0 = t.nfmts; // .T& formats cannot change x or e columns
|
|
|
|
// Data rows, with .T& format changes. Before a data line, format rows
|
|
// of rules alone, short of the last, are rules taking no data line.
|
|
i32 maxrows = 1;
|
|
for (iz i = 0; i < s.len; i++) maxrows += s.data[i]=='\n';
|
|
t.rows = new(a, maxrows+TBL_MAXFMTS, TblRow);
|
|
i32 section = 0, sectionrow = 0, pending = 0;
|
|
while (s.len) {
|
|
Cut l = cut(s, '\n');
|
|
s = l.tail;
|
|
Str line = stripcr(l.head);
|
|
if (startswith(line, S(".T&"))) {
|
|
section = t.nfmts;
|
|
sectionrow = 0;
|
|
b32 fdone = 0, fok = 1;
|
|
while (s.len && !fdone && fok) {
|
|
Cut fl2 = cut(s, '\n');
|
|
s = fl2.tail;
|
|
fok = tblparsefmt(&t, stripcr(fl2.head), &fdone, a);
|
|
}
|
|
continue;
|
|
}
|
|
if (line.len && (line.data[0]=='.' || line.data[0]=='\'')) {
|
|
Str req = trim(cuthead(line, 1));
|
|
if (startswith(req, S("sp"))) {
|
|
Scaled v = scaled(cuthead(req, 2), 'v');
|
|
pending += v.ok ? vlines(v.value) : 1;
|
|
pending = pending<0 ? 0 : pending>MAXV ? MAXV : pending;
|
|
}
|
|
continue;
|
|
}
|
|
// Rule-only format rows come before the next real data line
|
|
Str tl = trim(line);
|
|
b32 datarule = equals(tl, S("_")) || equals(tl, S("="));
|
|
for (; !datarule && section+sectionrow<t.nfmts-1; sectionrow++) {
|
|
u8 rule = tblfmtrule(&t, section+sectionrow);
|
|
if (!rule) break;
|
|
TblRow *row = t.rows + t.nrows++;
|
|
row->fmt = section + sectionrow;
|
|
row->rule = rule;
|
|
row->keyed = 1;
|
|
row->space = pending;
|
|
pending = 0;
|
|
}
|
|
TblRow *row = t.rows + t.nrows;
|
|
if (t.nfmts) {
|
|
i32 last = t.nfmts - 1;
|
|
row->fmt = section + sectionrow;
|
|
row->fmt = row->fmt>last ? last : row->fmt;
|
|
}
|
|
if (datarule) {
|
|
t.nrows++;
|
|
row->rule = tl.data[0];
|
|
} else {
|
|
tblrow(&t, &s, line, a);
|
|
}
|
|
row->space = pending;
|
|
pending = 0;
|
|
if (!row->rule) sectionrow++;
|
|
}
|
|
|
|
// Column count
|
|
for (i32 i = 0; i < t.nfmts; i++) {
|
|
t.ncols = t.fmts[i].n>t.ncols ? t.fmts[i].n : t.ncols;
|
|
}
|
|
for (i32 r = 0; r < t.nrows; r++) {
|
|
if (!t.nfmts) t.ncols = t.rows[r].ncells>t.ncols ? t.rows[r].ncells : t.ncols;
|
|
}
|
|
i32 n = t.ncols;
|
|
if (!n) return;
|
|
|
|
// Render cells and measure columns. Widths are in basic units, like
|
|
// tbl's registers, so that shares of spans and of the expansion are
|
|
// rounded to character positions like groff.
|
|
i32 *colw = new(a, n, i32);
|
|
i32 *left = new(a, n, i32);
|
|
i32 *right = new(a, n, i32);
|
|
i32 *sep = new(a, n, i32);
|
|
i32 *vline = new(a, n+1, i32);
|
|
i32 *minw = new(a, n, i32); // the last w() of each column, or -1
|
|
for (i32 j = 0; j < n; j++) {
|
|
minw[j] = -1;
|
|
}
|
|
for (i32 f = 0; f < t.nfmts; f++) {
|
|
for (i32 j = 0; j < t.fmts[f].n && j < n; j++) {
|
|
i32 w = t.fmts[f].spec[j].minw;
|
|
minw[j] = w>=0 ? w*U_PER_N : minw[j];
|
|
}
|
|
}
|
|
for (i32 j = 0; j < n; j++) {
|
|
sep[j] = 3;
|
|
colw[j] = minw[j]>=0 ? minw[j] : U_PER_N; // 1n unless w() given
|
|
}
|
|
// Expanding and equal-width columns: x or e in any initial format row
|
|
b32 *xcol = new(a, n, b32);
|
|
b32 *ecol = new(a, n, b32);
|
|
i32 nx = 0;
|
|
for (i32 f = 0; f < nfmt0; f++) {
|
|
for (i32 j = 0; j < t.fmts[f].n && j < n; j++) {
|
|
nx += t.fmts[f].spec[j].expand && !xcol[j];
|
|
xcol[j] |= t.fmts[f].spec[j].expand;
|
|
ecol[j] |= t.fmts[f].spec[j].equal;
|
|
}
|
|
}
|
|
|
|
// Ordinary entries first: they set the initial column widths
|
|
for (i32 r = 0; r < t.nrows; r++) {
|
|
TblRow *row = t.rows + r;
|
|
if (row->rule) continue;
|
|
for (i32 j = 0; j < n; j++) {
|
|
TblSpec *sp = tblspec(&t, row, j);
|
|
if (j < n-1) sep[j] = sp->sep;
|
|
if (j >= row->ncells) continue;
|
|
TblCell *cell = row->cells + j;
|
|
if (cell->rule || cell->vspan || cell->block || sp->key=='s' || sp->key=='^') {
|
|
continue;
|
|
}
|
|
tblrender(c, cell, sp, 0, perm, scratch);
|
|
if (tblspan(&t, row, j) > 1) {
|
|
continue; // spanning: handled below
|
|
}
|
|
if (sp->key=='n' && cell->nlines==1 && cell->left>=0) {
|
|
left[j] = cell->left>left[j] ? cell->left : left[j];
|
|
i32 rw = cell->width - cell->left;
|
|
right[j] = rw>right[j] ? rw : right[j];
|
|
} else {
|
|
i32 w = cell->width * U_PER_N;
|
|
colw[j] = w>colw[j] ? w : colw[j];
|
|
}
|
|
}
|
|
}
|
|
for (i32 j = 0; j < n; j++) {
|
|
i32 w = (left[j] + right[j]) * U_PER_N;
|
|
colw[j] = w>colw[j] ? w : colw[j];
|
|
}
|
|
tblequal(colw, ecol, n);
|
|
tbldivide(&t, colw, sep, ecol, xcol, scratch);
|
|
|
|
// Text blocks, in order: each is filled to the larger of its width so
|
|
// far and L*C/(N+1), or the w() widths if all its columns have one,
|
|
// then may widen its column, like tbl. Blocks over expanding columns
|
|
// wait for the expanded width.
|
|
for (i32 r = 0; r < t.nrows; r++) {
|
|
TblRow *row = t.rows + r;
|
|
for (i32 j = 0; j < row->ncells && j < n; j++) {
|
|
TblCell *cell = row->cells + j;
|
|
TblSpec *sp = tblspec(&t, row, j);
|
|
if (!cell->block || sp->key=='s' || sp->key=='^') continue;
|
|
i32 span = tblspan(&t, row, j);
|
|
b32 x = 0;
|
|
b32 allw = 1;
|
|
for (i32 k = j; k < j+span; k++) {
|
|
x |= xcol[k];
|
|
allw &= minw[k] >= 0;
|
|
}
|
|
if (x) continue;
|
|
i32 cw = tblspanw(colw, sep, j, span);
|
|
i32 bw = ts->ll*U_PER_N*span/(n + 1);
|
|
bw = allw ? tblspanw(minw, sep, j, span) : bw;
|
|
tblrender(c, cell, sp, hcols(cw>bw ? cw : bw), perm, scratch);
|
|
i32 w = cell->width * U_PER_N;
|
|
colw[j] = span==1 && w>colw[j] ? w : colw[j];
|
|
}
|
|
}
|
|
tblequal(colw, ecol, n);
|
|
tbldivide(&t, colw, sep, ecol, xcol, scratch);
|
|
|
|
// Vertical lines
|
|
for (i32 f = 0; f < t.nfmts; f++) {
|
|
vline[0] = t.fmts[f].vleft>vline[0] ? t.fmts[f].vleft : vline[0];
|
|
for (i32 j = 0; j < t.fmts[f].n && j < n; j++) {
|
|
if (t.fmts[f].spec[j].vline) vline[j+1] = 1;
|
|
}
|
|
}
|
|
if (t.allbox) {
|
|
for (i32 j = 0; j <= n; j++) vline[j] = 1;
|
|
}
|
|
if (t.box) {
|
|
vline[0] = vline[n] = 1;
|
|
}
|
|
i32 frame = vline[0] ? 2 : 0;
|
|
i32 rframe = vline[n] ? 2 : 0;
|
|
|
|
// Expanding columns share what the others leave of the line, if that
|
|
// exceeds their natural widths. Then their text blocks are filled to
|
|
// that width, or spanning ones as above on a line less the frame.
|
|
if (nx) {
|
|
i32 rest = (ts->ll - ts->indent - frame - rframe) * U_PER_N;
|
|
i32 nat = 0;
|
|
for (i32 j = 0; j < n; j++) {
|
|
rest -= (xcol[j] ? 0 : colw[j]) + (j<n-1 ? sep[j]*U_PER_N : 0);
|
|
nat += xcol[j] ? colw[j] : 0;
|
|
}
|
|
i32 xw = rest>nat ? rest/nx : 0;
|
|
for (i32 j = 0; j < n; j++) {
|
|
colw[j] = xcol[j] && xw>colw[j] ? xw : colw[j];
|
|
}
|
|
i32 ll = ts->ll - (vline[0]>0) - (vline[n]>0);
|
|
for (i32 r = 0; r < t.nrows; r++) {
|
|
TblRow *row = t.rows + r;
|
|
for (i32 j = 0; j < row->ncells && j < n; j++) {
|
|
TblCell *cell = row->cells + j;
|
|
TblSpec *sp = tblspec(&t, row, j);
|
|
if (!cell->block || sp->key=='s' || sp->key=='^') continue;
|
|
i32 span = tblspan(&t, row, j);
|
|
b32 x = 0;
|
|
for (i32 k = j; k < j+span; k++) {
|
|
x |= xcol[k];
|
|
}
|
|
if (!x) continue;
|
|
i32 cw = tblspanw(colw, sep, j, span);
|
|
i32 bw = span>1 ? ll*U_PER_N*span/(n + 1) : 0;
|
|
tblrender(c, cell, sp, hcols(cw>bw ? cw : bw), perm, scratch);
|
|
i32 w = cell->width * U_PER_N;
|
|
colw[j] = span==1 && w>colw[j] ? w : colw[j];
|
|
}
|
|
}
|
|
tbldivide(&t, colw, sep, ecol, xcol, scratch);
|
|
}
|
|
|
|
// Horizontal positions, rounded from basic units like troff motions,
|
|
// after which column widths are in character cells
|
|
i32 total = (frame + rframe) * U_PER_N;
|
|
for (i32 j = 0; j < n; j++) {
|
|
total += colw[j] + (j<n-1 ? sep[j]*U_PER_N : 0);
|
|
}
|
|
i32 x0 = ts->indent;
|
|
i32 avail = ts->ll - ts->indent;
|
|
if (t.expand && avail*U_PER_N>total) {
|
|
i32 extra = avail - hcols(total);
|
|
for (i32 j = 0; j < n; j++) {
|
|
i32 add = extra/(n - j);
|
|
colw[j] += add * U_PER_N;
|
|
extra -= add;
|
|
}
|
|
} else if (t.center) {
|
|
// Measured from groff: a frame costs one more column, and an
|
|
// overwide table may extend left into the indentation.
|
|
x0 += (avail - hcols(total) - (vline[0]||vline[n] ? 1 : 0))/2;
|
|
x0 = x0<0 ? 0 : x0;
|
|
}
|
|
i32 *colx = new(a, n+1, i32);
|
|
i32 *vx = new(a, n+1, i32);
|
|
i32 *numu = new(a, n, i32); // numeric alignment points, basic units
|
|
i32 u = (x0 + frame) * U_PER_N;
|
|
vx[0] = x0;
|
|
for (i32 j = 0; j < n; j++) {
|
|
colx[j] = hcols(u);
|
|
numu[j] = u + (colw[j] - (left[j] + right[j])*U_PER_N)/2 + left[j]*U_PER_N;
|
|
u += colw[j];
|
|
colw[j] = hcols(u) - colx[j];
|
|
if (j < n-1) {
|
|
vx[j+1] = hcols(u + sep[j]*U_PER_N/2);
|
|
u += sep[j] * U_PER_N;
|
|
}
|
|
}
|
|
i32 xend = hcols(u) + rframe; // one past the right frame
|
|
vx[n] = xend - 1;
|
|
|
|
// Vertical spans: \^ or a ^ format continues the cell above, across
|
|
// any data rule rows, while a format rule row continues as itself.
|
|
// vorg is the row of the cell shown in each row and column, and vend
|
|
// the last row of the span starting at each cell.
|
|
i32 *vorg = new(a, (iz)t.nrows*n, i32);
|
|
i32 *vend = new(a, (iz)t.nrows*n, i32);
|
|
for (i32 r = 0, prev = -1; r < t.nrows; r++) {
|
|
TblRow *row = t.rows + r;
|
|
for (i32 j = 0; j < n; j++) {
|
|
i32 k = r*n + j;
|
|
vorg[k] = vend[k] = r;
|
|
if (row->rule || prev<0) continue;
|
|
if ((j<row->ncells && row->cells[j].vspan) || tblspec(&t, row, j)->key=='^') {
|
|
i32 o = vorg[prev*n+j];
|
|
for (i32 q = prev+1; q <= r; q++) {
|
|
vorg[q*n+j] = o;
|
|
}
|
|
vend[o*n+j] = r;
|
|
}
|
|
}
|
|
prev = row->rule && !row->keyed ? prev : r;
|
|
}
|
|
|
|
// Lines of each row: the allbox rule below the row above, if that is
|
|
// a data or format rule row and more data follows, blank lines, a
|
|
// rule, then its own lines. Cells sit at the top of their row, and
|
|
// spanning cells are centered over their rows unless t, the last row
|
|
// growing to fit. Like tbl, a row ending no entry takes one line, and
|
|
// others what their entries need, at least one for those of a single
|
|
// row. In no-space mode, rows take no blank lines until one ends an
|
|
// entry or is a rule.
|
|
i32 *start = new(a, t.nrows, i32);
|
|
i32 *height = new(a, t.nrows, i32);
|
|
i32 lastdata = -1;
|
|
for (i32 r = 0; r < t.nrows; r++) {
|
|
lastdata = t.rows[r].rule ? lastdata : r;
|
|
}
|
|
b32 ns = ts->nospace && !t.box;
|
|
for (i32 r = 0, g = 0; r < t.nrows; r++) {
|
|
TblRow *row = t.rows + r;
|
|
b32 ab = t.allbox && r>0 && (!row[-1].rule || row[-1].keyed) && r<=lastdata;
|
|
row->space = ns ? 0 : row->space;
|
|
g += ab + row->space + (row->rule>0);
|
|
start[r] = g;
|
|
i32 h = 0;
|
|
b32 ends = 0;
|
|
for (i32 j = 0; !row->rule && j < n; j++) {
|
|
i32 o = vorg[r*n+j];
|
|
if (vend[o*n+j]!=r || tblspec(&t, t.rows+o, j)->key=='s') continue;
|
|
// An empty entry takes a line, but not a format rule row
|
|
i32 lines = j<t.rows[o].ncells ? t.rows[o].cells[j].nlines : 0;
|
|
lines = lines>1 ? lines : !t.rows[o].rule;
|
|
i32 need = lines - (g - start[o]);
|
|
h = need>h ? need : h;
|
|
ends = 1;
|
|
}
|
|
height[r] = row->rule ? 0 : ends ? h : !ns;
|
|
ns &= !row->rule && !ends;
|
|
g += height[r];
|
|
}
|
|
|
|
b32 utf8 = ts->utf8;
|
|
Str hz = utf8 ? S("\xe2\x94\x80") : S("-");
|
|
Str vt = utf8 ? S("\xe2\x94\x82") : S("|");
|
|
Str dh = utf8 ? S("\xe2\x95\x90") : S("=");
|
|
|
|
// Column lines cut by the spanning cells shown in each row, then a
|
|
// row of none. Like tbl, a rule row's are cut by the data rows on
|
|
// either side.
|
|
i32 nc = n + 1;
|
|
b32 *cross = new(a, (iz)(t.nrows+1)*nc, b32);
|
|
for (i32 r = 0; r < t.nrows; r++) {
|
|
for (i32 j = 0; !t.rows[r].rule && j < n; j++) {
|
|
i32 span = tblspan(&t, t.rows+vorg[r*n+j], j);
|
|
for (i32 k = j+1; k < j+span; k++) {
|
|
cross[r*nc+k] = 1;
|
|
}
|
|
}
|
|
}
|
|
for (i32 r = 1; r < t.nrows; r++) {
|
|
for (i32 k = 0; t.rows[r].rule && k < nc; k++) {
|
|
cross[r*nc+k] |= cross[(r-1)*nc+k];
|
|
}
|
|
}
|
|
for (i32 r = t.nrows-2; r >= 0; r--) {
|
|
for (i32 k = 0; t.rows[r].rule && k < nc; k++) {
|
|
cross[r*nc+k] |= cross[(r+1)*nc+k];
|
|
}
|
|
}
|
|
b32 *none = cross + (iz)t.nrows*nc;
|
|
b32 *last = t.nrows ? none-nc : none;
|
|
|
|
if (t.box) {
|
|
tblrule(ts, hz, x0, xend, vline, vx, n, 0, 0, cross);
|
|
emitrow(ts, xend);
|
|
}
|
|
b32 *open = new(a, n, b32);
|
|
i32 xr = xend + !vline[n]; // like tbl, rules reach the right edge
|
|
for (i32 r = 0, g = 0; r < t.nrows; r++) {
|
|
TblRow *row = t.rows + r;
|
|
i32 own = start[r] - g; // first line of the row's own
|
|
i32 ab = own - row->space - (row->rule>0); // allbox rule lines
|
|
b32 *cut = cross + (iz)r*nc;
|
|
for (i32 j = 0, k = 0; j < n; j++) {
|
|
// A continued cell holds open the columns it spans
|
|
i32 o = vorg[r*n+j];
|
|
i32 e = o<r ? j+tblspan(&t, t.rows+o, j) : 0;
|
|
k = e>k ? e : k;
|
|
open[j] = j < k;
|
|
}
|
|
for (i32 line = 0; line < own+height[r]; line++, g++) {
|
|
b32 rl = line<ab || (row->rule && line==own-1);
|
|
if (line < ab) {
|
|
// The allbox rule lies below the row above, and the line
|
|
// to a rule just under it is cut as in that row
|
|
b32 *below = row->rule && !row->space ? cut-nc : cut;
|
|
tblrule(ts, hz, x0, xr, vline, vx, n, open, cut-nc, below);
|
|
} else if (rl) {
|
|
// A rule row lies between its neighbors
|
|
b32 *above = !r ? none : line==ab ? cut-nc : cut;
|
|
b32 *below = r<t.nrows-1 ? cut+nc : cut;
|
|
Str ch = row->rule=='=' ? dh : hz;
|
|
tblrule(ts, ch, x0, xr, vline, vx, n, open, above, below);
|
|
} else {
|
|
clearrow(ts, xend+1);
|
|
for (i32 j = 0; j <= n; j++) {
|
|
if (vline[j] && !cut[j]) tblplace(ts, vx[j], vt);
|
|
}
|
|
}
|
|
for (i32 j = 0; j < n; j++) {
|
|
i32 o = vorg[r*n+j];
|
|
if (o==r && line<own) continue;
|
|
TblRow *orow = t.rows + o;
|
|
TblSpec *sp = tblspec(&t, orow, j);
|
|
if (sp->key=='s' || sp->key=='^') continue;
|
|
i32 span = tblspan(&t, orow, j);
|
|
i32 sw = colx[j+span-1] + colw[j+span-1] - colx[j];
|
|
if (sp->key=='_' || sp->key=='=' || (j<orow->ncells && orow->cells[j].rule)) {
|
|
// Ruled cell, or \Rx repeating a character
|
|
Str rc = sp->key=='=' ? dh : hz;
|
|
if (j<orow->ncells && orow->cells[j].rule) {
|
|
u8 *p = &orow->cells[j].rule;
|
|
rc = *p=='=' ? dh : *p=='_' ? hz : (Str){p, 1};
|
|
}
|
|
for (i32 xx = 0; o==r && line==own && xx<sw; xx++) {
|
|
tblplace(ts, colx[j]+xx, rc);
|
|
}
|
|
continue;
|
|
}
|
|
if (j >= orow->ncells) continue;
|
|
TblCell *cell = orow->cells + j;
|
|
i32 e = vend[o*n+j];
|
|
i32 top = start[o];
|
|
if (e>o && !sp->top) {
|
|
top += (start[e] + height[e] - start[o] - cell->nlines)/2;
|
|
}
|
|
i32 li = g - top;
|
|
if (li<0 || li>=cell->nlines) continue;
|
|
Capline *cl = cell->lines + li;
|
|
i32 w = cell->block ? cell->width : caplinewidth(cl); // blocks as a whole
|
|
i32 cx = colx[j];
|
|
switch (sp->key) {
|
|
case 'r': cx += sw - w; break;
|
|
case 'c': cx += (sw - w)/2; break;
|
|
case 'n':
|
|
if (span==1 && cell->left>=0) {
|
|
cx = hcols(numu[j] - cell->left*U_PER_N);
|
|
} else {
|
|
cx += (sw - w)/2;
|
|
}
|
|
break;
|
|
}
|
|
for (i32 k = 0; k < cl->len; k++) {
|
|
if (cx+k>=0 && cx+k<ts->rowcap && cl->cells[k].data) {
|
|
ts->row[cx+k] = cl->cells[k];
|
|
}
|
|
}
|
|
}
|
|
emitrow(ts, rl ? xr : xend);
|
|
}
|
|
}
|
|
if (t.box) {
|
|
tblrule(ts, hz, x0, xend, vline, vx, n, 0, last, 0);
|
|
emitrow(ts, xend);
|
|
}
|
|
}
|
|
|
|
static void tblline(Ctx *c, Str line, Arena scratch)
|
|
{
|
|
if (isterminator(line, S("TE"))) {
|
|
c->tblactive = 0;
|
|
tbllayout(c, bstr(c->tblsrc), scratch);
|
|
return;
|
|
}
|
|
bappend(&c->tblsrc, line, &c->perm);
|
|
bappendu8(&c->tblsrc, '\n', &c->perm);
|
|
}
|
|
|
|
|
|
// mdoc(7), following mandoc's terminal layout
|
|
|
|
enum {
|
|
MDIN = 5, // body indent
|
|
MDSS = 3, // subsection heading indent
|
|
MDDS = 6, // "Ds" display indent
|
|
};
|
|
|
|
enum {
|
|
MS_OTHER,
|
|
MS_NAME,
|
|
MS_SYNOPSIS,
|
|
MS_LIBRARY,
|
|
MS_SEEALSO,
|
|
MS_AUTHORS,
|
|
};
|
|
|
|
enum {
|
|
LT_TAG, LT_ITEM, LT_BULLET, LT_DASH, LT_ENUM, LT_HANG, LT_OHANG,
|
|
LT_INSET, LT_DIAG, LT_COLUMN,
|
|
};
|
|
|
|
enum {
|
|
DL_OPEN = 1,
|
|
DL_CLOSE,
|
|
DL_MIDDLE,
|
|
};
|
|
|
|
typedef struct {
|
|
Str *args;
|
|
b32 *quoted;
|
|
i32 n;
|
|
i32 i;
|
|
b32 block; // quoted arguments are words, not macros
|
|
} MdArgs;
|
|
|
|
static i32 mddelim(Str s)
|
|
{
|
|
if (s.len != 1) {
|
|
return 0;
|
|
}
|
|
switch (s.data[0]) {
|
|
case '(': case '[':
|
|
return DL_OPEN;
|
|
case '.': case ',': case ':': case ';': case ')': case ']': case '?':
|
|
case '!':
|
|
return DL_CLOSE;
|
|
case '|':
|
|
return DL_MIDDLE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// The kind of a callable macro named by an argument, or zero.
|
|
static i32 mdcallable(Ctx *c, Str s)
|
|
{
|
|
if (s.len<2 || s.len>3) {
|
|
return 0;
|
|
}
|
|
Entry *e = find(c->kinds, s);
|
|
return e && e->inc && e->num>=D_DD ? e->num : 0;
|
|
}
|
|
|
|
static Str glyph(Ctx *c, Str name)
|
|
{
|
|
Entry *e = find(c->glyphmap, name);
|
|
return e ? (c->ts->utf8 ? e->str : e->alt) : name;
|
|
}
|
|
|
|
// Emit a word in a font with mdoc spacing: no space after an opening
|
|
// delimiter, before a closing one, or after any word under .Sm off,
|
|
// and delimiters are never styled.
|
|
static void mdword(Ctx *c, Str w, i32 font, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
i32 d = mddelim(w);
|
|
b32 space = !c->mdnospace && !c->cont && d!=DL_CLOSE;
|
|
c->cont = 0;
|
|
if (space && hasglyphs(ts, 0, ts->ntoks) && ts->toks[ts->ntoks-1].type!=TK_SPACE) {
|
|
tsspace(ts, 1);
|
|
} else if (!space && (d==DL_CLOSE || c->mdnospace)) {
|
|
tsunspace(ts);
|
|
}
|
|
i32 saved = c->font, prev = c->prevfont;
|
|
c->font = d ? F_R : font;
|
|
Sink k = {0};
|
|
k.c = c;
|
|
c->nobreak = 1;
|
|
tokenize(c, w.len ? w : S("\\&"), &k, scratch); // "" holds its space
|
|
c->nobreak = 0;
|
|
c->font = saved;
|
|
c->prevfont = prev;
|
|
c->mdnospace = d==DL_OPEN || c->mdsmoff;
|
|
c->mdeos = d==DL_CLOSE && (w.data[0]=='.' || w.data[0]=='!' || w.data[0]=='?');
|
|
c->mdwrote = 1;
|
|
}
|
|
|
|
static void mdmacro(Ctx *c, i32 kind, Str name, MdArgs *a, Arena scratch);
|
|
|
|
// Process the remaining arguments: words, delimiters, callable macros.
|
|
// A called macro parses the rest of the line, and unlike a block macro
|
|
// it also calls macros named by quoted arguments.
|
|
static void mdrest(Ctx *c, MdArgs *a, i32 font, Arena scratch)
|
|
{
|
|
while (a->i < a->n) {
|
|
i32 i = a->i++;
|
|
Str w = a->args[i];
|
|
i32 k = a->block && a->quoted[i] ? 0 : mdcallable(c, w);
|
|
if (k && c->mddepth<MAXDEPTH) {
|
|
a->block = 0;
|
|
c->mddepth++;
|
|
mdmacro(c, k, w, a, scratch);
|
|
c->mddepth--;
|
|
} else {
|
|
mdword(c, w, font, scratch);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format arguments up to the next callable macro. Returns the number of
|
|
// non-delimiter words.
|
|
static i32 mdinline(Ctx *c, MdArgs *a, i32 font, Arena scratch)
|
|
{
|
|
i32 n = 0;
|
|
while (a->i < a->n) {
|
|
Str w = a->args[a->i];
|
|
if (mdcallable(c, w)) {
|
|
break;
|
|
}
|
|
mdword(c, w, font, scratch);
|
|
n += !mddelim(w);
|
|
a->i++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// Is the next argument missing or a macro? Like mandoc, then some
|
|
// in-line macros print nothing.
|
|
static b32 mdnoarg(Ctx *c, MdArgs *a)
|
|
{
|
|
return a->i==a->n || mdcallable(c, a->args[a->i]);
|
|
}
|
|
|
|
// Does a word argument follow before the next callable macro?
|
|
static b32 mdhasword(Ctx *c, MdArgs *a)
|
|
{
|
|
for (i32 i = a->i; i < a->n; i++) {
|
|
if (mdcallable(c, a->args[i])) return 0;
|
|
if (!mddelim(a->args[i])) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// End of the arguments, before any trailing closing delimiters, which
|
|
// are never quoted.
|
|
static i32 mdcontentend(MdArgs *a)
|
|
{
|
|
i32 end = a->n;
|
|
for (; end>a->i && !a->quoted[end-1] && mddelim(a->args[end-1])==DL_CLOSE; end--) {}
|
|
return end;
|
|
}
|
|
|
|
// Partial-implicit enclosure: the rest of the line, except trailing
|
|
// closing delimiters, inside open/close.
|
|
static void mdenclose(Ctx *c, MdArgs *a, Str open, Str close, i32 font, Arena scratch)
|
|
{
|
|
i32 end = mdcontentend(a);
|
|
for (; a->i<end && mddelim(a->args[a->i])==DL_OPEN; a->i++) {
|
|
mdword(c, a->args[a->i], F_R, scratch);
|
|
}
|
|
mdword(c, open, F_R, scratch);
|
|
c->mdnospace = 1;
|
|
MdArgs sub = {a->args, a->quoted, end, a->i, 1};
|
|
mdrest(c, &sub, font, scratch);
|
|
a->i = end;
|
|
c->mdnospace = 1;
|
|
mdword(c, close, F_R, scratch);
|
|
}
|
|
|
|
static Str mdunbreakable(Str s, Arena *a)
|
|
{
|
|
Buf b = {0};
|
|
for (iz i = 0; i < s.len; i++) {
|
|
if (s.data[i] == ' ') {
|
|
bappend(&b, S("\\ "), a);
|
|
} else {
|
|
bappendu8(&b, s.data[i], a);
|
|
}
|
|
}
|
|
return b.data ? bstr(b) : S("");
|
|
}
|
|
|
|
static void mdheader(Ctx *c, Arena scratch)
|
|
{
|
|
if (c->page) {
|
|
return;
|
|
}
|
|
Typeset *ts = c->ts;
|
|
tsbreak(ts);
|
|
c->page = 1;
|
|
ts->fill = 0; // titles never wrap
|
|
Sink k = {0};
|
|
k.c = c;
|
|
i32 font = c->font;
|
|
c->font = F_R;
|
|
Str ref = concat(&scratch, c->mdtitle, S("("));
|
|
ref = concat(&scratch, ref, c->mdsec);
|
|
ref = concat(&scratch, ref, S(")"));
|
|
tokenize(c, ref, &k, scratch);
|
|
i32 left = ts->ntoks;
|
|
tokenize(c, c->mdvol, &k, scratch);
|
|
i32 center = ts->ntoks;
|
|
tokenize(c, ref, &k, scratch);
|
|
tstitle(ts, left, center);
|
|
printu8(ts->out, '\n');
|
|
c->font = font;
|
|
ts->indent = MDIN;
|
|
ts->tindent = -1;
|
|
ts->fill = 1;
|
|
ts->adjust = AD_L;
|
|
ts->tabw = 8;
|
|
ts->vcollapse = 1;
|
|
ts->zerogap = 1;
|
|
ts->nospace = 1;
|
|
ts->vspace = 0;
|
|
}
|
|
|
|
static void mdfooter(Ctx *c, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
tsbreak(ts);
|
|
ts->vspace = 1;
|
|
ts->nospace = 0;
|
|
b32 fill = ts->fill;
|
|
ts->fill = 0; // titles never wrap
|
|
Sink k = {0};
|
|
k.c = c;
|
|
c->font = F_R;
|
|
tokenize(c, c->mdos, &k, scratch);
|
|
i32 left = ts->ntoks;
|
|
tokenize(c, c->mddate, &k, scratch);
|
|
i32 center = ts->ntoks;
|
|
tokenize(c, c->mdos, &k, scratch);
|
|
tstitle(ts, left, center);
|
|
ts->fill = fill;
|
|
c->page = 0;
|
|
}
|
|
|
|
static Str mdvolume(Str sec)
|
|
{
|
|
if (sec.len != 1) {
|
|
return defaultmanual(sec);
|
|
}
|
|
return sec.data[0]=='4' ? S("Device Drivers Manual") : defaultmanual(sec);
|
|
}
|
|
|
|
// A scaled width like "4n", or -1.
|
|
static i32 mdscaled(Str w)
|
|
{
|
|
if (w.len && (digit(w.data[0]) || w.data[0]=='.')) {
|
|
Num n = evalexpr(w, 'n', 0);
|
|
if (n.ok && !n.rest.len) return hcols(n.value);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// The -width argument of .Bl, in columns: a scaled width, "Ds", or the
|
|
// width of a string; mandoc then adds two columns of separation.
|
|
static i32 mdwidth(Ctx *c, Str w, Arena scratch)
|
|
{
|
|
// Default widths of macros named as widths, from mandoc
|
|
static struct { Str name; i32 width; } macros[] = {
|
|
{STR("Ad"), 12}, {STR("Ao"), 12}, {STR("An"), 12}, {STR("Aq"), 12},
|
|
{STR("Ar"), 12}, {STR("Bo"), 12}, {STR("Bq"), 12}, {STR("Cd"), 12},
|
|
{STR("Cm"), 10}, {STR("Do"), 10}, {STR("Dq"), 12}, {STR("Dv"), 12},
|
|
{STR("Eo"), 12}, {STR("Em"), 10}, {STR("Er"), 17}, {STR("Ev"), 15},
|
|
{STR("Fa"), 12}, {STR("Fl"), 10}, {STR("Fo"), 16}, {STR("Fn"), 16},
|
|
{STR("Ic"), 10}, {STR("Li"), 16}, {STR("Ms"), 6}, {STR("Nm"), 10},
|
|
{STR("No"), 12}, {STR("Oo"), 10}, {STR("Op"), 14}, {STR("Pa"), 32},
|
|
{STR("Pf"), 12}, {STR("Po"), 12}, {STR("Pq"), 12}, {STR("Ql"), 16},
|
|
{STR("Qo"), 12}, {STR("So"), 12}, {STR("Sq"), 12}, {STR("Sy"), 6},
|
|
{STR("Sx"), 16}, {STR("Tn"), 10}, {STR("Va"), 12}, {STR("Vt"), 12},
|
|
{STR("Xr"), 10},
|
|
};
|
|
i32 r = mdscaled(w);
|
|
for (i32 i = 0; r<0 && i<countof(macros); i++) {
|
|
if (equals(w, macros[i].name)) r = macros[i].width;
|
|
}
|
|
if (r < 0) {
|
|
r = equals(w, S("Ds")) ? MDDS : textwidth(c, w, scratch);
|
|
}
|
|
r = r<0 ? 0 : r>MAXIND ? MAXIND : r;
|
|
return r + 2;
|
|
}
|
|
|
|
// The -offset argument of .Bl or .Bd, in columns.
|
|
static i32 mdoffset(Ctx *c, Str w, Arena scratch)
|
|
{
|
|
if (equals(w, S("left"))) return 0;
|
|
if (equals(w, S("indent"))) return MDIN + 1;
|
|
if (equals(w, S("indent-two"))) return 2*(MDIN + 1);
|
|
if (equals(w, S("Ds"))) return MDDS;
|
|
i32 r = mdscaled(w);
|
|
r = r>=0 ? r : textwidth(c, w, scratch);
|
|
return r<0 ? 0 : r>MAXIND ? MAXIND : r;
|
|
}
|
|
|
|
static MdBlock *mdpush(Ctx *c, i32 kind)
|
|
{
|
|
if (c->mdnblocks == countof(c->mdblocks)) {
|
|
return 0;
|
|
}
|
|
MdBlock *b = c->mdblocks + c->mdnblocks++;
|
|
xset(b, 0, sizeof(*b));
|
|
b->kind = kind;
|
|
b->saveindent = c->ts->indent;
|
|
b->savefill = c->ts->fill;
|
|
return b;
|
|
}
|
|
|
|
static MdBlock *mdtop(Ctx *c, i32 kind)
|
|
{
|
|
for (i32 i = c->mdnblocks-1; i >= 0; i--) {
|
|
if (c->mdblocks[i].kind == kind) {
|
|
return c->mdblocks + i;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void mdpop(Ctx *c, i32 kind)
|
|
{
|
|
MdBlock *b = mdtop(c, kind);
|
|
if (!b) {
|
|
return;
|
|
}
|
|
c->ts->indent = b->saveindent;
|
|
c->ts->fill = b->savefill;
|
|
c->mdnblocks = (i32)(b - c->mdblocks);
|
|
}
|
|
|
|
// Note that a block's body begins here.
|
|
static void mdbody(Ctx *c, MdBlock *b)
|
|
{
|
|
b->lines = c->ts->lines;
|
|
b->body = c->ts->ntoks;
|
|
}
|
|
|
|
// Is nothing added to the block's body since it began?
|
|
static b32 mdatbody(Ctx *c, MdBlock *b)
|
|
{
|
|
return b && b->lines==c->ts->lines && b->body==c->ts->ntoks;
|
|
}
|
|
|
|
// Finish a list item's tag: body on the same line when it fits.
|
|
static void mdtagdone(Ctx *c, MdBlock *b)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
tscommit(ts);
|
|
i32 w = tspending(ts);
|
|
b32 any = hasglyphs(ts, 0, ts->ntoks);
|
|
b32 wrapped = ts->lines != b->lines;
|
|
c->mdnospace = 0;
|
|
c->mdwrote = 0; // the gap below replaces the line's end space
|
|
switch (b->type) {
|
|
case LT_TAG:
|
|
case LT_HANG:
|
|
// A tag fits beside the body when no wider than the width
|
|
// argument itself (the width includes two columns of gap)
|
|
if (any && !wrapped && (w+2<=b->width || b->type==LT_HANG)) {
|
|
tsfreeze(ts);
|
|
i32 gap = b->width - w;
|
|
tsmotion(ts, gap>0 ? gap : 1);
|
|
ts->word = ts->ntoks;
|
|
ts->tindent = b->indent;
|
|
c->mdnospace = 1; // the gap is the body's leading space
|
|
} else if (any) {
|
|
tsbreak(ts);
|
|
} else {
|
|
ts->tindent = -1; // the body of an empty tag starts at its indent
|
|
}
|
|
ts->indent = b->indent + b->width;
|
|
break;
|
|
case LT_OHANG:
|
|
tsbreak(ts);
|
|
ts->indent = b->indent;
|
|
break;
|
|
case LT_INSET:
|
|
case LT_DIAG:
|
|
// Like mandoc, one nonbreaking space after an inset head, two
|
|
// after any diag head, whether or not it ends a sentence, and
|
|
// the body's first word joins the head's last word
|
|
if (any || b->type==LT_DIAG) {
|
|
tsunspace(ts);
|
|
tsreopen(ts);
|
|
tsmotion(ts, b->type==LT_DIAG ? 2 : 1);
|
|
c->mdnospace = 1;
|
|
}
|
|
ts->indent = b->indent;
|
|
break;
|
|
}
|
|
mdbody(c, b);
|
|
}
|
|
|
|
// Does a macro read the rest of its line as words, even quoted macro
|
|
// names? Like mandoc, the enclosures do.
|
|
static b32 mdencloses(i32 kind)
|
|
{
|
|
switch (kind) {
|
|
case D_AQ: case D_BQ: case D_BRQ: case D_DQ: case D_OP: case D_PQ:
|
|
case D_QL: case D_QQ: case D_SQ: case D_AO: case D_BO: case D_BRO:
|
|
case D_DO: case D_EO: case D_OO: case D_PO: case D_QO: case D_SO:
|
|
case D_XO:
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Does an argument end a -column cell: a tab, or Ta? Like mandoc, a
|
|
// quoted Ta is a word unless the macro reading it calls macros.
|
|
static b32 mdcellend(MdArgs *a, i32 i, b32 called)
|
|
{
|
|
Str s = a->args[i];
|
|
if (a->quoted[i]) {
|
|
return called && equals(s, S("Ta"));
|
|
}
|
|
return equals(s, S("Ta")) || equals(s, S("\t")) || equals(s, S("\t "));
|
|
}
|
|
|
|
static void mditem(Ctx *c, MdArgs *a, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
MdBlock *b = mdtop(c, D_BL);
|
|
if (!b) {
|
|
mdrest(c, a, F_R, scratch);
|
|
return;
|
|
}
|
|
// Like mandoc, no blank line follows a -diag item without a body
|
|
b32 nobody = b->type==LT_DIAG && mdatbody(c, b);
|
|
tsbreak(ts);
|
|
if (!b->compact && b->count && b->type!=LT_COLUMN && !nobody) {
|
|
tsvspace(ts, 1);
|
|
}
|
|
b->count++;
|
|
ts->indent = b->indent;
|
|
ts->tindent = -1;
|
|
c->mdnospace = 0;
|
|
c->mdwrote = 0;
|
|
mdbody(c, b);
|
|
switch (b->type) {
|
|
case LT_ITEM:
|
|
break;
|
|
case LT_BULLET:
|
|
case LT_DASH:
|
|
case LT_ENUM: {
|
|
Str mark = b->type==LT_BULLET ? glyph(c, S("bu")) : S("-");
|
|
if (b->type == LT_ENUM) {
|
|
mark = concat(&scratch, fmti64(&scratch, b->count), S("."));
|
|
}
|
|
tsglyph(ts, mark, F_R, 0);
|
|
i32 w = tspending(ts);
|
|
tsfreeze(ts);
|
|
tsmotion(ts, b->width>w ? b->width-w : 1);
|
|
ts->word = ts->ntoks;
|
|
ts->tindent = b->indent;
|
|
ts->indent = b->indent + b->width;
|
|
c->mdnospace = 1;
|
|
mdbody(c, b);
|
|
} break;
|
|
case LT_COLUMN: {
|
|
// Cells separated by Ta or tabs, each starting at its column,
|
|
// and later lines of a cell, or of the item body, align with it.
|
|
// Macros end with their cell. Like mandoc, an overfull cell puts
|
|
// the next on a new line, and cells past the declared columns
|
|
// pack together.
|
|
i32 n = a->n, col = 0, x = 0;
|
|
i32 gap = b->ncols<5 ? 4 : b->ncols==5 ? 3 : 1;
|
|
ts->tindent = b->indent;
|
|
for (;;) {
|
|
i32 end = a->i;
|
|
for (b32 called = 0; end<n && !mdcellend(a, end, called); end++) {
|
|
i32 k = !a->quoted[end] || called ? mdcallable(c, a->args[end]) : 0;
|
|
called = k ? !mdencloses(k) : called;
|
|
}
|
|
a->n = end;
|
|
a->block = 1; // each cell parses like a new .It line
|
|
mdrest(c, a, F_R, scratch);
|
|
a->n = n;
|
|
if (end == n) {
|
|
return;
|
|
}
|
|
Str sep = a->args[end];
|
|
a->i = end + 1;
|
|
tscommit(ts);
|
|
i32 cur = tsstart(ts) - b->indent + tspending(ts);
|
|
b32 extra = col >= b->ncols;
|
|
x += extra ? 0 : b->colw[col] + gap;
|
|
col++;
|
|
if (!extra && cur>=x) {
|
|
tsbreak(ts);
|
|
} else {
|
|
ts->tindent = tsstart(ts); // the pending line keeps its start
|
|
tsfreeze(ts);
|
|
if (x > cur) tsmotion(ts, x-cur);
|
|
ts->word = ts->ntoks;
|
|
}
|
|
ts->indent = b->indent + x;
|
|
c->mdnospace = !equals(sep, S("\t "));
|
|
c->mdwrote = 0;
|
|
mdbody(c, b);
|
|
}
|
|
}
|
|
default: {
|
|
// Tagged forms: the arguments are the tag, and a long tag
|
|
// wraps to the body indentation
|
|
ts->tindent = b->indent;
|
|
ts->indent = b->indent + b->width;
|
|
b32 diag = b->type == LT_DIAG;
|
|
i32 saved = c->font;
|
|
for (i32 i = a->i; i < a->n; i++) {
|
|
if (!a->quoted[i] && equals(a->args[i], S("Xo"))) {
|
|
c->mdintag = 1;
|
|
}
|
|
}
|
|
mdrest(c, a, diag ? F_B : F_R, scratch);
|
|
c->font = saved;
|
|
if (!c->mdintag) {
|
|
mdtagdone(c, b);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
mdrest(c, a, F_R, scratch);
|
|
}
|
|
|
|
static Str mdstd(Ctx *c, Str arg, Arena *a)
|
|
{
|
|
static struct { Str name, text; } stds[] = {
|
|
{STR("-ansiC"), STR("ANSI X3.159-1989 (\\(lqANSI\\~C89\\(rq)")},
|
|
{STR("-ansiC-89"), STR("ANSI X3.159-1989 (\\(lqANSI\\~C89\\(rq)")},
|
|
{STR("-isoC"), STR("ISO/IEC 9899:1990 (\\(lqISO\\~C90\\(rq)")},
|
|
{STR("-isoC-90"), STR("ISO/IEC 9899:1990 (\\(lqISO\\~C90\\(rq)")},
|
|
{STR("-isoC-amd1"), STR("ISO/IEC 9899/AMD1:1995 (\\(lqISO\\~C90, Amendment 1\\(rq)")},
|
|
{STR("-isoC-99"), STR("ISO/IEC 9899:1999 (\\(lqISO\\~C99\\(rq)")},
|
|
{STR("-isoC-2011"), STR("ISO/IEC 9899:2011 (\\(lqISO\\~C11\\(rq)")},
|
|
{STR("-iso9945-1-90"), STR("ISO/IEC 9945-1:1990 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-iso9945-1-96"), STR("ISO/IEC 9945-1:1996 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-iso9945-2-93"), STR("ISO/IEC 9945-2:1993 (\\(lqPOSIX.2\\(rq)")},
|
|
{STR("-p1003.1"), STR("IEEE Std 1003.1 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-p1003.1-88"), STR("IEEE Std 1003.1-1988 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-p1003.1-90"), STR("IEEE Std 1003.1-1990 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-p1003.1-96"), STR("ISO/IEC 9945-1:1996 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-p1003.1b-93"), STR("IEEE Std 1003.1b-1993 (\\(lqPOSIX.1b\\(rq)")},
|
|
{STR("-p1003.1c-95"), STR("IEEE Std 1003.1c-1995 (\\(lqPOSIX.1c\\(rq)")},
|
|
{STR("-p1003.1g-2000"), STR("IEEE Std 1003.1g-2000 (\\(lqPOSIX.1g\\(rq)")},
|
|
{STR("-p1003.1i-95"), STR("IEEE Std 1003.1i-1995 (\\(lqPOSIX.1i\\(rq)")},
|
|
{STR("-p1003.1-2001"), STR("IEEE Std 1003.1-2001 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-p1003.1-2004"), STR("IEEE Std 1003.1-2004 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-p1003.1-2008"), STR("IEEE Std 1003.1-2008 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-p1003.1-2024"), STR("IEEE Std 1003.1-2024 (\\(lqPOSIX.1\\(rq)")},
|
|
{STR("-p1003.2"), STR("IEEE Std 1003.2 (\\(lqPOSIX.2\\(rq)")},
|
|
{STR("-p1003.2-92"), STR("IEEE Std 1003.2-1992 (\\(lqPOSIX.2\\(rq)")},
|
|
{STR("-p1003.2a-92"), STR("IEEE Std 1003.2a-1992 (\\(lqPOSIX.2\\(rq)")},
|
|
{STR("-svid4"), STR("System\\~V Interface Definition, Fourth Edition (\\(lqSVID4\\(rq)")},
|
|
{STR("-susv1"), STR("Version\\~1 of the Single UNIX Specification (\\(lqSUSv1\\(rq)")},
|
|
{STR("-susv2"), STR("Version\\~2 of the Single UNIX Specification (\\(lqSUSv2\\(rq)")},
|
|
{STR("-susv3"), STR("Version\\~3 of the Single UNIX Specification (\\(lqSUSv3\\(rq)")},
|
|
{STR("-susv4"), STR("Version\\~4 of the Single UNIX Specification (\\(lqSUSv4\\(rq)")},
|
|
{STR("-xpg3"), STR("X/Open Portability Guide Issue\\~3 (\\(lqXPG3\\(rq)")},
|
|
{STR("-xpg4"), STR("X/Open Portability Guide Issue\\~4 (\\(lqXPG4\\(rq)")},
|
|
{STR("-xpg4.2"), STR("X/Open Portability Guide Issue\\~4, Version\\~2 (\\(lqXPG4.2\\(rq)")},
|
|
{STR("-xsh5"), STR("X/Open System Interfaces and Headers Issue\\~5 (\\(lqXSH5\\(rq)")},
|
|
{STR("-xcurses4.2"), STR("X/Open Curses Issue\\~4, Version\\~2 (\\(lqXCURSES4.2\\(rq)")},
|
|
{STR("-ieee754"), STR("IEEE Std 754-1985")},
|
|
{STR("-iso8601"), STR("ISO 8601")},
|
|
{STR("-iso8802-3"), STR("ISO 8802-3: 1989")},
|
|
};
|
|
(void)c;
|
|
(void)a;
|
|
for (i32 i = 0; i < countof(stds); i++) {
|
|
if (equals(arg, stds[i].name)) {
|
|
return stds[i].text;
|
|
}
|
|
}
|
|
return arg;
|
|
}
|
|
|
|
static Str mdatt(Str v, Arena *a)
|
|
{
|
|
static struct { Str v, text; } vers[] = {
|
|
{STR("v1"), STR("Version\\~1 AT&T UNIX")}, {STR("v2"), STR("Version\\~2 AT&T UNIX")},
|
|
{STR("v3"), STR("Version\\~3 AT&T UNIX")}, {STR("v4"), STR("Version\\~4 AT&T UNIX")},
|
|
{STR("v5"), STR("Version\\~5 AT&T UNIX")}, {STR("v6"), STR("Version\\~6 AT&T UNIX")},
|
|
{STR("v7"), STR("Version\\~7 AT&T UNIX")}, {STR("32v"), STR("Version\\~32V AT&T UNIX")},
|
|
{STR("III"), STR("AT&T System\\~III UNIX")}, {STR("V"), STR("AT&T System\\~V UNIX")},
|
|
{STR("V.1"), STR("AT&T System\\~V Release\\~1 UNIX")},
|
|
{STR("V.2"), STR("AT&T System\\~V Release\\~2 UNIX")},
|
|
{STR("V.3"), STR("AT&T System\\~V Release\\~3 UNIX")},
|
|
{STR("V.4"), STR("AT&T System\\~V Release\\~4 UNIX")},
|
|
};
|
|
(void)a;
|
|
for (i32 i = 0; i < countof(vers); i++) {
|
|
if (equals(v, vers[i].v)) return vers[i].text;
|
|
}
|
|
return S("AT&T UNIX");
|
|
}
|
|
|
|
// Library descriptions for .Lb, from mandoc
|
|
static Str mdlib(Str name)
|
|
{
|
|
static struct { Str name, text; } libs[] = {
|
|
{STR("lib80211"), STR("802.11 Wireless Network Management Library (lib80211, \\-l80211)")},
|
|
{STR("libalias"), STR("Packet Aliasing Library (libalias, \\-lalias)")},
|
|
{STR("libarchive"), STR("Streaming Archive Library (libarchive, \\-larchive)")},
|
|
{STR("libarm"), STR("ARM Architecture Library (libarm, \\-larm)")},
|
|
{STR("libarm32"), STR("ARM32 Architecture Library (libarm32, \\-larm32)")},
|
|
{STR("libbe"), STR("Boot Environment Library (libbe, \\-lbe)")},
|
|
{STR("libbluetooth"), STR("Bluetooth Library (libbluetooth, \\-lbluetooth)")},
|
|
{STR("libbsdxml"), STR("eXpat XML parser library (libbsdxml, \\-lbsdxml)")},
|
|
{STR("libbsm"), STR("Basic Security Module Library (libbsm, \\-lbsm)")},
|
|
{STR("libc"), STR("Standard C\\~Library (libc, \\-lc)")},
|
|
{STR("libc_r"), STR("Reentrant C\\~Library (libc_r, \\-lc_r)")},
|
|
{STR("libcalendar"), STR("Calendar Arithmetic Library (libcalendar, \\-lcalendar)")},
|
|
{STR("libcam"), STR("Common Access Method User Library (libcam, \\-lcam)")},
|
|
{STR("libcasper"), STR("Casper Library (libcasper, \\-lcasper)")},
|
|
{STR("libcdk"), STR("Curses Development Kit Library (libcdk, \\-lcdk)")},
|
|
{STR("libcipher"), STR("FreeSec Crypt Library (libcipher, \\-lcipher)")},
|
|
{STR("libcompat"), STR("Compatibility Library (libcompat, \\-lcompat)")},
|
|
{STR("libcrypt"), STR("Crypt Library (libcrypt, \\-lcrypt)")},
|
|
{STR("libcurses"), STR("Curses Library (libcurses, \\-lcurses)")},
|
|
{STR("libcuse"), STR("Userland Character Device Library (libcuse, \\-lcuse)")},
|
|
{STR("libdevattr"), STR("Device attribute and event library (libdevattr, \\-ldevattr)")},
|
|
{STR("libdevctl"), STR("Device Control Library (libdevctl, \\-ldevctl)")},
|
|
{STR("libdevinfo"), STR("Device and Resource Information Utility Library (libdevinfo, \\-ldevinfo)")},
|
|
{STR("libdevstat"), STR("Device Statistics Library (libdevstat, \\-ldevstat)")},
|
|
{STR("libdisk"), STR("Interface to Slice and Partition Labels Library (libdisk, \\-ldisk)")},
|
|
{STR("libdl"), STR("Dynamic Linker Services Filter (libdl, \\-ldl)")},
|
|
{STR("libdm"), STR("Device Mapper Library (libdm, \\-ldm)")},
|
|
{STR("libdwarf"), STR("DWARF Access Library (libdwarf, \\-ldwarf)")},
|
|
{STR("libedit"), STR("Command Line Editor Library (libedit, \\-ledit)")},
|
|
{STR("libefi"), STR("EFI Runtime Services Library (libefi, \\-lefi)")},
|
|
{STR("libelf"), STR("ELF Access Library (libelf, \\-lelf)")},
|
|
{STR("libevent"), STR("Event Notification Library (libevent, \\-levent)")},
|
|
{STR("libexecinfo"), STR("Backtrace Information Library (libexecinfo, \\-lexecinfo)")},
|
|
{STR("libfetch"), STR("File Transfer Library (libfetch, \\-lfetch)")},
|
|
{STR("libform"), STR("Curses Form Library (libform, \\-lform)")},
|
|
{STR("libfsid"), STR("Filesystem Identification Library (libfsid, \\-lfsid)")},
|
|
{STR("libftpio"), STR("FTP Connection Management Library (libftpio, \\-lftpio)")},
|
|
{STR("libgeom"), STR("Userland API Library for Kernel GEOM subsystem (libgeom, \\-lgeom)")},
|
|
{STR("libgpio"), STR("General-Purpose Input Output (GPIO) library (libgpio, \\-lgpio)")},
|
|
{STR("libhammer"), STR("HAMMER Filesystem Userland Library (libhammer, \\-lhammer)")},
|
|
{STR("libi386"), STR("i386 Architecture Library (libi386, \\-li386)")},
|
|
{STR("libintl"), STR("Internationalized Message Handling Library (libintl, \\-lintl)")},
|
|
{STR("libipsec"), STR("IPsec Policy Control Library (libipsec, \\-lipsec)")},
|
|
{STR("libiscsi"), STR("iSCSI protocol library (libiscsi, \\-liscsi)")},
|
|
{STR("libisns"), STR("Internet Storage Name Service Library (libisns, \\-lisns)")},
|
|
{STR("libjail"), STR("Jail Library (libjail, \\-ljail)")},
|
|
{STR("libkcore"), STR("Kernel Memory Core Access Library (libkcore, \\-lkcore)")},
|
|
{STR("libkiconv"), STR("Kernel-side iconv Library (libkiconv, \\-lkiconv)")},
|
|
{STR("libkse"), STR("N:M Threading Library (libkse, \\-lkse)")},
|
|
{STR("libkvm"), STR("Kernel Data Access Library (libkvm, \\-lkvm)")},
|
|
{STR("libm"), STR("Math Library (libm, \\-lm)")},
|
|
{STR("libm68k"), STR("m68k Architecture Library (libm68k, \\-lm68k)")},
|
|
{STR("libmagic"), STR("Magic Number Recognition Library (libmagic, \\-lmagic)")},
|
|
{STR("libmandoc"), STR("Mandoc Macro Compiler Library (libmandoc, \\-lmandoc)")},
|
|
{STR("libmd"), STR("Message Digest (MD4, MD5, etc.) Support Library (libmd, \\-lmd)")},
|
|
{STR("libmemstat"), STR("Kernel Memory Allocator Statistics Library (libmemstat, \\-lmemstat)")},
|
|
{STR("libmenu"), STR("Curses Menu Library (libmenu, \\-lmenu)")},
|
|
{STR("libmj"), STR("Minimalist JSON library (libmj, \\-lmj)")},
|
|
{STR("libnetgraph"), STR("Netgraph User Library (libnetgraph, \\-lnetgraph)")},
|
|
{STR("libnetpgp"), STR("Netpgp Signing, Verification, Encryption and Decryption (libnetpgp, \\-lnetpgp)")},
|
|
{STR("libnetpgpverify"), STR("Netpgp Verification (libnetpgpverify, \\-lnetpgpverify)")},
|
|
{STR("libnpf"), STR("NPF Packet Filter Library (libnpf, \\-lnpf)")},
|
|
{STR("libnv"), STR("Name/value pairs library (libnv, \\-lnv)")},
|
|
{STR("libossaudio"), STR("OSS Audio Emulation Library (libossaudio, \\-lossaudio)")},
|
|
{STR("libpam"), STR("Pluggable Authentication Module Library (libpam, \\-lpam)")},
|
|
{STR("libpanel"), STR("Z-order for curses windows (libpanel, \\-lpanel)")},
|
|
{STR("libpcap"), STR("Packet capture Library (libpcap, \\-lpcap)")},
|
|
{STR("libpci"), STR("PCI Bus Access Library (libpci, \\-lpci)")},
|
|
{STR("libpmc"), STR("Performance Counters Library (libpmc, \\-lpmc)")},
|
|
{STR("libposix"), STR("POSIX Compatibility Library (libposix, \\-lposix)")},
|
|
{STR("libposix1e"), STR("POSIX.1e Security API Library (libposix1e, \\-lposix1e)")},
|
|
{STR("libppath"), STR("Property-List Paths Library (libppath, \\-lppath)")},
|
|
{STR("libproc"), STR("Processor Monitoring and Analysis Library (libproc, \\-lproc)")},
|
|
{STR("libprocstat"), STR("Process and Files Information Retrieval (libprocstat, \\-lprocstat)")},
|
|
{STR("libprop"), STR("Property Container Object Library (libprop, \\-lprop)")},
|
|
{STR("libpthread"), STR("POSIX Threads Library (libpthread, \\-lpthread)")},
|
|
{STR("libpthread_dbg"), STR("POSIX Debug Threads Library (libpthread_dbg, \\-lpthread_dbg)")},
|
|
{STR("libpuffs"), STR("puffs Convenience Library (libpuffs, \\-lpuffs)")},
|
|
{STR("libquota"), STR("Disk Quota Access and Control Library (libquota, \\-lquota)")},
|
|
{STR("libradius"), STR("RADIUS Client Library (libradius, \\-lradius)")},
|
|
{STR("librefuse"), STR("File System in Userspace Convenience Library (librefuse, \\-lrefuse)")},
|
|
{STR("libresolv"), STR("DNS Resolver Library (libresolv, \\-lresolv)")},
|
|
{STR("librpcsec_gss"), STR("RPC GSS-API Authentication Library (librpcsec_gss, \\-lrpcsec_gss)")},
|
|
{STR("librpcsvc"), STR("RPC Service Library (librpcsvc, \\-lrpcsvc)")},
|
|
{STR("librt"), STR("POSIX Real\\-time Library (librt, \\-lrt)")},
|
|
{STR("librtld_db"), STR("Debugging interface to the runtime linker Library (librtld_db, \\-lrtld_db)")},
|
|
{STR("librumpclient"), STR("Clientside Stubs for rump Kernel Remote Protocols (librumpclient, \\-lrumpclient)")},
|
|
{STR("libsaslc"), STR("Simple Authentication and Security Layer client library (libsaslc, \\-lsaslc)")},
|
|
{STR("libsbuf"), STR("Safe String Composition Library (libsbuf, \\-lsbuf)")},
|
|
{STR("libsdp"), STR("Bluetooth Service Discovery Protocol User Library (libsdp, \\-lsdp)")},
|
|
{STR("libssp"), STR("Buffer Overflow Protection Library (libssp, \\-lssp)")},
|
|
{STR("libstand"), STR("Standalone Applications Library (libstand, \\-lstand)")},
|
|
{STR("libstdthreads"), STR("C11 Threads Library (libstdthreads, \\-lstdthreads)")},
|
|
{STR("libsysdecode"), STR("System Argument Decoding Library (libsysdecode, \\-lsysdecode)")},
|
|
{STR("libSystem"), STR("System Library (libSystem, \\-lSystem)")},
|
|
{STR("libtacplus"), STR("TACACS+ Client Library (libtacplus, \\-ltacplus)")},
|
|
{STR("libtcplay"), STR("TrueCrypt-compatible API library (libtcplay, \\-ltcplay)")},
|
|
{STR("libtermcap"), STR("Termcap Access Library (libtermcap, \\-ltermcap)")},
|
|
{STR("libterminfo"), STR("Terminal Information Library (libterminfo, \\-lterminfo)")},
|
|
{STR("libthr"), STR("1:1 Threading Library (libthr, \\-lthr)")},
|
|
{STR("libufs"), STR("UFS File System Access Library (libufs, \\-lufs)")},
|
|
{STR("libugidfw"), STR("File System Firewall Interface Library (libugidfw, \\-lugidfw)")},
|
|
{STR("libulog"), STR("User Login Record Library (libulog, \\-lulog)")},
|
|
{STR("libusbhid"), STR("USB Human Interface Devices Library (libusbhid, \\-lusbhid)")},
|
|
{STR("libutil"), STR("System Utilities Library (libutil, \\-lutil)")},
|
|
{STR("libvgl"), STR("Video Graphics Library (libvgl, \\-lvgl)")},
|
|
{STR("libx86_64"), STR("x86_64 Architecture Library (libx86_64, \\-lx86_64)")},
|
|
{STR("libxo"), STR("Text, XML, JSON, and HTML Output Emission Library (libxo, \\-lxo)")},
|
|
{STR("libz"), STR("Compression Library (libz, \\-lz)")},
|
|
};
|
|
for (i32 i = 0; i < countof(libs); i++) {
|
|
if (equals(name, libs[i].name)) {
|
|
return libs[i].text;
|
|
}
|
|
}
|
|
return (Str){0};
|
|
}
|
|
|
|
// Position of an .Rs part in mandoc's output order
|
|
static i32 mdrefrank(u8 part)
|
|
{
|
|
Str order = S("ATBIJRNVUPQCDO");
|
|
i32 i = 0;
|
|
for (; i<order.len && order.data[i]!=part; i++) {}
|
|
return i;
|
|
}
|
|
|
|
// Begin a SYNOPSIS element on its own line, after a blank line if the
|
|
// previous line was an element other than its like or its function's
|
|
// type. Like mandoc, an .Nm block extends to the next .Nm, its first
|
|
// element continues the name's line, and the next .Nm gets a blank
|
|
// line only after a function or .Fd.
|
|
static void mdsynbreak(Ctx *c, i32 kind)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
if (c->mdsection != MS_SYNOPSIS) {
|
|
return;
|
|
}
|
|
i32 prev = c->mdsynprev;
|
|
c->mdsynprev = kind;
|
|
b32 nm = kind==D_NM && c->mdnmbody; // ends an .Nm block
|
|
if (!nm && prev==D_NM) {
|
|
return;
|
|
}
|
|
tsbreak(ts);
|
|
b32 fn = prev==D_FD || prev==D_FN || prev==D_FO;
|
|
b32 blank = nm ? fn :
|
|
prev==kind ? kind==D_FT || kind==D_FN || kind==D_FO :
|
|
prev==D_FT ? kind!=D_FN && kind!=D_FO :
|
|
fn || prev==D_IN || prev==D_VT;
|
|
if (blank) {
|
|
tsvspace(ts, 1);
|
|
}
|
|
}
|
|
|
|
// Left edge of SYNOPSIS elements: the body of an .Nm block, if any.
|
|
static i32 mdsynindent(Ctx *c)
|
|
{
|
|
return c->mdnmbody ? c->mdnmbody : MDIN;
|
|
}
|
|
|
|
static void mdmacro(Ctx *c, i32 kind, Str name, MdArgs *a, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
b32 syn = c->mdsection == MS_SYNOPSIS;
|
|
switch (kind) {
|
|
case D_AD: case D_AR: case D_EM: case D_VA: case D_PA: case D_FA: case D_FT:
|
|
case D_VT: {
|
|
b32 line = a->i==1; // first macro on the line
|
|
if (syn && (kind==D_FT || (kind==D_VT && line))) {
|
|
mdsynbreak(c, kind);
|
|
}
|
|
if (kind==D_FA && c->mdfo) {
|
|
// Function arguments inside .Fo
|
|
i32 end = mdcontentend(a);
|
|
for (; a->i < end; a->i++) {
|
|
if (c->mdfoargs++) {
|
|
mdword(c, S(","), F_R, scratch);
|
|
}
|
|
mdword(c, syn ? mdunbreakable(a->args[a->i], &scratch) : a->args[a->i], F_I, scratch);
|
|
}
|
|
break;
|
|
}
|
|
if (!mdhasword(c, a) && kind==D_AR) {
|
|
mdword(c, S("file\\ ..."), F_I, scratch);
|
|
} else if (!mdhasword(c, a) && kind==D_PA) {
|
|
mdword(c, S("~"), F_I, scratch);
|
|
}
|
|
mdinline(c, a, F_I, scratch);
|
|
if (syn && line && (kind==D_FT || kind==D_VT)) {
|
|
mdrest(c, a, F_R, scratch);
|
|
tsbreak(ts);
|
|
}
|
|
} break;
|
|
|
|
case D_CM: case D_IC: case D_SY: case D_CD: case D_FD: case D_MS:
|
|
if (syn && (kind==D_FD || kind==D_CD)) {
|
|
mdsynbreak(c, kind);
|
|
}
|
|
mdinline(c, a, F_B, scratch);
|
|
if (syn && (kind==D_FD || kind==D_CD)) {
|
|
mdrest(c, a, F_R, scratch);
|
|
tsbreak(ts);
|
|
}
|
|
break;
|
|
|
|
case D_DV: case D_ER: case D_EV: case D_LI: case D_NO: case D_TN: case D_MT:
|
|
mdinline(c, a, F_R, scratch);
|
|
break;
|
|
|
|
case D_SX:
|
|
mdinline(c, a, F_R, scratch);
|
|
break;
|
|
|
|
case D_NM: {
|
|
b32 line = a->i == 1;
|
|
if (!c->mdname.len && a->i<a->n && !mddelim(a->args[a->i]) && !mdcallable(c, a->args[a->i])) {
|
|
c->mdname = clone(&c->perm, a->args[a->i]);
|
|
}
|
|
if (syn && line) {
|
|
// Synopsis line: hanging indent after the name
|
|
mdsynbreak(c, D_NM);
|
|
Str nm = a->i<a->n && !mddelim(a->args[a->i]) && !mdcallable(c, a->args[a->i])
|
|
? a->args[a->i] : c->mdname;
|
|
i32 w = textwidth(c, nm, scratch);
|
|
c->mdnmbody = MDIN + w + 1;
|
|
ts->tindent = MDIN;
|
|
ts->indent = c->mdnmbody;
|
|
}
|
|
if (!mdhasword(c, a) && c->mdname.len) {
|
|
mdword(c, c->mdname, F_B, scratch);
|
|
}
|
|
mdinline(c, a, F_B, scratch);
|
|
} break;
|
|
|
|
case D_FL: {
|
|
// Each argument gets a leading dash; no arguments: a lone dash
|
|
b32 any = 0;
|
|
while (a->i < a->n) {
|
|
Str w = a->args[a->i];
|
|
if (mdcallable(c, w)) break;
|
|
if (mddelim(w)) {
|
|
if (!any) {
|
|
mdword(c, S("\\-"), F_B, scratch);
|
|
any = 1;
|
|
}
|
|
mdword(c, w, F_R, scratch);
|
|
} else {
|
|
mdword(c, concat(&scratch, S("\\-"), w), F_B, scratch);
|
|
any = 1;
|
|
}
|
|
a->i++;
|
|
}
|
|
if (!any) {
|
|
// A bare flag joins a following macro: "Fl Fl x" is "--x"
|
|
mdword(c, S("\\-"), F_B, scratch);
|
|
c->mdnospace = a->i < a->n;
|
|
}
|
|
} break;
|
|
|
|
case D_XR: {
|
|
if (mdnoarg(c, a)) {
|
|
break;
|
|
}
|
|
Str page = a->args[a->i++];
|
|
Str sec = a->i<a->n && !mddelim(a->args[a->i]) && !mdcallable(c, a->args[a->i])
|
|
? a->args[a->i++] : (Str){0};
|
|
Str text = page;
|
|
if (sec.data) {
|
|
text = concat(&scratch, page, S("("));
|
|
text = concat(&scratch, text, sec);
|
|
text = concat(&scratch, text, S(")"));
|
|
}
|
|
mdword(c, text, F_R, scratch);
|
|
} break;
|
|
|
|
case D_FN: {
|
|
if (a->i == a->n) {
|
|
break; // its name is never a macro
|
|
}
|
|
mdsynbreak(c, D_FN);
|
|
if (syn) {
|
|
if (tsempty(ts)) ts->tindent = mdsynindent(c); // not after .Nm's name
|
|
ts->indent = mdsynindent(c) + 4;
|
|
}
|
|
// Arguments end at a delimiter or a callable macro
|
|
Str fn = a->args[a->i++];
|
|
i32 end = a->i;
|
|
for (; end<a->n && !mddelim(a->args[end]) && !mdcallable(c, a->args[end]); end++) {}
|
|
mdword(c, fn, F_B, scratch);
|
|
c->mdnospace = 1;
|
|
mdword(c, S("("), F_R, scratch);
|
|
for (i32 k = 0; a->i < end; k++, a->i++) {
|
|
if (k) mdword(c, S(","), F_R, scratch);
|
|
mdword(c, syn ? mdunbreakable(a->args[a->i], &scratch) : a->args[a->i], F_I, scratch);
|
|
}
|
|
mdword(c, S(")"), F_R, scratch);
|
|
if (syn) {
|
|
mdword(c, S(";"), F_R, scratch);
|
|
mdrest(c, a, F_R, scratch);
|
|
tsbreak(ts);
|
|
ts->indent = mdsynindent(c);
|
|
}
|
|
} break;
|
|
|
|
case D_FO: {
|
|
mdsynbreak(c, D_FO);
|
|
if (syn) {
|
|
if (tsempty(ts)) ts->tindent = mdsynindent(c); // not after .Nm's name
|
|
ts->indent = mdsynindent(c) + 4;
|
|
}
|
|
if (a->i < a->n) {
|
|
mdword(c, a->args[a->i++], F_B, scratch);
|
|
}
|
|
c->mdnospace = 1;
|
|
mdword(c, S("("), F_R, scratch);
|
|
c->mdfo = 1;
|
|
c->mdfoargs = 0;
|
|
} break;
|
|
|
|
case D_FC:
|
|
c->mdfo = 0;
|
|
mdword(c, S(")"), F_R, scratch);
|
|
if (syn) {
|
|
mdword(c, S(";"), F_R, scratch);
|
|
mdrest(c, a, F_R, scratch);
|
|
tsbreak(ts);
|
|
ts->indent = mdsynindent(c);
|
|
}
|
|
break;
|
|
|
|
case D_IN:
|
|
if (mdnoarg(c, a)) {
|
|
break;
|
|
} else if (syn && a->i==1) {
|
|
mdsynbreak(c, D_IN);
|
|
Str f = a->args[a->i++];
|
|
mdword(c, concat(&scratch, S("#include\\ <"), concat(&scratch, f, S(">"))), F_B, scratch);
|
|
} else {
|
|
Str f = a->args[a->i++];
|
|
mdword(c, S("<"), F_R, scratch);
|
|
c->mdnospace = 1;
|
|
mdword(c, f, F_I, scratch);
|
|
c->mdnospace = 1;
|
|
mdword(c, S(">"), F_R, scratch);
|
|
}
|
|
break;
|
|
|
|
case D_AN: {
|
|
if (a->i<a->n && equals(a->args[a->i], S("-split"))) {
|
|
c->mdsplit = c->mdsplitset = 1;
|
|
a->i++;
|
|
break;
|
|
}
|
|
if (a->i<a->n && equals(a->args[a->i], S("-nosplit"))) {
|
|
c->mdsplit = 0;
|
|
c->mdsplitset = 1;
|
|
a->i++;
|
|
break;
|
|
}
|
|
if (c->mdsection==MS_AUTHORS && a->i==1) {
|
|
if (c->mdauthors && (c->mdsplit || !c->mdsplitset)) {
|
|
tsbreak(ts);
|
|
}
|
|
c->mdauthors = 1;
|
|
}
|
|
mdinline(c, a, F_R, scratch);
|
|
} break;
|
|
|
|
case D_AP:
|
|
c->mdnospace = 1;
|
|
mdword(c, S("'"), F_R, scratch);
|
|
c->mdnospace = 1;
|
|
break;
|
|
|
|
case D_NS:
|
|
if (a->i == 1) {
|
|
break; // like mandoc, ignored first on a line
|
|
}
|
|
c->mdnospace = 1;
|
|
if (ts->ntoks && ts->toks[ts->ntoks-1].type==TK_SPACE) {
|
|
ts->ntoks--;
|
|
ts->word = ts->ntoks;
|
|
}
|
|
break;
|
|
|
|
case D_PF:
|
|
if (a->i < a->n) {
|
|
mdword(c, a->args[a->i++], F_R, scratch);
|
|
c->mdnospace = 1;
|
|
}
|
|
break;
|
|
|
|
case D_SM: {
|
|
if (a->i<a->n && equals(a->args[a->i], S("on"))) {
|
|
c->mdsmoff = 0;
|
|
a->i++;
|
|
} else if (a->i<a->n && equals(a->args[a->i], S("off"))) {
|
|
c->mdsmoff = 1;
|
|
a->i++;
|
|
} else {
|
|
c->mdsmoff = !c->mdsmoff;
|
|
}
|
|
// Like mandoc, on cancels a no-space, but not at the start of a
|
|
// line or of an item body after the gap of its tag, mark, or cell
|
|
MdBlock *b = mdtop(c, D_BL);
|
|
b32 fresh = !ts->ntoks ||
|
|
(mdatbody(c, b) && b->type!=LT_INSET && b->type!=LT_DIAG);
|
|
if (!fresh) {
|
|
c->mdnospace &= c->mdsmoff;
|
|
}
|
|
} break;
|
|
|
|
case D_AQ: {
|
|
b32 mt = a->n-a->i>=2 && equals(a->args[a->i], S("Mt"));
|
|
mdenclose(c, a, mt ? S("<") : glyph(c, S("la")),
|
|
mt ? S(">") : glyph(c, S("ra")), F_R, scratch);
|
|
} break;
|
|
case D_BQ: mdenclose(c, a, S("["), S("]"), F_R, scratch); break;
|
|
case D_BRQ: mdenclose(c, a, S("{"), S("}"), F_R, scratch); break;
|
|
case D_DQ: mdenclose(c, a, glyph(c, S("lq")), glyph(c, S("rq")), F_R, scratch); break;
|
|
case D_OP: mdenclose(c, a, S("["), S("]"), F_R, scratch); break;
|
|
case D_PQ: mdenclose(c, a, S("("), S(")"), F_R, scratch); break;
|
|
case D_QQ: mdenclose(c, a, S("\""), S("\""), F_R, scratch); break;
|
|
case D_SQ:
|
|
case D_QL: mdenclose(c, a, glyph(c, S("oq")), glyph(c, S("cq")), F_R, scratch); break;
|
|
|
|
case D_AO: case D_BO: case D_BRO: case D_DO: case D_OO: case D_PO:
|
|
case D_QO: case D_SO: {
|
|
Str open = kind==D_AO ? glyph(c, S("la")) : kind==D_BO ? S("[") :
|
|
kind==D_BRO ? S("{") : kind==D_DO ? glyph(c, S("lq")) :
|
|
kind==D_OO ? S("[") : kind==D_PO ? S("(") :
|
|
kind==D_QO ? S("\"") : glyph(c, S("oq"));
|
|
mdword(c, open, F_R, scratch);
|
|
c->mdnospace = 1;
|
|
a->block = 1;
|
|
} break;
|
|
|
|
case D_AC: case D_BC: case D_BRC: case D_DC: case D_OC: case D_PC:
|
|
case D_QC: case D_SC: {
|
|
Str close = kind==D_AC ? glyph(c, S("ra")) : kind==D_BC ? S("]") :
|
|
kind==D_BRC ? S("}") : kind==D_DC ? glyph(c, S("rq")) :
|
|
kind==D_OC ? S("]") : kind==D_PC ? S(")") :
|
|
kind==D_QC ? S("\"") : glyph(c, S("cq"));
|
|
c->mdnospace = 1;
|
|
mdword(c, close, F_R, scratch);
|
|
} break;
|
|
|
|
case D_EO:
|
|
if (a->i < a->n) {
|
|
mdword(c, a->args[a->i++], F_R, scratch);
|
|
c->mdnospace = 1;
|
|
}
|
|
a->block = 1;
|
|
break;
|
|
|
|
case D_EC:
|
|
c->mdnospace = 1;
|
|
if (a->i < a->n) {
|
|
mdword(c, a->args[a->i++], F_R, scratch);
|
|
}
|
|
break;
|
|
|
|
case D_XO:
|
|
a->block = 1;
|
|
break;
|
|
|
|
case D_XC:
|
|
if (c->mdintag) {
|
|
c->mdintag = 0;
|
|
mdrest(c, a, F_R, scratch);
|
|
MdBlock *b = mdtop(c, D_BL);
|
|
if (b) mdtagdone(c, b);
|
|
}
|
|
break;
|
|
|
|
case D_AT:
|
|
mdword(c, mdatt(a->i<a->n ? a->args[a->i++] : S(""), &scratch), F_R, scratch);
|
|
break;
|
|
|
|
case D_BSX: case D_BX: case D_DX: case D_FX: case D_NX: case D_OX:
|
|
case D_UX: {
|
|
Str os = kind==D_BSX ? S("BSD/OS") : kind==D_DX ? S("DragonFly") :
|
|
kind==D_FX ? S("FreeBSD") : kind==D_NX ? S("NetBSD") :
|
|
kind==D_OX ? S("OpenBSD") : kind==D_UX ? S("UNIX") : S("BSD");
|
|
Str v = {0};
|
|
if (a->i<a->n && !mddelim(a->args[a->i]) && !mdcallable(c, a->args[a->i])) {
|
|
v = a->args[a->i++];
|
|
}
|
|
if (kind == D_BX) {
|
|
Str text = v.data ? concat(&scratch, v, S("BSD")) : S("BSD");
|
|
if (v.data && a->i<a->n && !mddelim(a->args[a->i]) && !mdcallable(c, a->args[a->i])) {
|
|
text = concat(&scratch, text, S("-"));
|
|
text = concat(&scratch, text, a->args[a->i++]);
|
|
}
|
|
mdword(c, text, F_R, scratch);
|
|
} else if (v.data && kind!=D_UX) {
|
|
mdword(c, concat(&scratch, os, concat(&scratch, S("\\~"), v)), F_R, scratch);
|
|
} else {
|
|
mdword(c, os, F_R, scratch);
|
|
if (v.data) mdword(c, v, F_R, scratch);
|
|
}
|
|
} break;
|
|
|
|
case D_ST:
|
|
if (a->i < a->n) {
|
|
mdword(c, mdstd(c, a->args[a->i++], &scratch), F_R, scratch);
|
|
}
|
|
break;
|
|
|
|
case D_LK: {
|
|
if (mdnoarg(c, a)) {
|
|
break;
|
|
}
|
|
i32 end = mdcontentend(a);
|
|
Str url = a->i<end ? a->args[a->i++] : S("");
|
|
if (a->i < end) {
|
|
for (; a->i < end; a->i++) {
|
|
mdword(c, a->args[a->i], F_R, scratch);
|
|
}
|
|
c->mdnospace = 1;
|
|
mdword(c, S(":"), F_R, scratch);
|
|
}
|
|
mdword(c, url, F_I, scratch);
|
|
} break;
|
|
|
|
case D_EX: {
|
|
if (a->i<a->n && equals(a->args[a->i], S("-std"))) a->i++;
|
|
i32 end = mdcontentend(a);
|
|
i32 n = end - a->i;
|
|
mdword(c, S("The"), F_R, scratch);
|
|
if (!n && c->mdname.len) {
|
|
mdword(c, c->mdname, F_B, scratch);
|
|
}
|
|
for (i32 k = 0; a->i < end; k++, a->i++) {
|
|
if (k && n>2) mdword(c, S(","), F_R, scratch);
|
|
if (k && k==n-1) mdword(c, S("and"), F_R, scratch);
|
|
mdword(c, a->args[a->i], F_B, scratch);
|
|
}
|
|
mdword(c, n>1 ? S("utilities exit\\~0 on success, and\\~>0 if an error occurs.")
|
|
: S("utility exits\\~0 on success, and\\~>0 if an error occurs."),
|
|
F_R, scratch);
|
|
} break;
|
|
|
|
case D_RV: {
|
|
if (a->i<a->n && equals(a->args[a->i], S("-std"))) a->i++;
|
|
i32 end = mdcontentend(a);
|
|
i32 n = end - a->i;
|
|
mdword(c, S("The"), F_R, scratch);
|
|
if (!n) {
|
|
mdword(c, concat(&scratch, c->mdname, S("()")), F_B, scratch);
|
|
}
|
|
for (i32 k = 0; a->i < end; k++, a->i++) {
|
|
if (k && n>2) mdword(c, S(","), F_R, scratch);
|
|
if (k && k==n-1) mdword(c, S("and"), F_R, scratch);
|
|
mdword(c, concat(&scratch, a->args[a->i], S("()")), F_B, scratch);
|
|
}
|
|
mdword(c, n>1 ? S("functions return") : S("function returns"), F_R, scratch);
|
|
mdword(c, S("the value\\~0 if successful; otherwise the value\\~-1 "
|
|
"is returned and the global variable"), F_R, scratch);
|
|
mdword(c, S("errno"), F_I, scratch);
|
|
mdword(c, S("is set to indicate the error."), F_R, scratch);
|
|
} break;
|
|
|
|
case D_LB: {
|
|
if (a->i == a->n) {
|
|
break;
|
|
}
|
|
b32 line = a->i==1;
|
|
Str lib = a->args[a->i++];
|
|
Str text = mdlib(lib);
|
|
if (!text.data) {
|
|
text = concat(&scratch, S("library \\(lq"), lib);
|
|
text = concat(&scratch, text, S("\\(rq"));
|
|
}
|
|
mdword(c, text, F_R, scratch);
|
|
if (c->mdsection==MS_LIBRARY && line) {
|
|
mdrest(c, a, F_R, scratch);
|
|
tsbreak(ts);
|
|
}
|
|
} break;
|
|
|
|
case D_TA:
|
|
mdword(c, S("\\t"), F_R, scratch);
|
|
break;
|
|
|
|
case D_IGNORE:
|
|
mdinline(c, a, F_R, scratch);
|
|
break;
|
|
|
|
default:
|
|
// Block macros used as callable arguments: just their text
|
|
(void)name;
|
|
mdinline(c, a, F_R, scratch);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static Str mddate(Str s, Arena *a)
|
|
{
|
|
s = trim(s);
|
|
if (startswith(s, S("$Mdocdate:"))) {
|
|
s = trim(cuthead(s, 10));
|
|
if (s.len && s.data[s.len-1]=='$') s = trim(takehead(s, s.len-1));
|
|
// "Month D YYYY" -> "Month D, YYYY"
|
|
iz sp = -1;
|
|
for (iz i = s.len-1; i >= 0; i--) {
|
|
if (s.data[i] == ' ') { sp = i; break; }
|
|
}
|
|
if (sp > 0 && s.data[sp-1] != ',') {
|
|
s = concat(a, concat(a, takehead(s, sp), S(",")), cuthead(s, sp));
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
static void mdocline(Ctx *c, i32 kind, Str name, Str args, Arena scratch)
|
|
{
|
|
Typeset *ts = c->ts;
|
|
MdBlock *bl = kind==D_IT ? mdtop(c, D_BL) : 0;
|
|
b32 cells = bl && bl->type==LT_COLUMN; // tabs separate cells
|
|
// Index 0 is the macro itself, so a->i==1 means "first on the line"
|
|
MacroArgs m = macroargs(name, args, cells, &scratch);
|
|
Str *argv = m.args.data + 1;
|
|
i32 nargs = (i32)m.args.len - 1;
|
|
b32 *quoted = m.quoted.data;
|
|
b32 block = kind==D_SH || kind==D_SS || kind==D_ND || kind==D_D1 ||
|
|
kind==D_DL || kind==D_IT;
|
|
MdArgs a = {m.args.data, quoted, nargs+1, 1, block};
|
|
|
|
switch (kind) {
|
|
case D_DD:
|
|
c->mddate = clone(&c->perm, mddate(args, &scratch));
|
|
return;
|
|
|
|
case D_DT:
|
|
c->mdtitle = clone(&c->perm, nargs>0 ? argv[0] : S("UNTITLED"));
|
|
c->mdsec = clone(&c->perm, nargs>1 ? argv[1] : S("1"));
|
|
c->mdvol = clone(&c->perm, mdvolume(c->mdsec));
|
|
if (nargs > 2) {
|
|
Str v = concat(&scratch, c->mdvol, S(" ("));
|
|
v = concat(&scratch, v, argv[2]);
|
|
c->mdvol = clone(&c->perm, concat(&scratch, v, S(")")));
|
|
}
|
|
return;
|
|
|
|
case D_OS:
|
|
c->mdos = clone(&c->perm, joinargs(argv, nargs, &scratch));
|
|
return;
|
|
|
|
case D_SH:
|
|
case D_SS: {
|
|
mdheader(c, scratch);
|
|
tsbreak(ts);
|
|
c->mdnblocks = 0;
|
|
tsvspace(ts, 1);
|
|
ts->fill = 1;
|
|
ts->indent = MDIN;
|
|
ts->tindent = kind==D_SH ? 0 : MDSS;
|
|
if (kind == D_SH) {
|
|
Str h = joinargs(argv, nargs, &scratch);
|
|
c->mdsection = equals(h, S("NAME")) ? MS_NAME :
|
|
equals(h, S("SYNOPSIS")) ? MS_SYNOPSIS :
|
|
equals(h, S("LIBRARY")) ? MS_LIBRARY :
|
|
equals(h, S("SEE ALSO")) ? MS_SEEALSO :
|
|
equals(h, S("AUTHORS")) ? MS_AUTHORS : MS_OTHER;
|
|
c->mdauthors = 0;
|
|
}
|
|
c->mdsynprev = 0;
|
|
c->mdnmbody = 0;
|
|
c->mdnospace = 0;
|
|
mdrest(c, &a, F_B, scratch);
|
|
tsbreak(ts);
|
|
ts->indent = MDIN;
|
|
ts->tindent = -1;
|
|
ts->nospace = 1;
|
|
c->font = F_R;
|
|
return;
|
|
}
|
|
}
|
|
|
|
mdheader(c, scratch);
|
|
switch (kind) {
|
|
case D_PP:
|
|
tsvspace(ts, 1);
|
|
if (c->mdsection == MS_SYNOPSIS) {
|
|
ts->indent = mdsynindent(c);
|
|
}
|
|
return;
|
|
|
|
case D_ND: {
|
|
mdword(c, glyph(c, S("en")), F_R, scratch);
|
|
mdrest(c, &a, F_R, scratch);
|
|
tsendline(ts);
|
|
return;
|
|
}
|
|
|
|
case D_D1:
|
|
case D_DL: {
|
|
tsbreak(ts);
|
|
i32 indent = ts->indent;
|
|
b32 fill = ts->fill;
|
|
ts->indent += MDDS;
|
|
if (kind == D_DL) ts->fill = 0;
|
|
c->mdnospace = 0;
|
|
mdrest(c, &a, F_R, scratch);
|
|
tsbreak(ts);
|
|
ts->indent = indent;
|
|
ts->fill = fill;
|
|
return;
|
|
}
|
|
|
|
case D_BD: {
|
|
tsbreak(ts);
|
|
b32 compact = 0, filled = 1;
|
|
i32 offset = 0;
|
|
for (i32 i = 0; i < nargs; i++) {
|
|
Str w = argv[i];
|
|
if (equals(w, S("-literal")) || equals(w, S("-unfilled"))) {
|
|
filled = 0;
|
|
} else if (equals(w, S("-filled")) || equals(w, S("-ragged")) || equals(w, S("-centered"))) {
|
|
filled = 1;
|
|
} else if (equals(w, S("-compact"))) {
|
|
compact = 1;
|
|
} else if (equals(w, S("-offset")) && i+1<nargs) {
|
|
offset = mdoffset(c, argv[++i], scratch);
|
|
}
|
|
}
|
|
if (!compact) tsvspace(ts, 1);
|
|
MdBlock *b = mdpush(c, D_BD);
|
|
if (!b) return;
|
|
ts->indent = ts->indent+offset>MAXIND ? MAXIND : ts->indent+offset;
|
|
ts->fill = filled;
|
|
b->indent = ts->indent;
|
|
b->lines = ts->lines;
|
|
return;
|
|
}
|
|
|
|
case D_ED:
|
|
tsbreak(ts);
|
|
mdpop(c, D_BD);
|
|
return;
|
|
|
|
case D_BL: {
|
|
tsbreak(ts);
|
|
MdBlock *b = mdpush(c, D_BL);
|
|
if (!b) return;
|
|
b->type = LT_TAG;
|
|
b->width = -1;
|
|
i32 offset = 0;
|
|
for (i32 i = 0; i < nargs; i++) {
|
|
Str w = argv[i];
|
|
static struct { Str name; i32 type; } types[] = {
|
|
{STR("-tag"), LT_TAG}, {STR("-item"), LT_ITEM},
|
|
{STR("-bullet"), LT_BULLET}, {STR("-dash"), LT_DASH},
|
|
{STR("-hyphen"), LT_DASH}, {STR("-enum"), LT_ENUM},
|
|
{STR("-hang"), LT_HANG}, {STR("-ohang"), LT_OHANG},
|
|
{STR("-inset"), LT_INSET}, {STR("-diag"), LT_DIAG},
|
|
{STR("-column"), LT_COLUMN},
|
|
};
|
|
b32 matched = 0;
|
|
for (i32 t = 0; t < countof(types); t++) {
|
|
if (equals(w, types[t].name)) {
|
|
b->type = types[t].type;
|
|
matched = 1;
|
|
}
|
|
}
|
|
if (matched) {
|
|
continue;
|
|
} else if (equals(w, S("-compact"))) {
|
|
b->compact = 1;
|
|
} else if (equals(w, S("-width")) && i+1<nargs) {
|
|
b->width = mdwidth(c, argv[++i], scratch);
|
|
} else if (equals(w, S("-offset")) && i+1<nargs) {
|
|
offset = mdoffset(c, argv[++i], scratch);
|
|
} else if (b->type==LT_COLUMN && b->ncols<countof(b->colw)) {
|
|
// Plain strings: no scaled widths or macro names here
|
|
i32 cw = textwidth(c, w, scratch);
|
|
b->colw[b->ncols++] = cw<0 ? 0 : cw>MAXIND ? MAXIND : cw;
|
|
}
|
|
}
|
|
if (b->width < 0) {
|
|
b->width = b->type==LT_BULLET || b->type==LT_DASH ? 4 :
|
|
b->type==LT_ENUM ? 5 : MDDS+2;
|
|
}
|
|
if (!b->compact) tsvspace(ts, 1);
|
|
b->indent = ts->indent+offset>MAXIND ? MAXIND : ts->indent+offset;
|
|
return;
|
|
}
|
|
|
|
case D_EL:
|
|
tsbreak(ts);
|
|
mdpop(c, D_BL);
|
|
ts->tindent = -1;
|
|
return;
|
|
|
|
case D_IT:
|
|
mditem(c, &a, scratch);
|
|
if (cells && !c->mdintag) {
|
|
// Following lines continue the last cell, spaced as after
|
|
// an in-line macro line; a cell without words starts flush
|
|
if (!ts->fill) {
|
|
tsbreak(ts);
|
|
} else if (!c->mdnospace && !c->mdsmoff && c->mdwrote) {
|
|
tsspace(ts, 1 + (c->mdeos ? (ts->sentspace + 6)/12 : 0));
|
|
}
|
|
} else if (!c->mdintag && !c->mdnospace && ts->ntoks && hasglyphs(ts, ts->word, ts->ntoks)) {
|
|
// Text on the following lines continues the item body
|
|
tsendline(ts);
|
|
}
|
|
return;
|
|
|
|
case D_RS: {
|
|
// Like mandoc, a blank line separates a reference in SEE ALSO
|
|
// from anything before it in its section, item body, or display
|
|
MdBlock *b = c->mdnblocks ? c->mdblocks+c->mdnblocks-1 : 0;
|
|
if (c->mdsection==MS_SEEALSO && !mdatbody(c, b)) {
|
|
tsvspace(ts, 1);
|
|
}
|
|
c->mdrefn = 0;
|
|
mdpush(c, D_RS);
|
|
return;
|
|
}
|
|
|
|
case D_RE: {
|
|
mdpop(c, D_RS);
|
|
i32 nauth = 0;
|
|
b32 quote = 0; // %T is quoted beside a book or journal
|
|
for (i32 i = 0; i < c->mdrefn; i++) {
|
|
nauth += c->mdrefs[i].part == 'A';
|
|
quote |= c->mdrefs[i].part=='B' || c->mdrefs[i].part=='J';
|
|
}
|
|
Buf b = {0};
|
|
for (i32 i = 0; i < c->mdrefn; i++) {
|
|
MdRef r = c->mdrefs[i];
|
|
if (i && i==nauth-1) {
|
|
bappend(&b, nauth==2 ? S(" and ") : S(", and "), &scratch);
|
|
} else if (i) {
|
|
bappend(&b, S(", "), &scratch);
|
|
}
|
|
if (r.part=='T' && quote) {
|
|
bappend(&b, S("\\(lq"), &scratch);
|
|
bappend(&b, r.v, &scratch);
|
|
bappend(&b, S("\\(rq"), &scratch);
|
|
} else if (r.part=='T' || r.part=='B' || r.part=='I' || r.part=='J') {
|
|
bappend(&b, S("\\fI"), &scratch);
|
|
bappend(&b, r.v, &scratch);
|
|
bappend(&b, S("\\fR"), &scratch);
|
|
} else {
|
|
bappend(&b, r.v, &scratch);
|
|
}
|
|
}
|
|
c->mdrefn = 0;
|
|
if (b.len) {
|
|
bappend(&b, S("."), &scratch);
|
|
Sink k = {0};
|
|
k.c = c;
|
|
mdword(c, S(""), F_R, scratch);
|
|
tokenize(c, bstr(b), &k, scratch);
|
|
tsendline(ts);
|
|
}
|
|
return;
|
|
}
|
|
|
|
case D_REF: {
|
|
// Parts sort into mandoc's order and are formatted at .Re
|
|
if (!nargs || c->mdrefn==countof(c->mdrefs)) {
|
|
return; // like mandoc, skip parts without arguments
|
|
}
|
|
MdRef r = {0};
|
|
r.v = clone(&c->perm, joinargs(argv, nargs, &scratch));
|
|
r.part = name.len>1 ? name.data[1] : 'O';
|
|
i32 i = c->mdrefn++;
|
|
for (; i>0 && mdrefrank(c->mdrefs[i-1].part)>mdrefrank(r.part); i--) {
|
|
c->mdrefs[i] = c->mdrefs[i-1];
|
|
}
|
|
c->mdrefs[i] = r;
|
|
return;
|
|
}
|
|
|
|
case D_BK:
|
|
case D_EK:
|
|
case D_NOOP:
|
|
return;
|
|
|
|
case D_BF: {
|
|
Str f = nargs ? argv[0] : S("");
|
|
c->font = equals(f, S("-emphasis")) || equals(f, S("Em")) ? F_I :
|
|
equals(f, S("-symbolic")) || equals(f, S("Sy")) ? F_B : F_R;
|
|
return;
|
|
}
|
|
|
|
case D_EF:
|
|
c->font = F_R;
|
|
return;
|
|
}
|
|
|
|
// In-line macro lines
|
|
a.i = 1;
|
|
c->mdwrote = 0;
|
|
mdmacro(c, kind, name, &a, scratch);
|
|
mdrest(c, &a, F_R, scratch);
|
|
c->mdeos &= !quoted[nargs]; // a quoted "." ends no sentence
|
|
if (!c->mdfo && kind!=D_FC) {
|
|
c->mdsynprev = kind; // .Fo through .Fc is one element
|
|
}
|
|
if (c->mdintag) {
|
|
return;
|
|
}
|
|
if (!ts->fill) {
|
|
// Like mandoc, .Sm off never joins unfilled lines
|
|
if (!c->mdnospace || c->mdsmoff) tsbreak(ts);
|
|
} else if (!c->mdnospace && !c->mdsmoff) {
|
|
// Like mandoc, a sentence ends here only when the line's
|
|
// last output is the sentence-ending delimiter itself. A line
|
|
// without words keeps the space already pending.
|
|
tscommit(ts);
|
|
b32 pending = ts->ntoks && ts->toks[ts->ntoks-1].type==TK_SPACE;
|
|
if (hasglyphs(ts, 0, ts->ntoks) && (c->mdwrote || !pending)) {
|
|
tsspace(ts, 1 + (c->mdeos ? (ts->sentspace + 6)/12 : 0));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Driver
|
|
|
|
static void initctx(Ctx *c)
|
|
{
|
|
Arena *perm = &c->perm;
|
|
for (i32 i = 0; i < countof(builtins); i++) {
|
|
Entry *e = lookup(&c->kinds, builtins[i].name, perm);
|
|
e->num = builtins[i].kind;
|
|
e->live = 1;
|
|
}
|
|
for (i32 i = 0; i < countof(mdocs); i++) {
|
|
Entry *e = lookup(&c->kinds, mdocs[i].name, perm);
|
|
e->num = mdocs[i].kind;
|
|
e->inc = mdocs[i].callable;
|
|
e->live = 1;
|
|
}
|
|
for (i32 i = 0; i < countof(glyphs); i++) {
|
|
Entry *e = lookup(&c->glyphmap, glyphs[i].name, perm);
|
|
e->str = glyphs[i].utf8;
|
|
e->alt = glyphs[i].ascii;
|
|
e->live = 1;
|
|
e = lookup(&c->asciimap, glyphs[i].utf8, perm);
|
|
if (!e->live) {
|
|
e->str = glyphs[i].ascii;
|
|
e->live = 1;
|
|
}
|
|
}
|
|
for (i32 i = 0; i < countof(translits); i++) {
|
|
Entry *e = lookup(&c->asciimap, translits[i].utf8, perm);
|
|
e->str = translits[i].ascii;
|
|
e->live = 1;
|
|
}
|
|
setstring(c, S("R"), S("\\(rg"));
|
|
setstring(c, S("Tm"), S("\\(tm"));
|
|
setstring(c, S("lq"), S("\\(lq"));
|
|
setstring(c, S("rq"), S("\\(rq"));
|
|
setstring(c, S("S"), S(""));
|
|
setstring(c, S("HF"), S("B"));
|
|
c->margin = IN;
|
|
c->pindent = IN;
|
|
c->pd = 1;
|
|
c->ts->indent = hcols(IN);
|
|
c->budget = (i64)1<<20; // the largest known page uses about 95k
|
|
}
|
|
|
|
static void run(Ctx *c, Arena scratch)
|
|
{
|
|
for (;;) {
|
|
Arena tmp = scratch;
|
|
Str line = nextline(c, &tmp);
|
|
if (!line.data) {
|
|
break;
|
|
}
|
|
processline(c, line, tmp, 0, 0);
|
|
}
|
|
if (c->defining) {
|
|
Entry *e = c->defining;
|
|
e->str = bstr(c->defbody);
|
|
e->live = 1;
|
|
c->defining = 0;
|
|
}
|
|
tsbreak(c->ts);
|
|
endpage(c, scratch);
|
|
}
|
|
|
|
static void usage(Output *out)
|
|
{
|
|
print(out, S(
|
|
"usage: nroff [-man|-mdoc] [-Tascii|-Tutf8] [-rLL=Nn] [-w] [FILE...]\n"
|
|
" -m* accepted and ignored; the format is detected\n"
|
|
" -t accepted and ignored; tbl is built in\n"
|
|
" -Tascii ASCII output (default)\n"
|
|
" -Tutf8 UTF-8 output\n"
|
|
" -rLL=N line length (default: $MANWIDTH, $COLUMNS, or 78)\n"
|
|
" -w warn about unsupported input\n"
|
|
" -h print this message\n"
|
|
"With no FILE, or when FILE is -, read standard input.\n"));
|
|
}
|
|
|
|
static i32 parsewidth(Str s)
|
|
{
|
|
Scaled v = scaled(s, 'n');
|
|
if (!v.ok || v.rel) {
|
|
return 0;
|
|
}
|
|
i32 cols = hcols(v.value);
|
|
return cols>=10 && cols<=1000 ? cols : 0;
|
|
}
|
|
|
|
static i32 nroff(i32 argc, u8 **argv, Plt *plt, Arena mem)
|
|
{
|
|
iz cap = mem.end - mem.beg;
|
|
Arena perm = mem;
|
|
Arena scratch = mem;
|
|
perm.end = mem.beg + cap/4*3;
|
|
scratch.beg = perm.end;
|
|
|
|
Output *out = newoutput(&perm, plt, 1);
|
|
Output *err = newoutput(&perm, plt, 2);
|
|
|
|
Ctx *c = new(&perm, 1, Ctx);
|
|
c->plt = plt;
|
|
c->err = err;
|
|
c->ts = newtypeset(&perm, out);
|
|
c->perm = perm; // from here on, allocate only from c->perm
|
|
Typeset *ts = c->ts;
|
|
|
|
ts->sgr = plt_sgr(plt) && !plt_getenv(plt, S("GROFF_NO_SGR"), &scratch).data;
|
|
i32 width = parsewidth(plt_getenv(plt, S("MANWIDTH"), &scratch));
|
|
if (!width) {
|
|
width = parsewidth(plt_getenv(plt, S("COLUMNS"), &scratch));
|
|
}
|
|
|
|
i32 nfiles = 0;
|
|
Str *files = new(&c->perm, argc+1, Str);
|
|
b32 dashdash = 0;
|
|
for (i32 i = 1; i < argc; i++) {
|
|
Str arg = import(argv[i]);
|
|
if (dashdash || arg.len<2 || arg.data[0]!='-') {
|
|
files[nfiles++] = arg;
|
|
continue;
|
|
}
|
|
if (equals(arg, S("--"))) {
|
|
dashdash = 1;
|
|
} else if (arg.data[1] == 'm' || equals(arg, S("-t")) ||
|
|
arg.data[1] == 'P') {
|
|
// ignored
|
|
} else if (arg.data[1] == 'T') {
|
|
Str t = cuthead(arg, 2);
|
|
if (!t.len && i+1<argc) {
|
|
t = import(argv[++i]);
|
|
}
|
|
ts->utf8 = equals(t, S("utf8")) || equals(t, S("utf-8"));
|
|
} else if (arg.data[1] == 'r') {
|
|
Str r = cuthead(arg, 2);
|
|
if (!r.len && i+1<argc) {
|
|
r = import(argv[++i]);
|
|
}
|
|
Cut kv = cut(r, '=');
|
|
Str key = kv.head, val = kv.tail;
|
|
if (!kv.ok && key.len > 1) {
|
|
// groff also accepts -rLL78n
|
|
key = takehead(r, 2);
|
|
val = cuthead(r, 2);
|
|
if (key.len<2 || r.len<3) {
|
|
key = takehead(r, 1);
|
|
val = cuthead(r, 1);
|
|
}
|
|
}
|
|
if (equals(key, S("LL"))) {
|
|
i32 w = parsewidth(val);
|
|
if (w) width = w;
|
|
} else if (key.len) {
|
|
Scaled v = scaled(val, 'u');
|
|
if (v.ok) setreg(c, key, v.value);
|
|
}
|
|
} else if (equals(arg, S("-w")) || startswith(arg, S("-w")) ||
|
|
startswith(arg, S("-W"))) {
|
|
c->warn = arg.data[1] == 'w';
|
|
} else if (equals(arg, S("-h")) || equals(arg, S("--help"))) {
|
|
usage(out);
|
|
flush(out);
|
|
return out->err;
|
|
} else {
|
|
print(err, S("nroff: unknown option: "));
|
|
print(err, arg);
|
|
print(err, S("\n"));
|
|
usage(err);
|
|
flush(err);
|
|
return 2;
|
|
}
|
|
}
|
|
ts->ll = width ? width : 78;
|
|
|
|
initctx(c);
|
|
if (!nfiles) {
|
|
files[nfiles++] = S("-");
|
|
}
|
|
for (i32 i = nfiles-1; i >= 0; i--) {
|
|
b32 isstdin = equals(files[i], S("-"));
|
|
Str src = plt_load(plt, isstdin ? (Str){0} : files[i], &c->perm);
|
|
if (!src.data) {
|
|
print(err, S("nroff: cannot open: "));
|
|
print(err, files[i]);
|
|
print(err, S("\n"));
|
|
flush(err);
|
|
c->status = 1;
|
|
continue;
|
|
}
|
|
pushfile(c, isstdin ? S("<stdin>") : files[i], src);
|
|
}
|
|
run(c, scratch);
|
|
flush(out);
|
|
flush(err);
|
|
// A failed write (e.g. the pager quit) is quiet but not successful
|
|
return c->status ? c->status : out->err;
|
|
}
|
|
|
|
|
|
#if TEST
|
|
// Table-driven tests: each case is rendered at 40 columns and compared
|
|
// with output verified against groff (man) or mandoc (mdoc).
|
|
#define __USE_MINGW_SETJMP_NON_SEH // mingw: SEH longjmp fails at -O0
|
|
#include <setjmp.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
struct Plt {
|
|
Str input;
|
|
b32 sgr;
|
|
jmp_buf oom;
|
|
iz len;
|
|
u8 out[1<<16];
|
|
};
|
|
|
|
static Str plt_load(Plt *plt, Str path, Arena *a)
|
|
{
|
|
return path.data ? (Str){0} : clone(a, plt->input);
|
|
}
|
|
|
|
static b32 plt_write(Plt *plt, i32 fd, u8 *buf, i32 len)
|
|
{
|
|
if (fd == 2) {
|
|
fwrite(buf, 1, touz(len), stderr);
|
|
return 1;
|
|
}
|
|
if (len > countof(plt->out) - plt->len) {
|
|
return 0;
|
|
}
|
|
xcpy(plt->out+plt->len, buf, touz(len));
|
|
plt->len += len;
|
|
return 1;
|
|
}
|
|
|
|
static Str plt_getenv(Plt *plt, Str name, Arena *a)
|
|
{
|
|
(void)plt;
|
|
(void)name;
|
|
(void)a;
|
|
return (Str){0};
|
|
}
|
|
|
|
static b32 plt_sgr(Plt *plt)
|
|
{
|
|
return plt->sgr;
|
|
}
|
|
|
|
static void plt_oom(Plt *plt)
|
|
{
|
|
longjmp(plt->oom, 1);
|
|
}
|
|
|
|
typedef struct {
|
|
char *name;
|
|
char *input;
|
|
char *want;
|
|
} Case;
|
|
|
|
// Render input with the given options, returning the exit status.
|
|
static i32 render(Plt *plt, byte *mem, iz cap, char *input, char **opts, i32 nopts)
|
|
{
|
|
char *argv[8] = {"nroff"};
|
|
i32 argc = 1;
|
|
for (i32 i = 0; i < nopts; i++) {
|
|
argv[argc++] = opts[i];
|
|
}
|
|
plt->input = import((u8 *)input);
|
|
plt->len = 0;
|
|
xset(mem, 0x55, touz(cap)); // catch reads of uninitialized memory
|
|
Arena a = {plt, mem, mem+cap};
|
|
if (setjmp(plt->oom)) {
|
|
return -1;
|
|
}
|
|
return nroff(argc, (u8 **)argv, plt, a);
|
|
}
|
|
|
|
static b32 expect(Case *c, Plt *plt)
|
|
{
|
|
Str got = {plt->out, plt->len};
|
|
Str want = import((u8 *)c->want);
|
|
if (equals(got, want)) {
|
|
return 1;
|
|
}
|
|
fprintf(stderr, "FAIL %s\n--- want\n%s--- got\n%.*s---\n",
|
|
c->name, c->want, (int)got.len, (char *)got.data);
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
static Case cases[] = {
|
|
{
|
|
"fill",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH NAME\n"
|
|
"t \\- test\n"
|
|
".SH DESCRIPTION\n"
|
|
"One two three four five six seven eight nine ten eleven twelve.\n"
|
|
"Thirteen.\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" t - test\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" One two three four five six seven\n"
|
|
" eight nine ten eleven twelve.\n"
|
|
" Thirteen.\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"sentence",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"End here.\n"
|
|
"Next \"q.\"\n"
|
|
"Mid. line e.g.\\& not.\n"
|
|
"And (x.)\n"
|
|
"end\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" End here. Next \"q.\" Mid. line\n"
|
|
" e.g. not. And (x.) end\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"adjust",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".ad l\n"
|
|
"left adjusted words that wrap around the line for sure ok\n"
|
|
".ad b\n"
|
|
"both adjusted words that wrap around the line for sure ok\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" left adjusted words that wrap\n"
|
|
" around the line for sure ok both\n"
|
|
" adjusted words that wrap around\n"
|
|
" the line for sure ok\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tags",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TP 4\n"
|
|
"ab\n"
|
|
"fits\n"
|
|
".TP 4\n"
|
|
"abcd\n"
|
|
"next line\n"
|
|
".TP\n"
|
|
".B \\-x\n"
|
|
"bold tag\n"
|
|
".IP \\(bu 2\n"
|
|
"bullet\n"
|
|
".IP \"\" 4\n"
|
|
"no tag\n"
|
|
".HP 4\n"
|
|
"hanging paragraph text that wraps to the next line\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" ab fits\n"
|
|
"\n"
|
|
" abcd\n"
|
|
" next line\n"
|
|
"\n"
|
|
" -x bold tag\n"
|
|
"\n"
|
|
" \xe2\x80\xa2"" bullet\n"
|
|
"\n"
|
|
" no tag\n"
|
|
"\n"
|
|
" hanging paragraph text that wraps\n"
|
|
" to the next line\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"adresume",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".ad c\n"
|
|
".na\n"
|
|
".ad\n"
|
|
"centered\n"
|
|
".br\n"
|
|
".ad r\n"
|
|
".ad\n"
|
|
"right\n"
|
|
".br\n"
|
|
".ad r\n"
|
|
".na\n"
|
|
"left\n"
|
|
".br\n"
|
|
".ad c\n"
|
|
".nr J \\n(.j\n"
|
|
".ad l\n"
|
|
"j=\\nJ\n"
|
|
".br\n"
|
|
".ad \\nJ\n"
|
|
"centered\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" centered\n"
|
|
" right\n"
|
|
" left\n"
|
|
" j=3\n"
|
|
" centered\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"rsneg",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"top\n"
|
|
".RS -4\n"
|
|
"out four\n"
|
|
".RE\n"
|
|
".RS 3\n"
|
|
"in three\n"
|
|
".RS -2\n"
|
|
"back two\n"
|
|
".RE\n"
|
|
"in three\n"
|
|
".RE\n"
|
|
".RS foo\n"
|
|
"not moved\n"
|
|
".RE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" top\n"
|
|
" out four\n"
|
|
" in three\n"
|
|
" back two\n"
|
|
" in three\n"
|
|
" not moved\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"rs",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"top\n"
|
|
".RS\n"
|
|
"in one\n"
|
|
".RS 3.5\n"
|
|
"in two\n"
|
|
".RE\n"
|
|
"back one\n"
|
|
".RE\n"
|
|
"back\n"
|
|
".SS Sub\n"
|
|
"s\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" top\n"
|
|
" in one\n"
|
|
" in two\n"
|
|
" back one\n"
|
|
" back\n"
|
|
"\n"
|
|
" Sub\n"
|
|
" s\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"fonts",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"\\fBb\\fR \\fIi\\fR \\f(BIbi\\fP r\n"
|
|
".BR bold roman\n"
|
|
".IR it ro\n"
|
|
".B\n"
|
|
"next line bold\n"
|
|
"plain\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" b i bi r boldroman itro next line\n"
|
|
" bold plain\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"escapes",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"a\\-b \\e \\(em \\(lq\\(rq \\[u00E9] \\(co \\&.x a\\ b \\(bu \\(aq \\s-1small\\s0 x\\|y z\\c\n"
|
|
"w\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" a-b \\ \xe2\x80\x94"" \xe2\x80\x9c""\xe2\x80\x9d"" \xc3\xa9"" \xc2\xa9"" .x a b \xe2\x80\xa2"" ' small\n"
|
|
" xy zw\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"strings",
|
|
".ds X hello\n"
|
|
".ds Y \\\\*X world\n"
|
|
".nr N 5\n"
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"\\*X \\*Y \\nN \\n(.g \\w'abc'\n"
|
|
".nr N +2\n"
|
|
"\\nN\n"
|
|
".as X !\n"
|
|
"\\*X\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" hello hello world 5 1 72 7 hello!\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"cond",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".if n yes-n\n"
|
|
".if t yes-t\n"
|
|
".ie \\n(.g>0 \\{\\\n"
|
|
"block-true\n"
|
|
".\\}\n"
|
|
".el \\{\\\n"
|
|
"block-false\n"
|
|
".\\}\n"
|
|
".if '\\fBa\\fR'a' strcmp\n"
|
|
".if !d ZZ undef\n"
|
|
".if (1+2*3)=9 math\n"
|
|
".if r .g dot-g\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" yes-n block-true undef math dot-g\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"macros",
|
|
".de GR\n"
|
|
"Hello \\\\$1 and \\\\$2.\n"
|
|
"..\n"
|
|
".de NEST\n"
|
|
".GR \\\\$@\n"
|
|
"..\n"
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".GR one two\n"
|
|
".NEST \"three four\" five\n"
|
|
"after\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" Hello one and two. Hello three\n"
|
|
" four and five. after\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"macroargs",
|
|
".de GR\n"
|
|
"Hello \\\\$1 and \\\\$2.\n"
|
|
"..\n"
|
|
".ds W world\n"
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".GR \\*W x\\*[W]y\n"
|
|
".GR \"\\*W\" z\n"
|
|
"after\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" Hello world and xworldy. Hello\n"
|
|
" world and z. after\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"macroframes",
|
|
".de L0\n..\n"
|
|
".de L1\n.L0 \\\\$1\n.L0 \\\\$1\n.L0 \\\\$1\n.L0 \\\\$1\n..\n"
|
|
".de L2\n.L1 \\\\$1\n.L1 \\\\$1\n.L1 \\\\$1\n.L1 \\\\$1\n..\n"
|
|
".de L3\n.L2 \\\\$1\n.L2 \\\\$1\n.L2 \\\\$1\n.L2 \\\\$1\n..\n"
|
|
".de L4\n.L3 \\\\$1\n.L3 \\\\$1\n.L3 \\\\$1\n.L3 \\\\$1\n\\\\$2\n..\n"
|
|
".ds x xxxxxxxxxxxxxxxx\n"
|
|
".as x \\*x\n.as x \\*x\n.as x \\*x\n.as x \\*x\n"
|
|
".as x \\*x\n.as x \\*x\n.as x \\*x\n.as x \\*x\n"
|
|
".as x \\*x\n.as x \\*x\n.as x \\*x\n.as x \\*x\n"
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".L4 \\*x end\n"
|
|
"ok\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" end ok\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"titles",
|
|
".TH \"A B\" 1 2026 \"src a b c\" \"x y z w\"\n"
|
|
".SH NAME\n"
|
|
"text\n"
|
|
".RS 20\n"
|
|
"indented\n",
|
|
"A B(1) x y z w A B(1)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" text\n"
|
|
" indented\n"
|
|
"\n"
|
|
"src a b c 2026 A B(1)\n",
|
|
},
|
|
{
|
|
"titletabs",
|
|
".TH \"A\tB\" 1 \"d\te\" \"s\tr\" \"M\tN\"\n"
|
|
".SH NAME\n"
|
|
"text\n",
|
|
"A B(1) M N A B(1)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" text\n"
|
|
"\n"
|
|
"s r d e A B(1)\n",
|
|
},
|
|
{
|
|
"titleabbrev",
|
|
".TH \"foo\\-bar\\-baz\\-qux\\-\\fBcorge\\fR\" 1 d s M\n"
|
|
".SH NAME\n"
|
|
"text\n",
|
|
"foo-bar...corge(1) M foo-bar...corge(1)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" text\n"
|
|
"\n"
|
|
"s d foo-bar...corge(1)\n",
|
|
},
|
|
{
|
|
"titletabstops",
|
|
".TH \"AAAAA\tBBBBBBBB\" 1 d \"s\tr\" M\n"
|
|
".SH NAME\n"
|
|
".ta 3n 6n\n"
|
|
"text\n",
|
|
"AAAAA...BBBBB(1) M AAAAA...BBBBB(1)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" text\n"
|
|
"\n"
|
|
"s r d AAAAA...BBBBB(1)\n",
|
|
},
|
|
{
|
|
"nofill",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".nf\n"
|
|
"line one\n"
|
|
"\tTabbed\n"
|
|
" spaced\n"
|
|
".fi\n"
|
|
"filled again\n"
|
|
".in 4\n"
|
|
"indented\n"
|
|
".ti +2\n"
|
|
"temp\n"
|
|
"back to in\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" line one\n"
|
|
" Tabbed\n"
|
|
" spaced\n"
|
|
" filled again\n"
|
|
" indented\n"
|
|
" temp back to in\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"center",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".ce 2\n"
|
|
"center one\n"
|
|
"center two\n"
|
|
"not centered\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" center one\n"
|
|
" center two\n"
|
|
" not centered\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"links",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"See\n"
|
|
".UR https://e.com\n"
|
|
"link\n"
|
|
".UE .\n"
|
|
".MT a@b.c\n"
|
|
".ME\n"
|
|
".MR ls 1 ,\n"
|
|
".OP \\-o file\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" See link \xe2\x9f\xa8""https://e.com\xe2\x9f\xa9"".\n"
|
|
" \xe2\x9f\xa8""a@b.c\xe2\x9f\xa9"" ls(1), [-o file]\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"hyphen",
|
|
".TH T 1\n"
|
|
".SH A\n"
|
|
"xxxxxxxxxxxxxxxxxxxxxxxxx alpha-beta-gamma 8-bit-thing\n"
|
|
"xxxxxxxxxxxxxxxxxxxxx (\\%discretionary)\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" xxxxxxxxxxxxxxxxxxxxxxxxx alpha-\n"
|
|
" beta-gamma 8-bit-thing\n"
|
|
" xxxxxxxxxxxxxxxxxxxxx (-\n"
|
|
" discretionary)\n"
|
|
"\n"
|
|
" T(1)\n",
|
|
},
|
|
{
|
|
"blank",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"para one\n"
|
|
"\n"
|
|
"para two\n"
|
|
".PP\n"
|
|
".PP\n"
|
|
"after two PP\n"
|
|
".sp 2\n"
|
|
"after sp 2\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" para one\n"
|
|
"\n"
|
|
" para two\n"
|
|
"\n"
|
|
" after two PP\n"
|
|
"\n"
|
|
"\n"
|
|
" after sp 2\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tbl",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"tab(;);\n"
|
|
"l r c.\n"
|
|
"left;right;center\n"
|
|
"longer text;2;c\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" left right center\n"
|
|
" longer text 2 c\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblbox",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"box center tab(;);\n"
|
|
"l | n.\n"
|
|
"Name;Value\n"
|
|
"_\n"
|
|
"alpha;1.5\n"
|
|
"beta;10\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" Name \xe2\x94\x82"" Value \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" alpha \xe2\x94\x82"" 1.5 \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" beta \xe2\x94\x82"" 10 \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblblock",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"allbox tab(:);\n"
|
|
"lB l.\n"
|
|
"T{\n"
|
|
"Header\n"
|
|
"T}:T{\n"
|
|
"A longer text block that will need to wrap within its column\n"
|
|
"T}\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" Header \xe2\x94\x82"" A longer text \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" block that \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" will need to \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" wrap within \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" its column \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblexpand",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"allbox;\n"
|
|
"lbx lb\n"
|
|
"l l.\n"
|
|
"Interface\tValue\n"
|
|
"T{\n"
|
|
".BR open (),\n"
|
|
".BR openat (),\n"
|
|
".BR creat ()\n"
|
|
"T}\tMT-Safe\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"lx l lx.\n"
|
|
"a\tbb\tc\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" Interface \xe2\x94\x82"" Value \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" open(), openat(), \xe2\x94\x82"" MT-Safe \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" creat() \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
" a bb c\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblxblock",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"allbox;\n"
|
|
"lbx lb\n"
|
|
"l l.\n"
|
|
"Interface\tValue\n"
|
|
"T{\n"
|
|
".BR cos (),\n"
|
|
".BR sin ()\n"
|
|
"T}\tMT-Safe\n"
|
|
"\\fBpthread_setname_np\\fP()\tMT-Safe\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"lx lx l.\n"
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaa\tb\tc\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" Interface \xe2\x94\x82"" Value \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" cos(), sin() \xe2\x94\x82"" MT-Safe \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" pthread_setname_np() \xe2\x94\x82"" MT-Safe \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
" aaaaaaaaaaaaaaaaaaaaaaaaaa b c\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblspan",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"tab(;);\n"
|
|
"l s s\n"
|
|
"l l l.\n"
|
|
"Spanning entry text;\n"
|
|
"a;b;c\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"allbox tab(;);\n"
|
|
"l s\n"
|
|
"lx l.\n"
|
|
"T{\n"
|
|
"A spanning block over an expanding column\n"
|
|
"T}\n"
|
|
"ab;cd\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" Spanning entry text\n"
|
|
" a b c\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" A spanning block over an \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" expanding column \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" ab \xe2\x94\x82"" cd \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblvspan",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"allbox tab(;);\n"
|
|
"l l l.\n"
|
|
"a;T{\n"
|
|
"one two three four five six\n"
|
|
"T};top\n"
|
|
"\\^;c;d\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" one two \xe2\x94\x82"" top \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" three four \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" a \xe2\x94\x82"" five six \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" c \xe2\x94\x82"" d \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblxspan",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"tab(;);\n"
|
|
"l lx s.\n"
|
|
"aa;bbbbbbbbbbbbbbbb\n"
|
|
"c;d\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"tab(;);\n"
|
|
"l s l.\n"
|
|
"ab;c\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"allbox tab(;);\n"
|
|
"l s s\n"
|
|
"n n n.\n"
|
|
"hhhhhhhhhhhhhhhhhhhhhh\n"
|
|
"1.5;22.25;3\n"
|
|
"10;2;333.1\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" aa bbbbbbbbbbbbbbbb\n"
|
|
" c d\n"
|
|
" ab c\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" hhhhhhhhhhhhhhhhhhhhhh \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" 1.5 \xe2\x94\x82"" 22.25 \xe2\x94\x82"" 3 \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" 10 \xe2\x94\x82"" 2 \xe2\x94\x82"" 333.1 \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblvspans",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"allbox tab(;);\n"
|
|
"c c s\n"
|
|
"c ^ s\n"
|
|
"l l l.\n"
|
|
"X;Head\n"
|
|
"Y\n"
|
|
"a;b;c\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"allbox tab(;);\n"
|
|
"c c\n"
|
|
"^ ^\n"
|
|
"l l.\n"
|
|
"Name;Value\n"
|
|
"\n"
|
|
"a;b\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" X \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4"" Head \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" Y \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" a \xe2\x94\x82"" b \xe2\x94\x82"" c \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" Name \xe2\x94\x82"" Value \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" a \xe2\x94\x82"" b \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblblockw",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"allbox;\n"
|
|
"lbw8 lb\n"
|
|
"l l.\n"
|
|
"Name\tValue\n"
|
|
"T{\n"
|
|
".BR foo (),\n"
|
|
".BR bar ()\n"
|
|
"T}\tMT-Safe\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"box;\n"
|
|
"c.\n"
|
|
"T{\n"
|
|
"alpha beta gamma delta epsilon zeta\n"
|
|
"T}\n"
|
|
"wwwwwwwwwwwwwwwwwwwwwww\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" Name \xe2\x94\x82"" Value \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" foo(), \xe2\x94\x82"" MT-Safe \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" bar() \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" alpha beta gamma delta \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" epsilon zeta \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" wwwwwwwwwwwwwwwwwwwwwww \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblfmtrule",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"text\n"
|
|
".TS\n"
|
|
"tab(;);\n"
|
|
"l c c l\n"
|
|
"____\n"
|
|
"lB c c l.\n"
|
|
"Signal;Std;Action;Comment\n"
|
|
".sp\n"
|
|
"SIGINT;P1990;Term;Interrupt\n"
|
|
"SIGHUP;P1990;Term;Hangup\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" text\n"
|
|
" Signal Std Action Comment\n"
|
|
"\n"
|
|
" \xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\n"
|
|
" SIGINT P1990 Term Interrupt\n"
|
|
" SIGHUP P1990 Term Hangup\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblallboxrule",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"text\n"
|
|
".TS\n"
|
|
"allbox tab(;);\n"
|
|
"l\n"
|
|
"^.\n"
|
|
"X\n"
|
|
"_\n"
|
|
"\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"allbox tab(;);\n"
|
|
"l l.\n"
|
|
"a;b\n"
|
|
".sp\n"
|
|
"_\n"
|
|
"c;d\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" text\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" X \xe2\x94\x82""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" a \xe2\x94\x82"" b \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" \xe2\x94\x82"" \xe2\x94\x82""\n"
|
|
" \xe2\x94\x9c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xbc""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xa4""\n"
|
|
" \xe2\x94\x82"" c \xe2\x94\x82"" d \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblnospace",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".TS\n"
|
|
"tab(;);\n"
|
|
"l l l.\n"
|
|
"a;b;c\n"
|
|
"\\^;\\^;\\^\n"
|
|
"\\^;\\^;\\^\n"
|
|
"d;e;f\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" a b c\n"
|
|
" d e f\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"tblw0",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"text\n"
|
|
".TS\n"
|
|
"tab(;);\n"
|
|
"lw(0) l.\n"
|
|
"T{\n"
|
|
"alpha beta gamma\n"
|
|
"T};x\n"
|
|
".TE\n"
|
|
".TS\n"
|
|
"allbox tab(;);\n"
|
|
"l lw(0) l.\n"
|
|
"a;;b\n"
|
|
".TE\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" text\n"
|
|
" alpha x\n"
|
|
" beta\n"
|
|
" gamma\n"
|
|
" \xe2\x94\x8c""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xac""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x90""\n"
|
|
" \xe2\x94\x82"" a \xe2\x94\x82"" \xe2\x94\x82"" b \xe2\x94\x82""\n"
|
|
" \xe2\x94\x94""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\xb4""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x80""\xe2\x94\x98""\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"example",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"text\n"
|
|
".EX\n"
|
|
"code here\n"
|
|
".EE\n"
|
|
"after\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" text\n"
|
|
" code here\n"
|
|
" after\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"spaces",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
"trailing spaces\n"
|
|
"next\n"
|
|
" leading spaces\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" trailing spaces next\n"
|
|
" leading spaces\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
{
|
|
"pod",
|
|
".de Vb\n"
|
|
".ft CW\n"
|
|
".nf\n"
|
|
".ne \\\\$1\n"
|
|
"..\n"
|
|
".de Ve\n"
|
|
".ft R\n"
|
|
".fi\n"
|
|
"..\n"
|
|
".ie n \\{\\\n"
|
|
". ds C` \"\"\n"
|
|
". ds C' \"\"\n"
|
|
"'br\\}\n"
|
|
".el\\{\\\n"
|
|
". ds C`\n"
|
|
". ds C'\n"
|
|
"'br\\}\n"
|
|
".de IX\n"
|
|
"..\n"
|
|
".TH POD 1 2026-01-01 pod\n"
|
|
".SH NAME\n"
|
|
"pod \\- generated\n"
|
|
".SH OPTIONS\n"
|
|
".IP \"\\fB\\-o\\fR \\fIfile\\fR\" 4\n"
|
|
".IX Item \"-o file\"\n"
|
|
"Write to \\f(CW\\*(C`file\\*(C'\\fR.\n"
|
|
".Vb 1\n"
|
|
"\\& verbatim text\n"
|
|
".Ve\n",
|
|
"POD(1) General Commands Manual POD(1)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" pod - generated\n"
|
|
"\n"
|
|
"OPTIONS\n"
|
|
" -o file\n"
|
|
" Write to \"file\".\n"
|
|
" verbatim text\n"
|
|
"\n"
|
|
"pod 2026-01-01 POD(1)\n",
|
|
},
|
|
{
|
|
"mdoc",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh NAME\n"
|
|
".Nm test\n"
|
|
".Nd a test page\n"
|
|
".Sh SYNOPSIS\n"
|
|
".Nm\n"
|
|
".Op Fl abc\n"
|
|
".Op Fl o Ar file\n"
|
|
".Ar input ...\n"
|
|
".Sh DESCRIPTION\n"
|
|
"The\n"
|
|
".Nm\n"
|
|
"utility does\n"
|
|
".Dq things .\n"
|
|
"See\n"
|
|
".Xr ls 1 ,\n"
|
|
".Xr cp 1\n"
|
|
"and\n"
|
|
".Pa /tmp .\n"
|
|
".Bl -tag -width Ds\n"
|
|
".It Fl a\n"
|
|
"all\n"
|
|
".It Fl verylongflag\n"
|
|
"long\n"
|
|
".El\n"
|
|
".Pp\n"
|
|
".Bl -bullet -compact\n"
|
|
".It\n"
|
|
"one\n"
|
|
".It\n"
|
|
"two\n"
|
|
".El\n"
|
|
".Sh AUTHORS\n"
|
|
".An Al Author Aq Mt al@example.com\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" test \xe2\x80\x93"" a test page\n"
|
|
"\n"
|
|
"SYNOPSIS\n"
|
|
" test [-abc] [-o file] input ...\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" The test utility does \xe2\x80\x9c""things\xe2\x80\x9d"".\n"
|
|
" See ls(1), cp(1) and /tmp.\n"
|
|
"\n"
|
|
" -a all\n"
|
|
"\n"
|
|
" -verylongflag\n"
|
|
" long\n"
|
|
"\n"
|
|
" \xe2\x80\xa2"" one\n"
|
|
" \xe2\x80\xa2"" two\n"
|
|
"\n"
|
|
"AUTHORS\n"
|
|
" Al Author <al@example.com>\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdocfn",
|
|
".Dd January 1, 2026\n"
|
|
".Dt LIB 3\n"
|
|
".Os W64\n"
|
|
".Sh NAME\n"
|
|
".Nm f\n"
|
|
".Nd function\n"
|
|
".Sh SYNOPSIS\n"
|
|
".In stdio.h\n"
|
|
".Ft int\n"
|
|
".Fn func \"const char *name\" \"int flags\"\n"
|
|
".Sh DESCRIPTION\n"
|
|
".Fn func\n"
|
|
"returns\n"
|
|
".Dv NULL .\n"
|
|
".Bd -literal -offset indent\n"
|
|
"literal\ttab\n"
|
|
".Ed\n",
|
|
"LIB(3) Library Functions Manual LIB(3)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" f \xe2\x80\x93"" function\n"
|
|
"\n"
|
|
"SYNOPSIS\n"
|
|
" #include <stdio.h>\n"
|
|
"\n"
|
|
" int\n"
|
|
" func(const char *name, int flags);\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" func() returns NULL.\n"
|
|
"\n"
|
|
" literal tab\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdoccolumn",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh DESCRIPTION\n"
|
|
"Columns:\n"
|
|
".Bl -column Ar Value\n"
|
|
".It Sy Key Ta Sy Value\n"
|
|
".It one\ttwo\n"
|
|
".It Fl x Ta a long last cell that wraps to its own column\n"
|
|
".El\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" Columns:\n"
|
|
"\n"
|
|
" Key Value\n"
|
|
" one two\n"
|
|
" -x a long last cell that wraps\n"
|
|
" to its own column\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdocspacing",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh DESCRIPTION\n"
|
|
".Fl L\n"
|
|
".Sm off\n"
|
|
".Oo Ar addr : Oc\n"
|
|
".Ar port\n"
|
|
".Sm on\n"
|
|
"and\n"
|
|
".Ar x Ns\n"
|
|
".Tg x\n"
|
|
"y\n"
|
|
".Tg\n"
|
|
"z\n"
|
|
".Ql \",\"\n"
|
|
".Dq \"Ar\" .\n"
|
|
".Bl -inset\n"
|
|
".It Inset head\n"
|
|
"body\n"
|
|
".El\n"
|
|
".Bl -diag\n"
|
|
".It Diag head\n"
|
|
"body\n"
|
|
".El\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" -L [addr:]port and xy z \xe2\x80\x98"",\xe2\x80\x99"" \xe2\x80\x9c""Ar\xe2\x80\x9d"".\n"
|
|
"\n"
|
|
" Inset head body\n"
|
|
"\n"
|
|
" Diag head body\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdocrefs",
|
|
".Dd January 1, 2026\n"
|
|
".Dt LIB 3\n"
|
|
".Os W64\n"
|
|
".Sh LIBRARY\n"
|
|
".Lb libc\n"
|
|
".Lb libnone\n"
|
|
".Sh SYNOPSIS\n"
|
|
".In unistd.h\n"
|
|
".Vt extern char *optarg;\n"
|
|
".Vt extern int optind;\n"
|
|
".Ft int\n"
|
|
".Fn getopt\n"
|
|
".Sh DESCRIPTION\n"
|
|
"See\n"
|
|
".In stdio.h .\n"
|
|
".Sh SEE ALSO\n"
|
|
".Xr ls 1\n"
|
|
".Rs\n"
|
|
".%T The C Book\n"
|
|
".%A Al\n"
|
|
".%A Bo\n"
|
|
".%I Pub\n"
|
|
".%D 1988\n"
|
|
".Re\n"
|
|
".Rs\n"
|
|
".%A Al\n"
|
|
".%A Bo\n"
|
|
".%A Cy\n"
|
|
".%T Report\n"
|
|
".%J Journal\n"
|
|
".Re\n",
|
|
"LIB(3) Library Functions Manual LIB(3)\n"
|
|
"\n"
|
|
"LIBRARY\n"
|
|
" Standard C Library (libc, -lc)\n"
|
|
" library \xe2\x80\x9c""libnone\xe2\x80\x9d""\n"
|
|
"\n"
|
|
"SYNOPSIS\n"
|
|
" #include <unistd.h>\n"
|
|
"\n"
|
|
" extern char *optarg;\n"
|
|
" extern int optind;\n"
|
|
"\n"
|
|
" int\n"
|
|
" getopt();\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" See <stdio.h>.\n"
|
|
"\n"
|
|
"SEE ALSO\n"
|
|
" ls(1)\n"
|
|
"\n"
|
|
" Al and Bo, The C Book, Pub, 1988.\n"
|
|
"\n"
|
|
" Al, Bo, and Cy, \xe2\x80\x9c""Report\xe2\x80\x9d"", Journal.\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdoccells",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh DESCRIPTION\n"
|
|
".Bl -column xxxx yyyy -compact\n"
|
|
".It a\t Sy b\tc\n"
|
|
".It a\tTa b\n"
|
|
".It a Ta\tb\n"
|
|
".It Sy x Ta \"Ar\" y Ta z\n"
|
|
".El\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" a b c\n"
|
|
" a Ta b\n"
|
|
" a Ta b\n"
|
|
" x Ar y z\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdocwords",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh DESCRIPTION\n"
|
|
".Ar foo \"\"\n"
|
|
"bar\n"
|
|
".No \"\"\n"
|
|
"M\n"
|
|
".Bl -tag -width Ds\n"
|
|
".It Fl e\n"
|
|
".Sm on\n"
|
|
".Ar x\n"
|
|
".El\n"
|
|
".Sh SEE ALSO\n"
|
|
"Text first.\n"
|
|
".Bd -filled -compact\n"
|
|
".Rs\n"
|
|
".%A\n"
|
|
".%A Two\n"
|
|
".%T T2\n"
|
|
".Re\n"
|
|
".Ed\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" foo bar M\n"
|
|
"\n"
|
|
" -e x\n"
|
|
"\n"
|
|
"SEE ALSO\n"
|
|
" Text first.\n"
|
|
" Two, T2.\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdocsynnm",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh SYNOPSIS\n"
|
|
".Nm foo\n"
|
|
".In stdio.h\n"
|
|
".Nm bar\n"
|
|
".Fl b\n"
|
|
".Pp\n"
|
|
".Ft int\n"
|
|
".Fn f \"int a\"\n"
|
|
".Nm baz\n"
|
|
".Vt int x;\n"
|
|
".Nm qux\n"
|
|
".Fn g\n"
|
|
".Nm end\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"SYNOPSIS\n"
|
|
" foo #include <stdio.h>\n"
|
|
" bar -b\n"
|
|
"\n"
|
|
" int\n"
|
|
" f(int a);\n"
|
|
"\n"
|
|
" baz int x;\n"
|
|
" qux g();\n"
|
|
"\n"
|
|
" end\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdoctitles",
|
|
".Dd January 2, 2026\n"
|
|
".Dt T 1\n"
|
|
".Os a b c d e\n"
|
|
".Sh NAME\n"
|
|
".Nm t\n"
|
|
".Nd test\n"
|
|
".Bd -offset 20n\n"
|
|
"x y\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"NAME\n"
|
|
" t \xe2\x80\x93 test\n"
|
|
"\n"
|
|
" x y\n"
|
|
"\n"
|
|
"a b c d e January 2, 2026 a b c d e\n",
|
|
},
|
|
{
|
|
"mdocgaps",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh DESCRIPTION\n"
|
|
".Ar a\\h'1n' Ns\n"
|
|
".Sm on\n"
|
|
".Ar b\n"
|
|
".Sm off\n"
|
|
".Ar c\\h'2n'\n"
|
|
".Sm on\n"
|
|
".Ar d\n"
|
|
".Ar x \"\" y\n"
|
|
".Bd -literal\n"
|
|
".Ar \"e \" Ns\n"
|
|
".Sm on\n"
|
|
".Ar f\n"
|
|
".Ed\n"
|
|
".Bl -inset\n"
|
|
".It Fl g\n"
|
|
".Sm on\n"
|
|
".Ar h\n"
|
|
".It Fl i\n"
|
|
".Ns\n"
|
|
".Ar j\n"
|
|
".It Fl aaaa Ar bbbbbbbbbbbbbbbbbbbbbbbbbb\n"
|
|
"cccc\n"
|
|
".El\n"
|
|
".Bl -diag\n"
|
|
".It\n"
|
|
"k\n"
|
|
".El\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" a b c d x y\n"
|
|
"\n"
|
|
" e\n"
|
|
" f\n"
|
|
"\n"
|
|
" -g h\n"
|
|
"\n"
|
|
" -i j\n"
|
|
"\n"
|
|
" -aaaa\n"
|
|
" bbbbbbbbbbbbbbbbbbbbbbbbbb cccc\n"
|
|
"\n"
|
|
" k\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdocitems",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh SYNOPSIS\n"
|
|
".In a.h\n"
|
|
".Op Ar x\n"
|
|
".In b.h\n"
|
|
"text\n"
|
|
".Sh DESCRIPTION\n"
|
|
".Bl -column xxxx yyyy -compact\n"
|
|
".It a Sy\tb\tc\n"
|
|
".It Fl x Ar\tb\tc\n"
|
|
".It Ar x \"Ta\" y Ta z\n"
|
|
".It a \"Ta\" b Ta z\n"
|
|
".It a Ta b \"\"\n"
|
|
"more\n"
|
|
".It a Ta \"b \"\n"
|
|
"more\n"
|
|
".El\n"
|
|
".Sh SEE ALSO\n"
|
|
".Bl -tag -width Ds\n"
|
|
".It x\n"
|
|
"Text.\n"
|
|
".Rs\n"
|
|
".%A One\n"
|
|
".Re\n"
|
|
".It y\n"
|
|
".Rs\n"
|
|
".%A Two\n"
|
|
".Re\n"
|
|
".Rs\n"
|
|
".%A Three\n"
|
|
".Re\n"
|
|
".El\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"SYNOPSIS\n"
|
|
" #include <a.h> [x]\n"
|
|
" #include <b.h> text\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" a Sy b c\n"
|
|
" -x -Ar b c\n"
|
|
" x y z\n"
|
|
" a Ta b z\n"
|
|
" a b more\n"
|
|
" a b more\n"
|
|
"\n"
|
|
"SEE ALSO\n"
|
|
" x Text.\n"
|
|
"\n"
|
|
" One.\n"
|
|
"\n"
|
|
" y Two.\n"
|
|
"\n"
|
|
" Three.\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"mdocempty",
|
|
".Dd January 1, 2026\n"
|
|
".Dt TEST 1\n"
|
|
".Os W64\n"
|
|
".Sh DESCRIPTION\n"
|
|
".Xr\n"
|
|
".Pp\n"
|
|
".Lk\n"
|
|
"text\n"
|
|
".Fn\n"
|
|
".Fo\n"
|
|
".Fa a\n"
|
|
".Fc\n"
|
|
".Ar b\n"
|
|
".Ns\n"
|
|
"more\n"
|
|
".Pp\n"
|
|
".Ar xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \"\" yyyy\n"
|
|
".Bl -column xxxx yyyy -compact\n"
|
|
".It Op x \"Ta\" y Ta z\n"
|
|
".El\n"
|
|
".Bl -tag -width 4n\n"
|
|
".It\n"
|
|
".Ar z\n"
|
|
".El\n"
|
|
".Bl -diag\n"
|
|
".It\n"
|
|
".It x\n"
|
|
"body\n"
|
|
".El\n",
|
|
"TEST(1) General Commands Manual TEST(1)\n"
|
|
"\n"
|
|
"DESCRIPTION\n"
|
|
" text(a) b more\n"
|
|
"\n"
|
|
" xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"
|
|
" yyyy\n"
|
|
" [x Ta y]\n"
|
|
" z\n"
|
|
"\n"
|
|
" z\n"
|
|
"\n"
|
|
"\n"
|
|
" x body\n"
|
|
"\n"
|
|
"W64 January 1, 2026 W64\n",
|
|
},
|
|
{
|
|
"edges",
|
|
".TH T 1 2026-01-01 src\n"
|
|
".SH A\n"
|
|
".nf\n"
|
|
"abcdefghijklmnopqrstuvwxyz0123456789\n"
|
|
".tl |\\h'-100n'|||\n"
|
|
".fi\n"
|
|
"x\\[u89ABCDEF]y \\[u1F600]\n"
|
|
".ds a b\n"
|
|
".ds b c\n"
|
|
"\\*[\\*[a]] \\n[\\*[a]]\n"
|
|
".nr c 7\n"
|
|
"\\n[\\*[b]]\n"
|
|
"a\\\n"
|
|
"b\\\\\n"
|
|
"c\\\\\\\n"
|
|
"d\n",
|
|
"T(1) General Commands Manual T(1)\n"
|
|
"\n"
|
|
"A\n"
|
|
" abcdefghijklmnopqrstuvwxyz0123456789\n"
|
|
"\n"
|
|
" xy \xf0\x9f\x98\x80"" c 0 7 ab\\ c\\d\n"
|
|
"\n"
|
|
"src 2026-01-01 T(1)\n",
|
|
},
|
|
};
|
|
|
|
iz cap = (iz)1<<24;
|
|
byte *mem = malloc(touz(cap));
|
|
Plt *plt = calloc(1, sizeof(*plt));
|
|
i32 fails = 0;
|
|
i32 count = 0;
|
|
|
|
char *opts[] = {"-Tutf8", "-rLL=40n"};
|
|
for (i32 i = 0; i < countof(cases); i++, count++) {
|
|
render(plt, mem, cap, cases[i].input, opts, countof(opts));
|
|
fails += !expect(cases+i, plt);
|
|
}
|
|
|
|
// Emphasis: bold and underline, each line closing its own SGR
|
|
count++;
|
|
plt->sgr = 1;
|
|
render(plt, mem, cap, ".SH A\n\\fBb\\fR \\fIi\\fR\n", opts, countof(opts));
|
|
Case sgr = {
|
|
"sgr", 0,
|
|
"\x1b[1mA\x1b[22m\n"
|
|
" \x1b[1mb\x1b[22m \x1b[4mi\x1b[24m\n"
|
|
};
|
|
fails += !expect(&sgr, plt);
|
|
plt->sgr = 0;
|
|
|
|
// ASCII output substitutes special characters
|
|
count++;
|
|
char *ascii[] = {"-Tascii", "-rLL=40n"};
|
|
render(plt, mem, cap, "\\(em \\(lq\\(rq \\(co \\[u00E9] \xc3\xa9\n", ascii, countof(ascii));
|
|
Case asc = {"ascii", 0, " -- \"\" (C) e e\n"};
|
|
fails += !expect(&asc, plt);
|
|
|
|
// Runaway recursion terminates
|
|
count++;
|
|
render(plt, mem, cap, ".de A\n.A\n..\n.A\nok\n", opts, countof(opts));
|
|
Case rec = {"recursion", 0, " ok\n"};
|
|
fails += !expect(&rec, plt);
|
|
|
|
// Out of memory is reported, not a crash
|
|
count++;
|
|
i32 r = render(plt, mem, 1<<16, ".TH T 1\n.SH A\ntext\n", opts, countof(opts));
|
|
if (r != -1) {
|
|
fprintf(stderr, "FAIL oom: status %d\n", r);
|
|
fails++;
|
|
}
|
|
|
|
// Unknown option
|
|
count++;
|
|
char *bad[] = {"-q"};
|
|
r = render(plt, mem, cap, "", bad, countof(bad));
|
|
if (r != 2) {
|
|
fprintf(stderr, "FAIL option: status %d\n", r);
|
|
fails++;
|
|
}
|
|
|
|
// A failed write (output past 64 KiB) is quiet but sets the status
|
|
count++;
|
|
r = render(plt, mem, cap,
|
|
".ds a x x x x\n"
|
|
".as a \\*a\n.as a \\*a\n.as a \\*a\n.as a \\*a\n.as a \\*a\n"
|
|
".as a \\*a\n.as a \\*a\n.as a \\*a\n.as a \\*a\n.as a \\*a\n"
|
|
".as a \\*a\n.as a \\*a\n.as a \\*a\n.as a \\*a\n"
|
|
"\\*a\n", opts, countof(opts));
|
|
if (r != 1) {
|
|
fprintf(stderr, "FAIL write: status %d\n", r);
|
|
fails++;
|
|
}
|
|
|
|
printf("%d tests, %d failed\n", count, fails);
|
|
return fails != 0;
|
|
}
|
|
|
|
|
|
#elif __AFL_COMPILER
|
|
// Fuzz the whole formatter: arbitrary input must neither crash nor
|
|
// hang. A small arena makes pathological inputs fail fast.
|
|
// $ afl-gcc-fast -g3 -fsanitize=undefined -fsanitize-trap nroff.c
|
|
// $ afl-fuzz -i corpus -o results ./a.out
|
|
#include <setjmp.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
__AFL_FUZZ_INIT();
|
|
|
|
struct Plt {
|
|
Str input;
|
|
jmp_buf oom;
|
|
};
|
|
|
|
static Str plt_load(Plt *plt, Str path, Arena *a)
|
|
{
|
|
return path.data ? (Str){0} : clone(a, plt->input);
|
|
}
|
|
|
|
static b32 plt_write(Plt *plt, i32 fd, u8 *buf, i32 len)
|
|
{
|
|
(void)plt;
|
|
(void)fd;
|
|
(void)buf;
|
|
(void)len;
|
|
return 1;
|
|
}
|
|
|
|
static Str plt_getenv(Plt *plt, Str name, Arena *a)
|
|
{
|
|
(void)plt;
|
|
(void)name;
|
|
(void)a;
|
|
return (Str){0};
|
|
}
|
|
|
|
static b32 plt_sgr(Plt *plt)
|
|
{
|
|
(void)plt;
|
|
return 1;
|
|
}
|
|
|
|
static void plt_oom(Plt *plt)
|
|
{
|
|
longjmp(plt->oom, 1);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
__AFL_INIT();
|
|
iz cap = (iz)1<<24;
|
|
byte *mem = malloc(touz(cap));
|
|
u8 *buf = __AFL_FUZZ_TESTCASE_BUF;
|
|
u8 *src = 0;
|
|
static Plt plt;
|
|
while (__AFL_LOOP(10000)) {
|
|
i32 len = __AFL_FUZZ_TESTCASE_LEN;
|
|
src = realloc(src, touz(len));
|
|
memcpy(src, buf, touz(len));
|
|
plt.input = (Str){src, len};
|
|
char *argv[] = {"nroff", "-Tutf8", "-w", 0};
|
|
Arena a = {&plt, mem, mem+cap};
|
|
if (!setjmp(plt.oom)) {
|
|
nroff(3, (u8 **)argv, &plt, a);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#elif _WIN32
|
|
typedef uint16_t c16;
|
|
|
|
enum {
|
|
CP_UTF8 = 65001,
|
|
GENERIC_READ = (i32)0x80000000,
|
|
FILE_SHARE_ALL = 7,
|
|
OPEN_EXISTING = 3,
|
|
MEM_COMMIT = 0x1000,
|
|
MEM_RESERVE = 0x2000,
|
|
PAGE_READWRITE = 4,
|
|
VT_PROCESSING = 4,
|
|
ERROR_BROKEN_PIPE = 109,
|
|
ERROR_ENVVAR_NOT_FOUND = 203,
|
|
};
|
|
|
|
#define W32(r) __declspec(dllimport) r __stdcall
|
|
W32(c16 **) CommandLineToArgvW(c16 *, i32 *);
|
|
W32(b32) CloseHandle(uz);
|
|
W32(uz) CreateFileW(c16 *, i32, i32, uz, i32, i32, uz);
|
|
W32(void) ExitProcess(i32);
|
|
W32(c16 *) GetCommandLineW(void);
|
|
W32(b32) GetConsoleMode(uz, i32 *);
|
|
W32(i32) GetEnvironmentVariableW(c16 *, c16 *, i32);
|
|
W32(i32) GetLastError(void);
|
|
W32(uz) GetStdHandle(i32);
|
|
W32(i32) MultiByteToWideChar(i32, i32, u8 *, i32, c16 *, i32);
|
|
W32(b32) ReadFile(uz, u8 *, i32, i32 *, uz);
|
|
W32(b32) SetConsoleMode(uz, i32);
|
|
W32(byte *) VirtualAlloc(uz, iz, i32, i32);
|
|
W32(i32) WideCharToMultiByte(i32, i32, c16 *, i32, u8 *, i32, uz, uz);
|
|
W32(b32) WriteConsoleW(uz, c16 *, i32, i32 *, uz);
|
|
W32(b32) WriteFile(uz, u8 *, i32, i32 *, uz);
|
|
|
|
struct Plt {
|
|
uz out[3];
|
|
b32 console[3];
|
|
b32 sgr;
|
|
u8 pending[3][4]; // incomplete UTF-8 sequence per console
|
|
i32 npending[3];
|
|
u8 utf8[1<<12];
|
|
c16 wide[1<<12]; // at most one unit per UTF-8 byte
|
|
};
|
|
|
|
static i32 trunc32(iz n)
|
|
{
|
|
return n>maxof(i32) ? maxof(i32) : (i32)n;
|
|
}
|
|
|
|
static c16 *towide(Str s, Arena *a)
|
|
{
|
|
i32 len = trunc32(s.len);
|
|
i32 n = MultiByteToWideChar(CP_UTF8, 0, s.data, len, 0, 0);
|
|
c16 *w = new(a, n+1, c16);
|
|
MultiByteToWideChar(CP_UTF8, 0, s.data, len, w, n);
|
|
return w;
|
|
}
|
|
|
|
static Str plt_load(Plt *plt, Str path, Arena *a)
|
|
{
|
|
(void)plt;
|
|
uz h = GetStdHandle(-10);
|
|
b32 close = 0;
|
|
if (path.data) {
|
|
Arena scratch = *a;
|
|
c16 *wpath = towide(path, &scratch);
|
|
h = CreateFileW(wpath, GENERIC_READ, FILE_SHARE_ALL, 0, OPEN_EXISTING, 0, 0);
|
|
if (h == (uz)-1) {
|
|
return (Str){0};
|
|
}
|
|
close = 1;
|
|
}
|
|
|
|
// Read into the free space of the arena, then commit
|
|
Str r = {0};
|
|
r.data = (u8 *)a->beg;
|
|
iz avail = a->end - a->beg;
|
|
for (;;) {
|
|
i32 want = trunc32(avail - r.len);
|
|
if (want > 1<<20) want = 1<<20;
|
|
if (want <= 0) {
|
|
plt_oom(plt);
|
|
}
|
|
i32 got = 0;
|
|
if (!ReadFile(h, r.data+r.len, want, &got, 0)) {
|
|
if (GetLastError() == ERROR_BROKEN_PIPE) break; // pipe EOF
|
|
if (close) CloseHandle(h);
|
|
return (Str){0};
|
|
} else if (!got) {
|
|
break;
|
|
}
|
|
r.len += got;
|
|
}
|
|
if (close) {
|
|
CloseHandle(h);
|
|
}
|
|
a->beg += r.len;
|
|
return r;
|
|
}
|
|
|
|
static b32 plt_write(Plt *plt, i32 fd, u8 *buf, i32 len)
|
|
{
|
|
uz h = plt->out[fd];
|
|
if (!plt->console[fd]) {
|
|
for (i32 off = 0; off < len;) {
|
|
i32 n = 0;
|
|
if (!WriteFile(h, buf+off, len-off, &n, 0) || !n) {
|
|
return 0;
|
|
}
|
|
off += n;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// Console: convert complete UTF-8 sequences to UTF-16
|
|
u8 *pending = plt->pending[fd];
|
|
while (len > 0) {
|
|
u8 *tmp = plt->utf8;
|
|
i32 n = 0;
|
|
for (; n < plt->npending[fd]; n++) {
|
|
tmp[n] = pending[n];
|
|
}
|
|
plt->npending[fd] = 0;
|
|
i32 cap = (i32)countof(plt->utf8);
|
|
i32 take = len < cap-n ? len : cap-n;
|
|
xcpy(tmp+n, buf, touz(take));
|
|
n += take;
|
|
buf += take;
|
|
len -= take;
|
|
|
|
// Hold back an incomplete trailing sequence
|
|
i32 end = n;
|
|
i32 back = 0;
|
|
for (; back<3 && end-back-1>=0 && (tmp[end-back-1]&0xc0)==0x80; back++) {}
|
|
if (end-back-1 >= 0) {
|
|
u8 lead = tmp[end-back-1];
|
|
i32 need = lead>=0xf0 ? 4 : lead>=0xe0 ? 3 : lead>=0xc0 ? 2 : 1;
|
|
if (need > back+1) {
|
|
i32 hold = back + 1;
|
|
for (i32 i = 0; i < hold; i++) {
|
|
pending[i] = tmp[end-hold+i];
|
|
}
|
|
plt->npending[fd] = hold;
|
|
end -= hold;
|
|
}
|
|
}
|
|
i32 wlen = MultiByteToWideChar(CP_UTF8, 0, tmp, end, plt->wide,
|
|
(i32)countof(plt->wide));
|
|
for (i32 off = 0; off < wlen;) {
|
|
i32 w = 0;
|
|
if (!WriteConsoleW(h, plt->wide+off, wlen-off, &w, 0) || !w) {
|
|
return 0;
|
|
}
|
|
off += w;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static Str plt_getenv(Plt *plt, Str name, Arena *a)
|
|
{
|
|
Arena scratch = *a;
|
|
c16 *wname = towide(name, &scratch);
|
|
c16 *value = plt->wide; // free outside plt_write
|
|
i32 len = GetEnvironmentVariableW(wname, value, (i32)countof(plt->wide));
|
|
if (!len) {
|
|
// Distinguish an empty variable from an unset one
|
|
return GetLastError()==ERROR_ENVVAR_NOT_FOUND ? (Str){0} : S("");
|
|
} else if (len >= (i32)countof(plt->wide)) {
|
|
return (Str){0};
|
|
}
|
|
i32 n = WideCharToMultiByte(CP_UTF8, 0, value, len, 0, 0, 0, 0);
|
|
Str r = {new(a, n, u8), n};
|
|
WideCharToMultiByte(CP_UTF8, 0, value, len, r.data, n, 0, 0);
|
|
return r;
|
|
}
|
|
|
|
static b32 plt_sgr(Plt *plt)
|
|
{
|
|
return plt->sgr;
|
|
}
|
|
|
|
static void plt_oom(Plt *plt)
|
|
{
|
|
static u8 msg[] = "nroff: out of memory\n";
|
|
plt->console[2] = 0;
|
|
plt_write(plt, 2, msg, (i32)countof(msg)-1);
|
|
ExitProcess(2);
|
|
affirm(0);
|
|
}
|
|
|
|
void __stdcall mainCRTStartup(void)
|
|
{
|
|
static Plt plt;
|
|
for (i32 fd = 0; fd < 3; fd++) {
|
|
i32 mode;
|
|
plt.out[fd] = GetStdHandle(-10 - fd);
|
|
plt.console[fd] = fd && GetConsoleMode(plt.out[fd], &mode);
|
|
}
|
|
plt.sgr = 1;
|
|
i32 mode;
|
|
if (GetConsoleMode(plt.out[1], &mode)) {
|
|
plt.sgr = (mode&VT_PROCESSING) || SetConsoleMode(plt.out[1], mode|VT_PROCESSING);
|
|
}
|
|
|
|
iz cap = (iz)1 << (sizeof(void *)==8 ? 28 : 27);
|
|
byte *mem = VirtualAlloc(0, cap, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
|
|
if (!mem) {
|
|
cap = (iz)1 << 25;
|
|
mem = VirtualAlloc(0, cap, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
|
|
if (!mem) {
|
|
plt_oom(&plt);
|
|
}
|
|
}
|
|
Arena a = {&plt, mem, mem+cap};
|
|
|
|
i32 argc = 0;
|
|
c16 **wargv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
|
u8 **argv = new(&a, argc+1, u8 *);
|
|
for (i32 i = 0; i < argc; i++) {
|
|
i32 len = WideCharToMultiByte(CP_UTF8, 0, wargv[i], -1, 0, 0, 0, 0);
|
|
argv[i] = new(&a, len, u8);
|
|
WideCharToMultiByte(CP_UTF8, 0, wargv[i], -1, argv[i], len, 0, 0);
|
|
}
|
|
|
|
i32 r = nroff(argc, argv, &plt, a);
|
|
ExitProcess(r);
|
|
affirm(0);
|
|
}
|
|
|
|
|
|
#else // POSIX
|
|
#include <fcntl.h>
|
|
#include <stdlib.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
struct Plt {
|
|
i32 unused;
|
|
};
|
|
|
|
static Str plt_load(Plt *plt, Str path, Arena *a)
|
|
{
|
|
i32 fd = 0;
|
|
if (path.data) {
|
|
Arena scratch = *a;
|
|
Str cpath = concat(&scratch, path, S("\0"));
|
|
fd = open((char *)cpath.data, O_RDONLY);
|
|
if (fd < 0) {
|
|
return (Str){0};
|
|
}
|
|
}
|
|
Str r = {0};
|
|
r.data = (u8 *)a->beg;
|
|
for (;;) {
|
|
iz avail = a->end - a->beg - r.len;
|
|
if (avail <= 0) {
|
|
plt_oom(plt);
|
|
}
|
|
iz want = avail<(1<<20) ? avail : (1<<20);
|
|
iz got = read(fd, r.data+r.len, touz(want));
|
|
if (got < 0) {
|
|
if (fd) close(fd);
|
|
return (Str){0}; // e.g. a directory
|
|
} else if (!got) {
|
|
break;
|
|
}
|
|
r.len += got;
|
|
}
|
|
if (fd) {
|
|
close(fd);
|
|
}
|
|
a->beg += r.len;
|
|
return r;
|
|
}
|
|
|
|
static b32 plt_write(Plt *plt, i32 fd, u8 *buf, i32 len)
|
|
{
|
|
(void)plt;
|
|
for (i32 off = 0; off < len;) {
|
|
iz n = write(fd, buf+off, touz(len-off));
|
|
if (n <= 0) {
|
|
return 0;
|
|
}
|
|
off += (i32)n;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static Str plt_getenv(Plt *plt, Str name, Arena *a)
|
|
{
|
|
(void)plt;
|
|
Arena scratch = *a;
|
|
Str cname = concat(&scratch, name, S("\0"));
|
|
char *v = getenv((char *)cname.data);
|
|
return v ? clone(a, import((u8 *)v)) : (Str){0};
|
|
}
|
|
|
|
static b32 plt_sgr(Plt *plt)
|
|
{
|
|
(void)plt;
|
|
return 1;
|
|
}
|
|
|
|
static void plt_oom(Plt *plt)
|
|
{
|
|
(void)plt;
|
|
static char msg[] = "nroff: out of memory\n";
|
|
write(2, msg, sizeof(msg)-1);
|
|
_exit(2);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
static Plt plt;
|
|
iz cap = (iz)1<<28;
|
|
byte *mem = mmap(0, touz(cap), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
|
|
if (mem == MAP_FAILED) {
|
|
plt_oom(&plt);
|
|
}
|
|
Arena a = {&plt, mem, mem+cap};
|
|
return nroff(argc, (u8 **)argv, &plt, a);
|
|
}
|
|
#endif
|