groff-commit
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[groff] 03/04: [grotty]: Add OSC 8 hyperlink support.


From: G. Branden Robinson
Subject: [groff] 03/04: [grotty]: Add OSC 8 hyperlink support.
Date: Fri, 1 Oct 2021 07:53:04 -0400 (EDT)

gbranden pushed a commit to branch master
in repository groff.

commit ab73e8189988d15bae12a82c6b3eb07948eda1d7
Author: G. Branden Robinson <g.branden.robinson@gmail.com>
AuthorDate: Fri Oct 1 20:43:30 2021 +1000

    [grotty]: Add OSC 8 hyperlink support.
    
    * src/devices/grotty/tty.cpp: Do it.  Define `OSC` (Operating System
      Command) and `ST` (String Terminator) preprocessor symbols for these
      ECMA-48 (ISO 6429) character sequences.
    
      (tty_printer::simple_add_char): Add stripped-down alternative to
      `add_char()` member function for cases where we want to use many
      defaults because we're writing a device control command, not rendering
      a glyph.  (A function like `add_char()` that take 8 arguments of
      varying types is a code smell--phew!)
    
      (tty_printer::special): Call `special_link()` member function if the
      device control command is `link`.
    
      (tty_printer::special_link): Add new member function to generate OSC 8
      hyperlinks.
    
    * src/devices/grotty/grotty.1.man: Document it.  Observe in multiple
      places that disablement of SGR escape sequences disables OSC 8 too.
    
    * src/devices/grotty/tests/osc8_works.sh: Test it.
    * src/devices/grotty/grotty.am (TESTS, grotty_TESTS): Run test.
      (EXTRA_DIST): Ship test.
    
    * NEWS: Add item.
    
    Fixes <https://savannah.gnu.org/bugs/?60666>.  Thanks to Steffen
    Nurpmeso for supplying a proof-of-concept.  (I went with my own
    implementation, though, so blame me if it breaks.)
---
 ChangeLog                              |  31 +++++++++
 NEWS                                   |   4 ++
 src/devices/grotty/grotty.1.man        |  93 +++++++++++++++++++-------
 src/devices/grotty/grotty.am           |   6 +-
 src/devices/grotty/tests/osc8_works.sh | 118 +++++++++++++++++++++++++++++++++
 src/devices/grotty/tty.cpp             |  95 +++++++++++++++++++++++++-
 6 files changed, 320 insertions(+), 27 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 1653477..f1c5fca 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,36 @@
 2021-10-01  G. Branden Robinson <g.branden.robinson@gmail.com>
 
