Re: groff performance and a critique of mandoc(1)'s PDF generation

Ingo Schwarze <[email protected]>
Newsgroups gmane.comp.printing.groff.general
Message-ID <[email protected]>
Hi Branden,

G. Branden Robinson wrote on Sun, Aug 09, 2026 at 03:35:57AM -0500:

> I look forward to seeing the new requests I've added since groff
> 1.22.4 show up.  ;-)
> 
> stringup
> stringdown

i just implemented these two, see the commit appended below.

With groff-1.24, i see one detail of behaviour that feels at least
somewhat ugly to me, arguably even buggy.

The .stringup request is documented to accept exactly one argument.
However, when i append additional bogus arguments, those sneak
through verbatim to the output stream, at the place of the request,
even though the request is not documented to produce output.

For example, with groff-1.24, the input

  .ds mynm content
  .stringup mynm trailing garbage
  \*[mynm]

produces this output for me:

  trailing garbage CONTENT

Groff prints no wraning or error message.

In mandoc, i chose to instead only print

  CONTENT

to standard output and the following to standard error output:

  mandoc: stringup_bad.in:7:16: ERROR: skipping excess arguments: \
          stringup ... trailing garbage

Yours,
  Ingo


Log Message:
-----------
groff-1.23.0 invented new roff(7) requests .stringup and .stringdown that
change the case of the named user-defined string in place.  I haven't seen
them used in any manual page yet and - like for all roff(7) requests - do
not recommend using them, but i chose to implement them anyway because that
was almost as trivial as it would have been to mark them as unsupported.

G. Branden Robinson reminded me of the new feature.

Modified Files:
--------------
    mandoc:
        roff.7
        roff.c
        roff.h
    mandoc/regress/roff/string:
        Makefile

Added Files:
-----------
    mandoc/regress/roff/string:
        stringup.in
        stringup.out_ascii
        stringup.out_utf8
        stringup_bad.in
        stringup_bad.out_ascii
        stringup_bad.out_lint

Revision Data
-------------
Index: roff.c
===================================================================
RCS file: /home/cvs/mandoc/mandoc/roff.c,v
diff -Lroff.c -Lroff.c -u -p -r1.405 -r1.406
--- roff.c
+++ roff.c
@@ -1,6 +1,6 @@
 /* $Id$ */
 /*
- * Copyright (c) 2010-2015, 2017-2025 Ingo Schwarze <[email protected]>
+ * Copyright (c) 2010-2015, 2017-2026 Ingo Schwarze <[email protected]>
  * Copyright (c) 2008-2012, 2014 Kristaps Dzonsons <[email protected]>
  *
  * Permission to use, copy, modify, and distribute this software for any
@@ -244,6 +244,7 @@ static	void		 roff_setstrn(struct roffkv
 				size_t, const char *, size_t, int);
 static	int		 roff_shift(ROFF_ARGS);
 static	int		 roff_so(ROFF_ARGS);
+static	int		 roff_stringup(ROFF_ARGS);
 static	int		 roff_tr(ROFF_ARGS);
 static	int		 roff_Dd(ROFF_ARGS);
 static	int		 roff_TE(ROFF_ARGS);
@@ -309,6 +310,7 @@ const char *__roff_name[MAN_MAX + 1] = {
 	"rs",		"rt",		"schar",	"sentchar",
 	"shc",		"shift",	"sizes",	"so",
 	"spacewidth",	"special",	"spreadwarn",	"ss",
+	"stringdown",	"stringup",
 	"sty",		"substring",	"sv",		"sy",
 	"T&",		"tc",		"TE",
 	"TH",		"tkf",		"tl",
@@ -569,6 +571,8 @@ static	struct roffmac	 roffs[TOKEN_NONE]
 	{ roff_line_ignore, NULL, NULL, 0 },  /* special */
 	{ roff_line_ignore, NULL, NULL, 0 },  /* spreadwarn */
 	{ roff_line_ignore, NULL, NULL, 0 },  /* ss */
