;;; GNU info -> HTML gateway for the SU web server. -*- Scheme -*- ;;; This file is part of the Scheme Untergrund Networking package. ;;; Copyright (c) 1996 by Mike Sperber. ;;; based on code with the same purpose by Gaebe Engelhart ;;; For copyright information, see the file COPYING which comes with ;;; the distribution. ;;; (info-handler parse-info reference find-icon address) -> handler ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; This function creates a request handler that converts GNU info pages ;;; on-the-fly. It is highly parameterizable to accomodate a wide ;;; range of environments. The parameters specify how to find the ;;; source code for the info pages, and how to generate certain ;;; elements of the generated HTML output. ;;; ;;; PARSE-INFO specifies how to parse the URLs that end up in the ;;; handler. ;;; It can be: ;;; ;;; * a procedure which is called with the URL as its parameters. ;;; It is expected to return with two values, FIND-ENTRY and ;;; NODE-NAME. FIND-ENTRY, in turn, can be either a procedure ;;; which gets passed the file name of an info node and is ;;; supposed to return with an absolute name of same. If it is a ;;; list, that list is taken as a list of directories in which to ;;; search for the info files. NODE-NAME is supposed to be the ;;; name of an info node of the form (), extracted ;;; from the URL. ;;; ;;; * a list, in which case that is taken as a list of ;;; directories in which to search for the info files. The node ;;; name of a node is extracted from the URL by just taking the ;;; search component of the URL. ;;; ;;; * #f in which case the info path is taken from the environment ;;; variable INFOPATH, and the node name extraction works as ;;; above. ;;; ;;; REFERENCE specifies how to generate cross-references to other info ;;; nodes. It can be: ;;; ;;; * a procedure which gets called with the URL of the info page ;;; which contains the reference, and the node name of the node ;;; to be referenced. The procedure is expected to return the ;;; text for a link. ;;; ;;; * a string, in which case that is to be a prefix to which the ;;; node name is simply appended to yield the new link. ;;; ;;; * #f in which case all references have the form ;;; info?. ;;; ;;; FIND-ICON specifies to to find the various icons used to decorate ;;; info pages. It can be: ;;; ;;; * a procedure which gets passed one of the tags in ;;; DEFAULT-ICON-ALIST and is supposed to return a link for the ;;; appropriate icon (or #f if no icon is to be used) ;;; ;;; * a string which is taken as a prefix to which one of the ;;; appropriate icon name from DEFAULT-ICON-ALIST is appended. ;;; (Note that these icon names were stolen from the ;;; cern-httpd-3.0 distribution at ;;; http://www.w3.org/pub/WWW/Daemon/.) ;;; ;;; * a list which is taken as an alist of the same format as ;;; DEFAULT-ICON-ALIST. ;;; ;;; * #f in which case no icons are used. ;;; ;;; ADDRESS a string to be appended at the bottom of all info pages ;;; ;;; To install a vanilla info handler for a prefix "info?" that looks ;;; in the environment variable INFOPATH, just use something like ;;; (info-handler #f #f #f "Generated by info-gateway") ;;; TODO: write a CGI version of this (define-condition-type 'info-gateway-error '(error)) (define info-gateway-error? (condition-predicate 'info-gateway-error)) (define (info-gateway-error msg . irritants) (apply signal 'info-gateway-error msg irritants)) (define default-icon-alist '((info . "infodoc.gif") (up . "up.gif") (next . "next.gif") (previous . "prev.gif") (menu . "menu.gif"))) (define (info-handler parse-info reference find-icon address) (let ((icon-name (cond ((procedure? find-icon) find-icon) ((string? find-icon) (let ((alist (map (lambda (entry) (cons (car entry) (string-append find-icon (cdr entry)))) default-icon-alist))) (lambda (tag) (cond ((assq tag alist) => cdr) (else #f))))) ((list? find-icon) (lambda (tag) (cond ((assq tag find-icon) => cdr) (else #f)))) (else (lambda (tag) #f)))) (parse-info-url (cond ((procedure? parse-info) parse-info) ((list? parse-info) ; it's an info path (lambda (url) (values parse-info (unescape-uri (http-url:search url))))) (else (let ((info-path ((infix-splitter ":") (getenv "INFOPATH")))) (lambda (url) (values info-path (unescape-uri (http-url:search url)))))))) (make-reference (cond ((procedure? reference) reference) ((string? reference) (lambda (url node-name) (string-append reference node-name))) (else (lambda (url node-name) (string-append "info?" node-name)))))) (lambda (path req) (let ((request-method (request-method req))) (cond ((string=? request-method "GET") (with-fatal-error-handler (lambda (c decline) (cond ((info-gateway-error? c) (apply http-error http-status/bad-gateway req (condition-stuff c))) ((http-error? c) (apply http-error (car (condition-stuff c)) req (cddr (condition-stuff c)))) (else (decline)))) (make-response http-status/ok (status-code->text http-status/ok) (time) "text/html" '() (make-writer-body (lambda (out options) (receive (find-entry node-name) (parse-info-url (request-url req)) (display-node node-name (file-finder find-entry) (referencer make-reference (request-url req) out) icon-name out)) (with-tag out address () (write-string address out))))))) (else (make-http-error-response http-status/method-not-allowed req request-method))))))) (define split-header-line (let ((split (infix-splitter (make-regexp "(, *)|( +)|( *\t *)"))) (split-field (infix-splitter (make-regexp ": *")))) (lambda (l) (let ((fields (map split-field (split l)))) (define (search-field regexp) (cond ((any (lambda (field) (string-match regexp (car field))) fields) => cadr) (else #f))) (values (search-field "[F|f]ile") (search-field "[N|n]ode") (search-field "[U|u]p") (search-field "[P|p]rev(ious)?") (search-field "[N|n]ext")))))) (define (replace-if-empty-string s v) (if (zero? (string-length s)) v s)) (define (string-newline->space s) (string-map (lambda (c) (if (char=? c #\newline) #\space c)) s)) (define (parse-node-name node-name) (cond ((string-match "^\\((.*)\\)(.*)$" (string-newline->space node-name)) => (lambda (match) (values (replace-if-empty-string (match:substring match 1) #f) (replace-if-empty-string (match:substring match 2) "Top")))) (else (values #f (string-newline->space node-name))))) (define (unparse-node-name file node) (let* ((ext (file-name-extension file)) (file (if (string=? ext ".info") (file-name-sans-extension file) file))) (receive (file node) (if (and (string=? "dir" file) (not (string=? "" node)) (not (string=? "Top" node))) (values node "Top") (values file node)) (string-append "(" file ")" node)))) (define (display-icon file alt out) (emit-tag out 'img (cons 'src file) (cons 'alt alt) (cons 'align "bottom"))) (define (referencer make-reference old-entry out) (lambda (file node-name label . maybe-icon) (receive (node-file node) (parse-node-name node-name) (let ((file (or node-file file))) (with-tag out a ((href (make-reference old-entry (escape-uri (unparse-node-name file node))))) (if (and (not (null? maybe-icon)) (car maybe-icon)) (display-icon (car maybe-icon) (cadr maybe-icon) out)) (emit-text label out)))))) (define node-prologue (ascii->char 31)) (define node-epilogue-regexp (make-regexp (string-append (regexp-quote (string node-prologue)) "|" (regexp-quote (string (ascii->char 12)))))) (define (string-starts-with-char? s c) (and (not (zero? (string-length s))) (char=? c (string-ref s 0)))) (define (node-prologue? s) (string-starts-with-char? s node-prologue)) (define (node-epilogue? s) (regexp-exec node-epilogue-regexp s)) ;; Document title (define (display-title file node up previous next display-reference icon-name out) (define (maybe-display-header header icon alt) (if header (begin (newline out) (with-tag out b () (display-reference file header header icon alt))))) (emit-title out (string-append "Info Node: " (unparse-node-name file node))) (with-tag out h1 () (emit-tag out 'img (cons 'src (icon-name 'info)) (cons 'alt "Info Node") (cons 'align 'bottom)) (write-string (unparse-node-name file node) out)) (emit-tag out 'hr) (maybe-display-header next (icon-name 'next) "[Next]") (maybe-display-header previous (icon-name 'previous) "[Previous]") (maybe-display-header up (icon-name 'up) "[Up]") (if (or next previous up) (emit-tag out 'hr))) ;; Text ;; Dealing with cross references ;; info sucks (define xref-marker-regexp (make-regexp "\\*[Nn]ote([ \n]|$)")) (define xref-regexp (make-regexp "\\*[Nn]ote *([^:]*): *([^\t\n,.;:?!]*)")) (define max-xref-lines 3) (define complete-line (let ((split-xref-markers (field-splitter xref-marker-regexp)) (split-xrefs (field-splitter xref-regexp)) (cr (string #\newline))) (lambda (line port) (let loop ((line line) (count max-xref-lines)) (let ((xref-markers (split-xref-markers line)) (xrefs (split-xrefs line))) (if (= (length xref-markers) (length xrefs)) line (if (zero? count) (info-gateway-error "invalid cross reference") (let ((new-line (read-line port))) (if (eof-object? new-line) (info-gateway-error "unexpected end of info file inside cross reference")) (loop (string-append line cr new-line) (- count 1)))))))))) (define (display-xref xref file display-reference out) (let* ((match (regexp-exec xref-regexp xref)) (note (match:substring match 1)) (node-name (match:substring match 2)) (node-name (if (string=? "" node-name) note node-name)) (node-name (substring node-name (string-skip node-name char-set:whitespace) (string-length node-name)))) (emit-text "See " out) (display-reference file node-name note))) (define display-text (let ((split-xrefs (infix-splitter xref-regexp #f 'split))) (lambda (line port file display-reference out) (let* ((line (complete-line line port)) (components (split-xrefs line))) ;; in components, every 2nd element is a cross reference ;; also, it always has odd length or length zero (if (not (null? components)) (let loop ((components components)) (emit-text (car components) out) (if (not (null? (cdr components))) (begin (display-xref (cadr components) file display-reference out) (loop (cddr components)))))) (newline out))))) ;; Menus (define menu-regexp (make-regexp "^\\* +Menu:")) (define menu-item-regexp (make-regexp "^\\* +")) (define (char-splitter c) (lambda (s) (cond ((string-index s c) => (lambda (i) (values (substring s 0 i) (substring s (+ 1 i) (string-length s))))) (else (values s ""))))) (define colon-split (char-splitter #\:)) (define (display-menu-item-header line port file display-reference icon-name out) (let ((menu-line-split (infix-splitter menu-item-regexp))) (receive (note rest) (colon-split (cadr (menu-line-split line))) (receive (node-name text) (cond ((string-match ": *(.*)" rest) => (lambda (match) (values note (match:substring match 1)))) ((string-match "^ *([^.]*)\\.? *(.*)" rest) => (lambda (match) (values (match:substring match 1) (match:substring match 2)))) (else (info-gateway-error "invalid menu item"))) (emit-tag out 'dt) (display-reference file node-name note (icon-name 'menu) "*") (newline out) (if (and (not (string=? "" text)) (not (string=? "." text))) (begin (emit-tag out 'dd) (display-text text port file display-reference out))))))) (define (display-menu line port file display-reference icon-name out) (emit-close-tag out 'pre) (with-tag out dl () (let loop ((line line)) (if (eof-object? line) (info-gateway-error "unexpected end of info file")) (display-menu-item-header line port file display-reference icon-name out) (let finish-item-loop () (if (eof-object? line) (info-gateway-error "unexpected end of info file")) (let ((line (read-line port))) (cond ((or (eof-object? line) (node-epilogue? line) (string=? "" line)) (emit-tag out 'pre) (dispatch-line line port file display-reference icon-name out)) ((regexp-exec menu-item-regexp line) (loop line)) (else (display-text line port file display-reference out) (finish-item-loop)))))))) ;; Central dispatch (define (dispatch-line line port file display-reference icon-name out) (cond ((or (eof-object? line) (node-epilogue? line)) #f) ((string=? "" line) (emit-p out) #t) ((regexp-exec menu-regexp line) #t) ;; this should probably be expanded ((regexp-exec menu-item-regexp line) (display-menu line port file display-reference icon-name out)) (else (display-text line port file display-reference out) #t))) (define (display-body port file display-reference icon-name out) (let loop () (let ((line (read-line port))) (if (dispatch-line line port file display-reference icon-name out) (loop))))) (define (display-node node-name find-file display-reference icon-name out) (receive (file node) (parse-node-name node-name) (receive (port file-header node-header up-header prev-header next-header) (find-node file node find-file) (with-tag out html () (with-tag out head () (display-title file node-header up-header prev-header next-header display-reference icon-name out)) (with-tag out body () (with-tag out pre () (display-body port file display-reference icon-name out)))) (close-input-port port)))) ;; Finding nodes (define (ensure-node-prologue port msg) (let ((line (read-line port))) (if (or (eof-object? line) (not (node-prologue line))) (info-gateway-error "invalid info file" msg)))) (define (ensure-regexp-line port regexp msg) (let ((line (read-line port))) (if (or (eof-object? line) (not (string-match regexp line))) (info-gateway-error "invalid info file" msg)))) (define (ensure-tag-table-node port) (ensure-regexp-line port "^Tag Table:" "no tag table")) (define (ensure-indirect-tag-table-header port) (ensure-regexp-line port "^\\(Indirect\\)" "no indirect tag")) (define split-indirection (infix-splitter (make-regexp " *: *"))) (define (parse-indirection line) (let ((l (split-indirection line))) (if (null? (cdr l)) (info-gateway-error "invalid indirection entry in info file") (let ((file (car l)) (seek-pos (string->number (cadr l)))) (if (not seek-pos) (info-gateway-error "invalid indirection entry in info file")) (cons file seek-pos))))) (define (read-indirection-table port) (let loop ((table '())) (let ((line (read-line port))) (if (eof-object? line) (info-gateway-error "invalid info file")) (if (node-epilogue? line) (reverse table) (loop (cons (parse-indirection line) table)))))) (define tag-seek-separator (ascii->char 127)) (define parse-tag (let ((split (infix-splitter (make-regexp ", *"))) (split-field (infix-splitter ": ")) (split-node-info (infix-splitter (string tag-seek-separator)))) (define (barf) (info-gateway-error "invalid tag entry in info file")) (lambda (line) (let* ((fields (map split-field (split line))) (file (cond ((assoc "File" fields) => (lambda (p) (if (null? (cdr p)) (barf)) (cadr p))) (else #f)))) (cond ((assoc "Node" fields) => (lambda (p) (if (null? (cdr p)) (barf)) (let ((s (split-node-info (cadr p)))) (if (null? (cdr p)) (barf)) (let* ((node (car s)) (seek (string->number (cadr s)))) (if (not seek) (barf)) (values node file seek))))) (else (barf))))))) (define (find-tag node port) (let loop () (let ((line (read-line port))) (if (eof-object? line) (info-gateway-error "invalid info file")) (if (regexp-exec node-epilogue-regexp line) (http-error http-status/not-found #f "node not found")) (receive (entry-node file seek) (parse-tag line) (if (string=? node entry-node) (cons file seek) (loop)))))) (define (find-indirection-entry seek-pos indirection-table) (let loop ((table indirection-table)) (if (null? table) (http-error http-status/not-found #f "node not found")) (let* ((entry (car table)) (pos (cdr entry))) (if (and (>= seek-pos pos) (or (null? (cdr table)) (let* ((next-entry (cadr table)) (next-pos (cdr next-entry))) (< seek-pos next-pos)))) entry (loop (cdr table)))))) (define (file-finder with) (cond ((procedure? with) with) ((list? with) (lambda (file) (find-info-file file with))))) (define (find-node-port-with-tag-entry node tag-entry ? find-file) (let* ((port (if (input-port? ?) ? #f)) (indirection-table (if port #f ?)) (seek-pos (cdr tag-entry)) (indirection-entry (and indirection-table (find-indirection-entry seek-pos indirection-table))) (seek-pos (if indirection-entry (- seek-pos (cdr indirection-entry)) seek-pos)) ;; that's what the documentation says ... (seek-pos (if (>= seek-pos 1000) (- seek-pos 1000) 0)) (file (or (car tag-entry) (and indirection-entry (car indirection-entry)))) (port (if file (begin (if port (close-input-port port)) (open-input-file (find-file file))) port))) (seek port seek-pos) port)) (define (find-node file node find-file) (if (not file) (http-error http-status/not-found #f "no file in info node specification")) (let* ((fname (find-file file)) (port (open-input-file fname))) (let loop ((port port)) (let ((line (read-line port))) (if (eof-object? line) (http-error http-status/not-found #f "info node not found")) (if (node-prologue? line) (let ((header (read-line port))) (if (eof-object? header) (info-gateway-error "invalid info file")) (cond ((string-match "^Indirect:" header) (let ((indirection-table (read-indirection-table port))) (ensure-tag-table-node port) (ensure-indirect-tag-table-header port) (let ((tag-entry (find-tag node port))) (close-input-port port) (loop (find-node-port-with-tag-entry node tag-entry indirection-table find-file))))) ((string-match "^Tag Table:" header) (let ((tag-entry (find-tag node port))) (loop (find-node-port-with-tag-entry node tag-entry port find-file)))) ((string-match "^File:" header) (receive (file-header node-header up-header prev-header next-header) (split-header-line header) (if (string=? node-header node) (values port file-header node-header up-header prev-header next-header) (loop port)))) (else (loop port)))) (loop port)))))) ;; Finding files (define (info-file-alternative-names file) (receive (dir base ext) (parse-file-name file) (let* ((base (cond ((string-match "(.*)-info$" base) => (lambda (match) (match:substring match 1))) (else base))) (base-ci (string-map char-downcase base)) (alts-1 (if (string=? base base-ci) (list base) (list base base-ci))) (alts (append alts-1 (map (lambda (base) (string-append base ".info")) alts-1))) (alts (append alts (map (lambda (base) (string-append base "-info")) alts-1))) (alts (map (lambda (f) (string-append dir f)) alts)) (alts (cons file alts))) alts))) (define (find-info-file file info-path) (let ((alts (info-file-alternative-names file))) (let path-loop ((path info-path)) (if (null? path) (http-error http-status/not-found #f "info file not found")) (let alt-loop ((alts alts)) (if (null? alts) (path-loop (cdr path)) (let ((try (string-append (file-name-as-directory (car path)) (car alts)))) (if (file-exists? try) try (alt-loop (cdr alts)))))))))