+       [grotty]: Add OSC 8 hyperlink support.
+
+       * src/devices/grotty/tty.cpp: Do it.  Define `OSC` (Operating
+       System Command) and `ST` (String Terminator) preprocessor
+       symbols for these ECMA-48 (ISO 6429) character sequences.
+       (tty_printer::simple_add_char): Add stripped-down alternative to
+       `add_char()` member function for cases where we want to use many
+       defaults because we're writing a device control command, not
+       rendering a glyph.  (A function like `add_char()` that take 8
+       arguments of varying types is a code smell--phew!)
+       (tty_printer::special): Call `special_link()` member function if
+       the device control command is `link`.
+       (tty_printer::special_link): Add new member function to generate
+       OSC 8 hyperlinks.
+
+       * src/devices/grotty/grotty.1.man: Document it.  Observe in
+       multiple places that disablement of SGR escape sequences
+       disables OSC 8 too.
+
+       * src/devices/grotty/tests/osc8_works.sh: Test it.
+       * src/devices/grotty/grotty.am (TESTS, grotty_TESTS): Run test.
+       (EXTRA_DIST): Ship test.
+
+       * NEWS: Add item.
+
+       Fixes <https://savannah.gnu.org/bugs/?60666>.  Thanks to Steffen
+       Nurpmeso for supplying a proof-of-concept.  (I went with my own
+       implementation, though, so blame me if it breaks.)
+
+2021-10-01  G. Branden Robinson <g.branden.robinson@gmail.com>
+
        [troff]: Convert special character glyphs corresponding to
        Unicode Basic Latin ("ASCII") code points to those code points
        when they occur in device escapes.  (They should be correct for
diff --git a/NEWS b/NEWS
index 1a7ab30..2ef7d9c 100644
--- a/NEWS
+++ b/NEWS
@@ -206,6 +206,10 @@ o The s (ms) macro package has added strings, \*< and \*>, 
to perform
 grotty
 ------
 
+o A new device control command, "link", generates OSC 8 hyperlinks.
+  This means that groff documents can produce clickable links in the
+  terminal window for emulators that support such escape sequences.
+
 o On the Latin-1 output device ("groff -T latin1") the output glyph
   \[oq] (opening quote) is now rendered as code point 0x27 (apostrophe)
   instead of 0x60 (grave accent).  The ISO 8859/ECMA-94 Latin character
diff --git a/src/devices/grotty/grotty.1.man b/src/devices/grotty/grotty.1.man
index 44de5df..a048747 100644
--- a/src/devices/grotty/grotty.1.man
+++ b/src/devices/grotty/grotty.1.man
@@ -7,7 +7,7 @@ grotty \- groff output driver for typewriter-like (terminal) 
devices
 .\" Legal Terms
 .\" ====================================================================
 .\"
-.\" Copyright (C) 1989-2020 Free Software Foundation, Inc.
+.\" Copyright (C) 1989-2021 Free Software Foundation, Inc.
 .\"
 .\" Permission is granted to make and distribute verbatim copies of this
 .\" manual provided the copyright notice and this permission notice are
@@ -105,7 +105,7 @@ reads the standard input stream.
 Output is written to the standard output stream.
 .
 .
-.LP
+.P
 By default,
 .I grotty
 emits SGR escape sequences
@@ -118,8 +118,6 @@ Devices supporting the appropriate sequences can view
 .I roff
 documents using eight different background and foreground colors.
 .
-.
-.LP
 Following ISO\~6429,
 the following colors are defined in
 .IR tty.tmac :
@@ -128,8 +126,10 @@ black, white, red, green, blue, yellow, magenta, and cyan.
 Unrecognized colors are mapped to the default color,
 which is dependent on the settings of the terminal.
 .
+OSC\~8 hyperlinks are produced for these devices.
+.
 .
-.LP
+.P
 In keeping with long-standing practice and the rarity of terminals,
 hardware or emulated,
 that support oblique or italic fonts,
@@ -140,18 +140,20 @@ option below.
 .
 .
 .\" ====================================================================
-.SS "SGR support in pagers"
+.SS "SGR and OSC support in pagers"
 .\" ====================================================================
 .
 When paging
 .IR grotty 's
 output with
 .IR less (1),
-the latter program must be instructed to pass SGR sequences through to
-the device;
+the latter program must be instructed to pass SGR and OSC sequences
+through to the device;
 its
 .B \-R
-option is one way to achieve this.
+option is one way to achieve this
+.RI ( less
+version 566 or later is required for OSC\~8 support).
 .
 Consequently,
 programs like
@@ -212,7 +214,7 @@ see subsection \[lq]Device control commands\[rq],
 below.
 .
 .
-.LP
+.P
 The legacy output format can be rendered on a video terminal
 (or emulator)
 by piping
@@ -251,13 +253,41 @@ There is therefore no need to filter its output through
 .\" ====================================================================
 .
 .I grotty
-understands a single device control function produced using the
+understands the following device control functions produced using the
 .I roff
 .B \[rs]X
 escape sequence in a document.
 .
 .
 .TP
+.BR "\[rs]X\[aq]tty: link " [\c
+.IR uri \~[ key\c
+.BI = value\c
+] \|.\|.\|.\|]\c
+.B \[aq]
+.
+Embed a hyperlink using the OSC 8 terminal escape sequence.
+.
+Specifying
+.I uri
+starts hyperlinked text,
+and omitting it ends the hyperlink.
+.
+When
+.I uri
+is is present,
+any number of additional key/value pairs can be specified;
+their interpretation is the responsibility of the terminal emulator.
+.
+Spaces or tabs cannot appear literally in
+.IR uri ,
+.IR key ,
+or
+.IR value ;
+they must be represented in an alternate form.
+.
+.
+.TP
 .BR "\[rs]X\[aq]tty: sgr " [\c
 .IR n ]\c
 .B \[aq]
@@ -269,12 +299,17 @@ is non-zero or missing, enable SGR sequences
 otherwise,
 use the legacy output format.
 .
+This device control command,
+like the
+.B \-c
+option,
+also prevents the emission of terminal OSC 8 hyperlink escape sequences.
+.
 .
 .\" ====================================================================
 .SS "Device description files"
 .\" ====================================================================
 .
-.LP
 If
 .I DESC
 file for the character encoding contains the keyword
@@ -290,7 +325,7 @@ See
 for more details.
 .
 .
-.LP
+.P
 A font description file may contain a command
 .RB \[lq] internalname\~\c
 .IR n \[rq]
@@ -354,6 +389,8 @@ Use
 legacy output format
 (see subsection \[lq]Legacy output format\[rq] above).
 .
+OSC\~8 hyperlink terminal escape sequences are not emitted.
+.
 .
 .TP
 .B \-d
@@ -423,7 +460,7 @@ Render italic-styled text
 with the SGR attribute for italic text
 rather than underlined text.
 .
-Note that many terminals don't support this attribute;
+Many terminals don't support this attribute;
 however,
 .IR xterm (1),
 since patch\~#314 (2014-12-28),
@@ -563,7 +600,7 @@ Additional character definitions for use with
 .
 .\" The following no longer seems to be true; an inspection of the
 .\" font/*/dev*.am files suggests no evidence of it, at any rate.
-.\".LP
+.\".P
 .\"Note that on EBCDIC hosts,
 .\"only files for the \[lq]cp1047\[rq] device are installed.
 .
@@ -576,24 +613,24 @@ Additional character definitions for use with
 is intended only for simple documents.
 .
 .
-.LP
+.P
 There is no support for fractional horizontal or vertical motions.
 .
 .
-.LP
+.P
 There is no support for the
 .I roff
 .B \[rs]D
 escape sequence (draw command) other than horizontal and vertical lines.
 .
 .
-.LP
+.P
 Characters above the first line
 (i.e., with a vertical position of\~0)
 cannot be printed.
 .
 .
-.LP
+.P
 Color handling differs from
 .IR grops (@MAN1EXT@).
 .
@@ -625,7 +662,7 @@ not all of which may be supported by a given output device:
 (7)\~horizontal and vertical line-drawing.
 .
 .
-.LP
+.P
 .RS
 .EX
 You might see \ef[B]bold\ef[] and \ef[I]italic\ef[].
@@ -640,11 +677,11 @@ Black on cyan can have a 
\eM[cyan]\em[black]prominent\em[]\eM[]
 .RE
 .
 .
-.LP
+.P
 Compare and contrast the output of the following:
 .
 .
-.LP
+.P
 .RS
 .EX
 $ \c
@@ -666,7 +703,7 @@ $ \c
 .
 .
 .\" I wish this went without saying...
-.LP
+.P
 Note that the example file above is a \[lq]raw\[rq]
 .I groff
 document,
@@ -705,7 +742,15 @@ ECMA\-ST/\:Ecma\-048\:.pdf
 .UE .
 .
 .
-.LP
+.P
+.UR https://\:gist\:.github\:.com/\:egmontkob/\:\
+eb114294efbcd5adb1944c9f3cb5feda
+\[lq]Hyperlinks in Terminal Emulators\[rq]
+.UE ,
+Egmont Koblinger.
+.
+.
+.P
 .IR groff (@MAN1EXT@),
 .IR \%@g@troff (@MAN1EXT@),
 .IR groff_out (@MAN5EXT@),
diff --git a/src/devices/grotty/grotty.am b/src/devices/grotty/grotty.am
index 6a881fd..a741001 100644
--- a/src/devices/grotty/grotty.am
+++ b/src/devices/grotty/grotty.am
@@ -1,4 +1,4 @@
-# Copyright (C) 2014-2020 Free Software Foundation, Inc.
+# Copyright (C) 2014-2021 Free Software Foundation, Inc.
 #
 # This file is part of groff.
 #
@@ -26,6 +26,10 @@ EXTRA_DIST += \
   src/devices/grotty/grotty.1.man \
   src/devices/grotty/TODO
 
+grotty_TESTS = \
+  src/devices/grotty/tests/osc8_works.sh
+TESTS += $(grotty_TESTS)
+EXTRA_DIST += $(grotty_TESTS)
 
 # Local Variables:
 # fill-column: 72
diff --git a/src/devices/grotty/tests/osc8_works.sh 
b/src/devices/grotty/tests/osc8_works.sh
new file mode 100755
index 0000000..2e79d2a
--- /dev/null
+++ b/src/devices/grotty/tests/osc8_works.sh
@@ -0,0 +1,118 @@
+#!/bin/sh
+#
+# Copyright (C) 2021 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+set -e
+
+grotty="${abs_top_builddir:-.}/grotty"
+
+input="x T utf8
+x res 240 24 40
+x init
+p1
+x font 1 R
+f1
+s10
+V40
+H0
+md
+DFd
+tA
+n40 0
+x X tty: link
+x X tty: link h
+x X tty: link http://example.com/1
+x X tty: link
+x X tty: link http://example.com/2
+tB
+x X tty: link
+x X tty: link mailto:g.branden.robinson@gmail.com
+tBranden
+x X tty: link
+x trailer
+V2640
+x stop"
+
+# We expect diagnostics from the first few "x X tty: link" lines.  The
+# first should complain about a link ending without having been started.
+# The second is bogus ("h") but it's not grotty's job to validate the
+# structure of a URI.  The third should draw complaint because we didn't
+# end the (bogus) URI that we started with the second.
+
+# The remaining input is well-formed.  The URI ending in "1" is
+# effectively hidden because no character cells are drawn while it is
+# active.
+output=$(echo "$input" | "$grotty" -F font -F build/font | od -t c)
+
+# Expected:
+#0000000   A 033   ]   8   ;   ; 033   \ 033   ]   8   ;   ;   h 033   \
+#0000020 033   ]   8   ;   ; 033   \ 033   ]   8   ;   ;   h   t   t   p
+#0000040   :   /   /   e   x   a   m   p   l   e   .   c   o   m   /   1
+#0000060 033   \ 033   ]   8   ;   ; 033   \ 033   ]   8   ;   ;   h   t
+#0000100   t   p   :   /   /   e   x   a   m   p   l   e   .   c   o   m
+#0000120   /   2 033   \   B 033   ]   8   ;   ; 033   \ 033   ]   8   ;
+#0000140   ;   m   a   i   l   t   o   :   g   .   b   r   a   n   d   e
+#0000160   n   .   r   o   b   i   n   s   o   n   @   g   m   a   i   l
+#0000200   .   c   o   m 033   \   B   r   a   n   d   e   n 033   ]   8
+#0000220   ;   ; 033   \  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n
+#0000240  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n
+#*
+#0000320  \n  \n  \n  \n  \n  \n
+#0000326
+
+echo "testing for URI that corresponds to no character cells" >&2
+echo "$output" | grep -Eq 'A 033 +] +8 +; +; +033 +\\'
+
+echo "testing http URI (1)" >&2
+echo "$output" | grep -Eq '0000020 +.*033 +] +8 +; +; +h + t +t +p'
+
+echo "testing http URI (2)" >&2
+echo "$output" | grep -Eq '0000040 +: +/ +/ +e +x +a +m +p +l +e +\. +c'
+
+echo "testing http URI (3)" >&2
+echo "$output" | grep -Eq '0000040.* +o +m +/ +1'
+
+echo "testing http URI (4)" >&2
+echo "$output" | grep -Eq '0000060 +033 +\\'
+
+echo "testing mailto URI (1)" >&2
+echo "$output" | grep -Eq '0000120 +.*+033 +] +8 +;$'
+
+echo "testing mailto URI (2)" >&2
+echo "$output" | grep -Eq '0000140 +; +m +a +i +l +t +o +: +g +\. +b'
+
+echo "testing mailto URI (3)" >&2
+echo "$output" | grep -Eq '0000140.* +r +a +n +d +e$'
+
+echo "testing mailto URI (4)" >&2
+echo "$output" | grep -Eq '0000160 +n +\. +r +o +b +i +n +s +o +n +@'
+
+echo "testing mailto URI (5)" >&2
+echo "$output" | grep -Eq '0000160.* +g +m +a +i +l$'
+
+echo "testing mailto URI (6)" >&2
+echo "$output" | grep -Eq '0000200 +\. +c +o +m +033 +\\ +B +r +a +n +d'
+
+echo "testing mailto URI (7)" >&2
+echo "$output" | grep -Eq '0000200.* +e +n +033 +] +8$'
+
+echo "testing mailto URI (8)" >&2
+echo "$output" | grep -Eq '0000220 +; +; +033 +\\'
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/devices/grotty/tty.cpp b/src/devices/grotty/tty.cpp
index 3efd852..bd295a7 100644
--- a/src/devices/grotty/tty.cpp
+++ b/src/devices/grotty/tty.cpp
@@ -1,6 +1,6 @@
-// -*- C++ -*-
-/* Copyright (C) 1989-2020 Free Software Foundation, Inc.
+/* Copyright (C) 1989-2021 Free Software Foundation, Inc.
      Written by James Clark (jjc@jclark.com)
+     OSC 8 support by G. Branden Robinson
 
 This file is part of groff.
 
@@ -80,8 +80,12 @@ static unsigned char bold_underline_mode;
 
 #ifndef IS_EBCDIC_HOST
 #define CSI "\033["
+#define OSC8 "\033]8"
+#define ST "\033\\"
 #else
 #define CSI "\047["
+#define OSC8 "\047]8"
+#define ST "\047\\"
 #endif
 
 // SGR handling (ISO 6429)
@@ -183,12 +187,14 @@ class tty_printer : public printer {
   schar color_to_idx(color *);
   void add_char(output_character, int, int, int, color *, color *,
                unsigned char);
+  void simple_add_char(const output_character, const environment *);
   char *make_rgb_string(unsigned int, unsigned int, unsigned int);
   int tty_color(unsigned int, unsigned int, unsigned int, schar *,
                schar = DEFAULT_COLOR_IDX);
   void line(int, int, int, int, color *, color *);
   void draw_line(int *, int, const environment *);
   void draw_polygon(int *, int, const environment *);
+  void special_link(const char *, const environment *);
 public:
   tty_printer();
   ~tty_printer();
@@ -404,6 +410,12 @@ void tty_printer::add_char(output_character c, int w,
   *pp = g;
 }
 
+void tty_printer::simple_add_char(const output_character c,
+                                 const environment *env)
+{
+  add_char(c, 0, env->hpos, env->vpos, env->col, env->fill, 0);
+}
+
 void tty_printer::special(char *arg, const environment *env, char type)
 {
   if (type == 'u') {
@@ -443,6 +455,79 @@ void tty_printer::special(char *arg, const environment 
*env, char type)
       old_drawing_scheme = 0;
     update_options();
   }
+  else if (strncmp(command, "link", p - command) == 0)
+    special_link(p, env);
+}
+
+// Produce an OSC 8 hyperlink.  Given ditroff input of the form:
+//   x X tty: link [URI[ KEY=VALUE] ...]
+// produce "OSC 8 [;KEY=VALUE:]...;[URI]; ST ".  KEY/VALUE pairs can be
+// repeated arbitrarily and are separated by colons.  Omission of the
+// URI ends the hyperlink that was begun by specifying it.  See
+// <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>.
+void tty_printer::special_link(const char *arg, const environment *env)
+{
+  static bool is_link_active = false;
+  if (old_drawing_scheme)
+    return;
+  for (const char *s = OSC8; *s != '\0'; s++)
+    simple_add_char(*s, env);
+  simple_add_char(';', env);
+  char c = *arg;
+  if ('\0' == c || '\n' == c) {
+    simple_add_char(';', env);
+    if (!is_link_active)
+      warning("ending hyperlink when none was started");
+    is_link_active = false;
+  }
+  else {
+    // Our caller ensures that we see white space after 'link'.
+    assert(c == ' ' || c == '\t');
+    if (is_link_active) {
+      warning("new hyperlink started without ending previous one;"
+             " recovering");
+      simple_add_char(';', env);
+       for (const char *s = ST; *s != '\0'; s++)
+         simple_add_char(*s, env);
+       for (const char *s = OSC8; *s != '\0'; s++)
+         simple_add_char(*s, env);
+       simple_add_char(';', env);
+    }
+    is_link_active = true;
+    do
+      c = *arg++;
+    while (c == ' ' || c == '\t');
+    arg--;
+    // The first argument is the URI.
+    char *uri = (char *)arg;
+    while (c != '\0' && c != ' ' && c != '\t')
+      c = *arg++;
+    ptrdiff_t uri_len = arg - uri - 1;
+    arg--;
+    // Any remaining arguments are "key=value" pairs.
+    char *pair = 0;
+    bool done = false;
+    do {
+      if (pair != 0)
+       simple_add_char(':', env);
+      pair = (char *)arg;
+      bool in_pair = true;
+      do {
+       c = *arg++;
+       if ('\0' == c)
+         done = true;
+       else if (' ' == c || '\t' == c)
+         in_pair = false;
+       else
+         simple_add_char(c, env);
+      } while (!done && in_pair);
+    } while (!done);
+    simple_add_char(';', env);
+    for (size_t i = uri_len; i > 0; i--)
+      simple_add_char(*uri++, env);
+  }
+  for (const char *s = ST; *s != '\0'; s++)
+    simple_add_char(*s, env);
 }
 
 void tty_printer::change_color(const environment * const env)
@@ -926,3 +1011,9 @@ static void usage(FILE *stream)
   fprintf(stream, "usage: %s [-bBcdfhioruUv] [-F dir] [files ...]\n",
          program_name);
 }
+
+// Local Variables:
+// fill-column: 72
+// mode: C++
+// End:
+// vim: set cindent noexpandtab shiftwidth=2 textwidth=72:



reply via email to

[Prev in Thread] Current Thread [Next in Thread]