From 75a5bdedbe3299b4a8d58239fd6d4ddaf52c8e5b Mon Sep 17 00:00:00 2001 From: bptato Date: Fri, 29 Mar 2024 16:32:04 +0100 Subject: ansi2html: support passing titles Use content type attributes so e.g. git.cgi can set the title even with a text/x-ansi content type. (This commit also fixes some bugs in content type attribute handling.) --- adapter/format/ansi2html.nim | 32 ++++++++++++++++++++++++++++++-- bonus/git.cgi | 13 +++++-------- res/mailcap | 2 +- src/config/mailcap.nim | 1 + src/utils/twtstr.nim | 38 +++++++++++++++++++++----------------- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/adapter/format/ansi2html.nim b/adapter/format/ansi2html.nim index 4266a3ba..9abc6925 100644 --- a/adapter/format/ansi2html.nim +++ b/adapter/format/ansi2html.nim @@ -356,11 +356,39 @@ proc processData(state: var State, buf: openArray[char]) = of '\0': state.puts("\uFFFD") # HTML eats NUL, so replace it here else: state.putc(c) +proc usage() = + stderr.write("Usage: ansihtml [-s] [-t title]\n") + quit(1) + proc main() = var state = State() - let standalone = paramCount() >= 1 and paramStr(1) == "-s" + # parse args + let H = paramCount() + var i = 1 + var standalone = false + var title = "" + while i <= H: + let s = paramStr(i) + if s == "": + inc i + if s[0] != '-': + usage() + for j in 1 ..< s.len: + case s[j] + of 's': + standalone = true + of 't': + inc i + if i > H: usage() + title = paramStr(i).percentDecode() + else: discard + inc i + if standalone: + state.puts("\n") + if title != "": + state.puts("" & title.htmlEscape() & "\n") if standalone: - state.puts("\n") + state.puts("\n") state.puts("
\n")
   let ofl = fcntl(STDIN_FILENO, F_GETFL, 0)
   doAssert ofl != -1
diff --git a/bonus/git.cgi b/bonus/git.cgi
index 1f93862a..387f3a64 100755
--- a/bonus/git.cgi
+++ b/bonus/git.cgi
@@ -27,12 +27,7 @@ for (const p of std.getenv("QUERY_STRING").split('&')) {
 }
 
 function startGitCmd(config, params) {
-	const titleParams = params.join(' ').replace(/[&<>]/g,
-		x => ({'&': '&', '<': '<', '>': '>'}[x]));
-	std.out.puts(`Content-Type: text/html
-
-
-git ${titleParams}`);
+	std.out.puts("Content-Type: text/html\n\n");
 	std.out.flush();
 	const [read_fd, write_fd] = os.pipe();
 	const [read_fd2, write_fd2] = os.pipe();
@@ -43,7 +38,8 @@ function startGitCmd(config, params) {
 	os.close(write_fd);
 	const libexecDir = std.getenv("CHA_LIBEXEC_DIR") ??
 		'/usr/local/libexec/chawan';
-	os.exec([libexecDir + "/ansi2html"], {
+	const title = encodeURIComponent('git ' + params.join(' '));
+	os.exec([libexecDir + "/ansi2html", "-st", title], {
 		stdin: read_fd,
 		stdout: write_fd2,
 		block: false
@@ -78,7 +74,8 @@ if (params[0] == "log") {
 	}
 	f.close();
 } else {
-	std.out.puts("Content-Type: text/x-ansi\n\n");
+	const title = encodeURIComponent('git ' + params.join(' '));
+	std.out.puts(`Content-Type: text/x-ansi;title=${title}\n\n`);
 	std.out.flush();
 	const pid = os.exec(["git", ...config, ...params], {
 		block: false,
diff --git a/res/mailcap b/res/mailcap
index 4d6619d7..56c6582e 100644
--- a/res/mailcap
+++ b/res/mailcap
@@ -3,4 +3,4 @@
 text/gopher;	"$CHA_LIBEXEC_DIR"/gopher2html -u "$MAILCAP_URL"; x-htmloutput
 text/gemini;	"$CHA_LIBEXEC_DIR"/gmi2html; x-htmloutput
 text/markdown;	"$CHA_LIBEXEC_DIR"/md2html; x-htmloutput
-text/x-ansi;	"$CHA_LIBEXEC_DIR"/ansi2html -s; x-htmloutput
+text/x-ansi;	"$CHA_LIBEXEC_DIR"/ansi2html -st '%{title}'; x-htmloutput
diff --git a/src/config/mailcap.nim b/src/config/mailcap.nim
index b108ca20..41575ca8 100644
--- a/src/config/mailcap.nim
+++ b/src/config/mailcap.nim
@@ -308,6 +308,7 @@ proc unquoteCommand*(ecmd, contentType, outpath: string; url: URL;
         let s = contentType.getContentTypeAttr(attrname)
         cmd &= quoteFile(s, qs)
         attrname = ""
+        state = STATE_NORMAL
       elif c == '\\':
         state = STATE_ATTR_QUOTED
       else:
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 6f40ae7b..f0a64546 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -678,22 +678,26 @@ func strictParseEnum*[T: enum](s: string): Opt[T] =
   return err()
 
 proc getContentTypeAttr*(contentType, attrname: string): string =
-  let kvs = contentType.after(';')
-  var i = kvs.find(attrname)
+  var i = contentType.find(';')
+  if i == -1:
+    return ""
+  i = contentType.find(attrname, i)
+  if i == -1:
+    return ""
+  i = contentType.skipBlanks(i + attrname.len)
+  if i >= contentType.len or contentType[i] != '=':
+    return ""
+  i = contentType.skipBlanks(i + 1)
+  var q = false
   var s = ""
-  if i != -1 and kvs.len > i + attrname.len and
-      kvs[i + attrname.len] == '=':
-    i += attrname.len + 1
-    while i < kvs.len and kvs[i] in AsciiWhitespace:
-      inc i
-    var q = false
-    for j, c in kvs.toOpenArray(i, kvs.high):
-      if q:
-        s &= c
-      elif c == '\\':
-        q = true
-      elif c == ';' or c in AsciiWhitespace:
-        break
-      else:
-        s &= c
+  for c in contentType.toOpenArray(i, contentType.high):
+    if q:
+      s &= c
+      q = false
+    elif c == '\\':
+      q = true
+    elif c in AsciiWhitespace + {';'}:
+      break
+    else:
+      s &= c
   return s
-- 
cgit 1.4.1-2-gfad0