+	{ roff_stringup, NULL, NULL, 0 },  /* stringdown */
+	{ roff_stringup, NULL, NULL, 0 },  /* stringup */
 	{ roff_line_ignore, NULL, NULL, 0 },  /* sty */
 	{ roff_unsupp, NULL, NULL, 0 },  /* substring */
 	{ roff_line_ignore, NULL, NULL, 0 },  /* sv */
@@ -3943,6 +3947,33 @@ roff_so(ROFF_ARGS)
 
 	*offs = pos;
 	return ROFF_SO;
+}
+
+static int
+roff_stringup(ROFF_ARGS)
+{
+	const char	*name;
+	char		*cp;
+	size_t		 namesz;
+	int		 deftype;
+
+	cp = buf->buf + pos;
+	name = cp;
+	namesz = roff_getname(&cp, ln, pos);
+	if (*cp != '\0')
+		mandoc_msg(MANDOCERR_ARG_EXCESS, ln, (int)(cp - buf->buf),
+		    "%s ... %s", roff_name[tok], cp);
+	deftype = ROFFDEF_USER;
+	if ((cp = (char *)roff_getstrn(r, name, namesz, &deftype)) != NULL) {
+		while (*cp != '\0') {
+			if (tok == ROFF_stringup)
+				*cp = toupper(*cp);
+			else
+				*cp = tolower(*cp);
+			cp++;
+		}
+	}
+	return ROFF_IGN;
 }
 
 /* --- user defined strings and macros ------------------------------------ */
Index: roff.h
===================================================================
RCS file: /home/cvs/mandoc/mandoc/roff.h,v
diff -Lroff.h -Lroff.h -u -p -r1.76 -r1.77
--- roff.h
+++ roff.h
@@ -273,6 +273,8 @@ enum	roff_tok {
 	ROFF_special,
 	ROFF_spreadwarn,
 	ROFF_ss,
+	ROFF_stringdown,
+	ROFF_stringup,
 	ROFF_sty,
 	ROFF_substring,
 	ROFF_sv,
Index: roff.7
===================================================================
RCS file: /home/cvs/mandoc/mandoc/roff.7,v
diff -Lroff.7 -Lroff.7 -u -p -r1.123 -r1.124
--- roff.7
+++ roff.7
@@ -1,6 +1,7 @@
 .\" $Id$
 .\"
-.\" Copyright (c) 2010-2019,2022-2023,2025 Ingo Schwarze <[email protected]>
+.\" Copyright (c) 2010-2019, 2022-2023, 2025, 2026
+.\"               Ingo Schwarze <[email protected]>
 .\" Copyright (c) 2010, 2011, 2012 Kristaps Dzonsons <[email protected]>
 .\"
 .\" Permission to use, copy, modify, and distribute this software for any
@@ -1637,6 +1638,18 @@ Currently ignored.
 .It Ic \&ss Ar wordspace Op Ar sentencespace
 Set space character size.
 Currently ignored.
+.It Ic \&stringdown Ar stringname
+Convert each byte of the user-defined string with the given
+.Ar stringname
+to lower case.
+If the strings contains escape sequences, for example special character
+escapes, each byte of the names and arguments of the sequences is
+converted rather than attempting wide character case conversion.
+This is a groff-1.23 extension.
+.It Ic \&stringup Ar stringname
+Like
+.Ic \&stringdown ,
+but converts to upper case instead of to lower case.
 .It Ic \&sty Ar position style
 Associate style with a font position.
 This is a groff extension and currently ignored.
--- /dev/null
+++ regress/roff/string/stringup_bad.in
@@ -0,0 +1,8 @@
+.\" $OpenBSD: stringup_bad.in,v 1.1 2026/08/16 16:22:47 schwarze Exp $
+.TH STRING-STRINGUP_BAD 1 "August 16, 2026"
+.SH NAME
+string-stringup_bad - invalid syntax in case conversion
+.SH DESCRIPTION
+.ds mynm content
+.stringup mynm trailing garbage
+\*[mynm]
--- /dev/null
+++ regress/roff/string/stringup_bad.out_lint
@@ -0,0 +1 @@
+mandoc: stringup_bad.in:7:16: ERROR: skipping excess arguments: stringup ... trailing garbage
Index: Makefile
===================================================================
RCS file: /home/cvs/mandoc/mandoc/regress/roff/string/Makefile,v
diff -Lregress/roff/string/Makefile -Lregress/roff/string/Makefile -u -p -r1.5 -r1.6
--- regress/roff/string/Makefile
+++ regress/roff/string/Makefile
@@ -1,20 +1,26 @@
-# $OpenBSD: Makefile,v 1.10 2022/06/03 11:50:25 schwarze Exp $
+# $OpenBSD: Makefile,v 1.11 2026/08/16 16:22:47 schwarze Exp $
 #
 # This directory is intended for tests of string *expansion*,
 # in particular testing the behaviour of the \* escape sequence.
 # Tests of string *definitions* are better placed in the roff/ds
 # directory.
 
-REGRESS_TARGETS	 = dotT escape infinite name std undef zerolength
-LINT_TARGETS	 = name std undef
-UTF8_TARGETS	 = dotT
+REGRESS_TARGETS	 = dotT escape infinite name std stringup stringup_bad
+REGRESS_TARGETS	+= undef zerolength
+LINT_TARGETS	 = name std stringup_bad undef
+UTF8_TARGETS	 = dotT stringup
 HTML_TARGETS	 = dotT
-SKIP_MARKDOWN	 = escape infinite name std undef zerolength
+SKIP_MARKDOWN	 = escape infinite name std stringup stringup_bad
+SKIP_MARKDOWN	+= undef zerolength
 
 # The infinite test fails badly with groff-1.20.1:
 # It fails to print the following text.
 
 SKIP_GROFF	?= infinite
+
+# groff-1.24 lets excess .stringup arguments sneak through to the output stream
+
+SKIP_GROFF	+= stringup_bad
 
 # Groff can expand standard macros as strings, but mandoc cannot.
 
--- /dev/null
+++ regress/roff/string/stringup.in
@@ -0,0 +1,16 @@
+.\" $OpenBSD: stringup.in,v 1.1 2026/08/16 16:22:47 schwarze Exp $
+.TH STRING-STRINGUP 1 "August 16, 2026"
+.SH NAME
+string-stringup - convert case of string
+.SH DESCRIPTION
+.ie dmynm already defined (unexpected)
+.el undefined
+.stringup mynm
+.ie dmynm now defined
+.el still undefined (unexpected)
+.ds mynm R\['e]sum\['e]\"
+\*[mynm]
+.stringdown mynm
+\*[mynm]
+.stringup mynm
+\*[mynm]
--- /dev/null
+++ regress/roff/string/stringup.out_utf8
@@ -0,0 +1,9 @@
+STRING-STRINGUP(1)          General Commands Manual         STRING-STRINGUP(1)
+
+NNAAMMEE
+     string-stringup - convert case of string
+
+DDEESSCCRRIIPPTTIIOONN
+     undefined now defined Résumé résumé RÉSUMÉ
+
+OpenBSD                         August 16, 2026             STRING-STRINGUP(1)
--- /dev/null
+++ regress/roff/string/stringup.out_ascii
@@ -0,0 +1,9 @@
+STRING-STRINGUP(1)          General Commands Manual         STRING-STRINGUP(1)
+
+NNAAMMEE
+     string-stringup - convert case of string
+
+DDEESSCCRRIIPPTTIIOONN
+     undefined now defined R'esum'e r'esum'e R'ESUM'E
+
+OpenBSD                         August 16, 2026             STRING-STRINGUP(1)
--- /dev/null
+++ regress/roff/string/stringup_bad.out_ascii
@@ -0,0 +1,9 @@
+STRING-STRINGUP_BAD(1)      General Commands Manual     STRING-STRINGUP_BAD(1)
+
+NNAAMMEE
+     string-stringup_bad - invalid syntax in case conversion
+
+DDEESSCCRRIIPPTTIIOONN
+     CONTENT
+
+OpenBSD                         August 16, 2026         STRING-STRINGUP_BAD(1)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.