woman.el 173 KB
Newer Older
Eli Zaretskii's avatar
Eli Zaretskii committed
1 2
;;; woman.el --- browse UN*X manual pages `wo (without) man'

Paul Eggert's avatar
Paul Eggert committed
3
;; Copyright (C) 2000-2019 Free Software Foundation, Inc.
Eli Zaretskii's avatar
Eli Zaretskii committed
4

5
;; Author: Francis J. Wright <F.J.Wright@qmul.ac.uk>
6
;; Maintainer: emacs-devel@gnu.org
7
;; Keywords: help, unix
Eli Zaretskii's avatar
Eli Zaretskii committed
8
;; Adapted-By: Eli Zaretskii <eliz@gnu.org>
9
;; Version: 0.551
10
;; URL: http://centaur.maths.qmul.ac.uk/Emacs/WoMan/
Eli Zaretskii's avatar
Eli Zaretskii committed
11 12 13

;; This file is part of GNU Emacs.

14
;; GNU Emacs is free software: you can redistribute it and/or modify
Eli Zaretskii's avatar
Eli Zaretskii committed
15
;; it under the terms of the GNU General Public License as published by
16 17
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
Eli Zaretskii's avatar
Eli Zaretskii committed
18 19 20 21 22 23 24

;; GNU Emacs 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
25
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
Eli Zaretskii's avatar
Eli Zaretskii committed
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96

;;; Commentary:

;; WoMan implements a subset of the formatting performed by the Emacs
;; `man' (or `manual-entry') command to format a UN*X manual `page'
;; for display, but without calling any external programs.  It is
;; intended to emulate the whole of the -man macro package, plus those
;; ?roff requests that are most commonly used in man pages.  However,
;; the emulation is modified to include the reformatting done by the
;; Emacs `man' command.  No hyphenation is performed.

;; Advantages

;;   Much more direct, does not require any external programs.
;;   Supports completion on man page names.

;; Disadvantages

;;   Not a complete emulation.  Currently no support for eqn or tbl.
;;   Slightly slower for large man pages (but usually faster for
;;   small- and medium-size pages).

;; This browser works quite well on simple well-written man files.  It
;; works less well on idiosyncratic files that `break the rules' or
;; use the more obscure ?roff requests directly.  Current test results
;; are available in the file woman.status.

;; WoMan supports the use of compressed man files via
;; `auto-compression-mode' by turning it on if necessary.  But you may
;; need to adjust the user option `woman-file-compression-regexp'.

;; Read on for (currently) the only documentation for WoMan!

;; See also the documentation for the WoMan interactive commands and
;; user option variables, all of which begin with the prefix `woman-'.
;; This can be done most easily by loading WoMan and then running the
;; command `woman-mini-help', or selecting the WoMan menu option `Mini
;; Help' when WoMan is running.

;; WoMan is still under development!  Please let me know what doesn't
;; work -- I am adding and improving functionality as testing shows
;; that it is necessary.  See below for guidance on reporting bugs.

;; Recommended use
;; ===============

;; Put this in your .emacs:
;;   (autoload 'woman "woman"
;;             "Decode and browse a UN*X man page." t)
;;   (autoload 'woman-find-file "woman"
;;             "Find, decode and browse a specific UN*X man-page file." t)

;; Then either (1 -- *RECOMMENDED*): If the `MANPATH' environment
;; variable is set then WoMan will use it; otherwise you may need to
;; reset the Lisp variable `woman-manpath', and you may also want to
;; set the Lisp variable `woman-path'.  Please see the online
;; documentation for these variables.  Now you can execute the
;; extended command `woman', enter or select a manual entry topic,
;; using completion, and if necessary select a filename, using
;; completion.  By default, WoMan suggests the word nearest to the
;; cursor in the current buffer as the topic.

;; Or (2): Execute the extended command `woman-find-file' and enter a
;; filename, using completion.  This mode of execution may be useful
;; for temporary files outside the standard UN*X manual directory
;; structure.

;; Or (3): Put the next two sexpr's in your .emacs:
;; (autoload 'woman-dired-find-file "woman"
;;   "In dired, run the WoMan man-page browser on this file." t)
;; (add-hook 'dired-mode-hook
97 98
;;          (lambda ()
;;            (define-key dired-mode-map "W" 'woman-dired-find-file)))
Eli Zaretskii's avatar
Eli Zaretskii committed
99 100 101 102 103 104 105 106 107 108 109 110
;; and open the directory containing the man page file using dired,
;; put the cursor on the file, and press `W'.

;; In each case, the result should (!) be a buffer in Man mode showing
;; a formatted manual entry.  When called from WoMan, Man mode should
;; work as advertised, but modified where necessary in the context of
;; WoMan.  (However, `Man' will still invoke the standard Emacs
;; manual-browsing facility rather than `WoMan' -- this is
;; intentional!)

;; (By default, WoMan will automatically define the dired keys "W" and
;; "w" when it loads, but only if they are not already defined.  This
111
;; behavior is controlled by the user option `woman-dired-keys'.
Eli Zaretskii's avatar
Eli Zaretskii committed
112 113 114 115 116 117
;; Note that the `dired-x' (dired extra) package binds
;; `dired-copy-filename-as-kill' to the key "w" (as pointed out by Jim
;; Davidson), although "W" appears to be really unused.  The `dired-x'
;; package will over-write the WoMan binding to "w", whereas (by
;; default) WoMan will not overwrite the `dired-x' binding.)

118 119
;; Using the word at point as the default topic
;; ============================================
Eli Zaretskii's avatar
Eli Zaretskii committed
120

121 122 123 124 125 126
;; The `woman' command uses the word nearest to point in the current
;; buffer as the default topic to look up if it matches the name of a
;; manual page installed on the system.  The default topic can also be
;; used without confirmation by setting the user-option
;; `woman-use-topic-at-point' to t; thanks to Benjamin Riefenstahl for
;; suggesting this functionality.
Eli Zaretskii's avatar
Eli Zaretskii committed
127

128 129
;; The variable `woman-use-topic-at-point' can be rebound locally,
;; which may be useful to provide special private key bindings, e.g.
Eli Zaretskii's avatar
Eli Zaretskii committed
130 131

;;  (global-set-key "\C-cw"
132
;;  		  (lambda ()
Eli Zaretskii's avatar
Eli Zaretskii committed
133
;;  		    (interactive)
134
;;  		    (let ((woman-use-topic-at-point t))
Eli Zaretskii's avatar
Eli Zaretskii committed
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
;;  		      (woman)))))


;; Customization, Hooks and Imenu
;; ==============================

;; WoMan supports the GNU Emacs 20+ customization facility, and puts
;; a customization group called `WoMan' in the `Help' group under the
;; top-level `Emacs' group.  In order to be able to customize WoMan
;; without first loading it, add the following sexp to your .emacs:

;;  (defgroup woman nil
;;     "Browse UNIX manual pages `wo (without) man'."
;;     :tag "WoMan" :group 'help :load "woman")


;; WoMan currently runs two hooks: `woman-pre-format-hook' immediately
;; before formatting a buffer and `woman-post-format-hook' immediately
;; after formatting a buffer.  These hooks can be used for special
;; customizations that require code to be executed, etc., although
;; most customization should be possible by setting WoMan user option
;; variables, e.g. in `.emacs' and should NOT require the use of the
;; hooks.  `woman-pre-format-hook' might be appropriate for face
;; customization, whereas `woman-post-format-hook' might be
;; appropriate for installing a dynamic menu using `imenu' (although
;; it is better to use the built-in WoMan imenu support).

;; The WoMan menu provides an option to make a contents menu for the
;; current man page (using imenu).  Alternatively, if you set the
164
;; variable `woman-imenu' to t then WoMan will do it automatically
Eli Zaretskii's avatar
Eli Zaretskii committed
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
;; for every man page.  The menu title is the value of the variable
;; `woman-imenu-title', which is "CONTENTS" by default.  By default,
;; the menu shows manual sections and subsections, but you can change
;; this by changing the value of `woman-imenu-generic-expression'.
;; This facility is not yet widely tested and may be fooled by obscure
;; man pages that `break the rules'.

;; WoMan is configured not to replace spaces in an imenu *Completion*
;; buffer.  For further documentation of the use of imenu, such as
;; menu sorting, see the source file imenu.el, which is distributed
;; with GNU Emacs.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Howard Melman made (essentially) the following suggestions, which
;; are slightly different from the expression that I currently use.
;; You may prefer one of Howard's suggestions, which I think assume
182
;; that `case-fold-search' is t (which it is by default):
Eli Zaretskii's avatar
Eli Zaretskii committed
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216

;; (setq woman-imenu-generic-expression
;;       '((nil "^\\(   \\)?\\([A-Z][A-Z ]+[A-Z]\\)[ \t]*$" 2)))

;; will give support for .SH and .SS, though it won't show the heading
;; name hierarchy.  If you just want .SH in the imenu then use:

;; (setq woman-imenu-generic-expression
;;       '((nil "^\\([A-Z][A-Z ]+[A-Z]\\)[ \t]*$" 1)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; Vertical spacing and blank lines
;; ================================

;; The number of consecutive blank lines in the formatted buffer
;; should be either 0 or 1.  A blank line should leave a space like
;; .sp 1 (p. 14).  Current policy is to output vertical space only
;; immediately before text is output.


;; Horizontal and vertical spacing and resolution
;; ==============================================

;; WoMan currently assumes 10 characters per inch horizontally, hence
;; a horizontal resolution of 24 basic units, and 5 lines per inch
;; vertically, hence a vertical resolution of 48 basic units.  (nroff
;; uses 240 per inch).


;; The *WoMan-Log* buffer
;; ======================

217
;; This is modeled on the byte-compiler.  It logs all files formatted
Eli Zaretskii's avatar
Eli Zaretskii committed
218 219
;; by WoMan, and if WoMan finds anything that it cannot handle then it
;; writes a warning to this buffer.  If the variable `woman-show-log'
220
;; is non-nil (by default it is nil) then WoMan automatically
Eli Zaretskii's avatar
Eli Zaretskii committed
221 222 223 224 225 226 227 228 229 230
;; displays this buffer.  Many WoMan warnings can be completely
;; ignored, because they are reporting the fact that WoMan has ignored
;; requests that it is correct to ignore.  In some future version this
;; level of paranoia will be reduced, but not until WoMan is more
;; reliable.  At present, all warnings should be treated with some
;; suspicion.  Uninterpreted escape sequences are also logged (in some
;; cases).

;; Uninterpreted ?roff requests can optionally be left in the
;; formatted buffer to indicate precisely where they occur by
231 232
;; resetting the variable `woman-ignore' to nil (by default it is
;; t).
Eli Zaretskii's avatar
Eli Zaretskii committed
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247

;; Automatic initiation of woman decoding

;; (Probably not a good idea.  If you use it, be careful!)

;; Put something like this in your .emacs.  The call to
;; set-visited-file-name is to avoid font-locking triggered by
;; automatic major mode selection.

;; (autoload 'woman-decode-region "woman")

;; (setq format-alist
;;       (cons
;;        '(man "UN*X man-page source format" "\\.\\(TH\\|ig\\) "
;; 	     woman-decode-region nil nil
248 249 250
;; 	     (lambda (arg)
;; 		set-visited-file-name
;; 		(file-name-sans-extension buffer-file-name)))))
Eli Zaretskii's avatar
Eli Zaretskii committed
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
;;       format-alist))


;; Reporting Bugs
;; ==============

;; If WoMan fails completely, or formats a file incorrectly
;; (i.e. obviously wrongly or significantly differently from man) or
;; inelegantly, then please

;; (a) check that you are running the latest version of woman.el
;;     available from my web site (see above),

;; (b) check that the problem is not already described in the file
;;     woman.status, also available from my web site.

;; If both of the above are true then please email me the entry from
;; the *WoMan-Log* buffer relating to the problem file, together with
;; a brief description of the problem.  Please indicate where you got
;; the source file from, but do not send it to me unless I ask you to!
;; Thanks.  (There is at present no automated bug-reporting facility
;; for WoMan.)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; NOTE:

;; CASE-DEPENDENCE OF FILENAMES.  By default, WoMan ignores case in
;; file pathnames only when it seems appropriate.  MS-Windows users
;; who want complete case independence should set the NTEmacs variable
281
;; `w32-downcase-file-names' to t and use all lower case when
Eli Zaretskii's avatar
Eli Zaretskii committed
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
;; setting WoMan file paths.

;; (1) INCOMPATIBLE CHANGE!  WoMan no longer uses a persistent topic
;; cache by default.  (It caused too much confusion!)  Explicitly set
;; the variable `woman-cache-filename' to save the cache between Emacs
;; sessions.  This is recommended only if the command `woman' is too
;; slow the first time that it is run in an Emacs session, while it
;; builds its cache in main memory, which MAY be VERY slow.

;; (2) The user option `woman-cache-level' controls the amount of
;; information cached (in main memory and, optionally, saved to disc).

;; (3) UPDATING THE CACHE.  A prefix argument always causes the
;; `woman' command (only) to rebuild its topic cache, and to re-save
;; it to `woman-cache-filename' if this variable has a non-nil value.
;; This is necessary if the NAMES (not contents) of any of the
;; directories or files in the paths specified by `woman-manpath' or
;; `woman-path' change.  If WoMan user options that affect the cache
;; are changed then WoMan will automatically update its cache file on
;; disc (if one is in use) the next time it is run in a new Emacs
;; session.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; TO DO
;; =====

310
;; Reconsider case sensitivity of file names.
Eli Zaretskii's avatar
Eli Zaretskii committed
311 312 313 314
;; MUST PROCESS .if, .nr IN ORDER ENCOUNTERED IN FILE! (rcsfile, mf).
;; Allow general delimiter in `\v', cf. `\h'.
;; Improve major-mode documentation.
;; Pre-process conditionals in macro bodies if possible for speed?
315 316
;; Emulate more complete preprocessor support for tbl (.TS/.TE)
;; Emulate some preprocessor support for eqn (.EQ/.EN)
Eli Zaretskii's avatar
Eli Zaretskii committed
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
;; Re-write filling and adjusting code!
;; Allow word wrap at comma (for long option lists)?
;; Buffer list handling not quite right.
;; Make 10 or 12 pitch (cpi) optional -- 12 => ll = 78
;; Use unpaddable space for tabbing?
;; Tidy up handling of fonts when filling and adjusting
;;   -- see text/text properties?
;; Improve speed
;; Add font-lock support (for quoted strings, etc.)?
;; Optionally save large files in enriched format?
;; Add apropos facility by searching NAME (?) entry in man files?
;; Documentation -- optional auto-display of formatted WoMan man page?
;; Implement a bug reporter?
;; Support diversion and traps (to some extent) - for Tcl/tk pages?
;; Add a menu of WoMan buffers?
332
;; Fix .fc properly?
Eli Zaretskii's avatar
Eli Zaretskii committed
333 334 335 336 337 338 339 340 341 342 343 344 345 346


;; Implementation strategy [this description is now well out of date!]
;; -- three main passes, each to process respectively:

;;   1) non-breaking `.' requests including font macros
;;   2) \ escape sequences, mainly special characters and font changes
;;   3) breaking `.' requests, mainly filling and justification

;; For each pass, a control function finds and pre-processes the
;; escape or request and then calls the appropriate function to
;; perform the required formatting.  Based originally on enriched.el
;; and format.el.

347 348 349
;; The background information that made this project possible is
;; freely available courtesy of Bell Labs from
;; http://cm.bell-labs.com/7thEdMan/
Eli Zaretskii's avatar
Eli Zaretskii committed
350 351


352 353
;; Acknowledgments
;; ===============
Eli Zaretskii's avatar
Eli Zaretskii committed
354 355 356 357 358 359 360 361

;; For Heather, Kathryn and Madelyn, the women in my life
;; (although they will probably never use it)!

;; I also thank the following for helpful suggestions, bug reports,
;; code fragments, general interest, etc.:
;;   Jari Aalto <jari.aalto@cs.tpu.fi>
;;   Dean Andrews <dean@dra.com>
362
;;   Juanma Barranquero <lekktu@gmail.com>
Eli Zaretskii's avatar
Eli Zaretskii committed
363 364
;;   Karl Berry <kb@cs.umb.edu>
;;   Jim Chapman <jchapman@netcomuk.co.uk>
365
;;   Kin Cho <kin@neoscale.com>
Eli Zaretskii's avatar
Eli Zaretskii committed
366 367 368 369 370 371 372 373 374 375 376 377 378 379
;;   Frederic Corne <frederic.corne@erli.fr>
;;   Peter Craft <craft@alacritech.com>
;;   Charles Curley <ccurley@trib.com>
;;   Jim Davidson <jdavidso@teknowledge.com>
;;   Kevin D'Elia <Kevin.DElia@mci.com>
;;   John Fitch <jpff@maths.bath.ac.uk>
;;   Hans Frosch <jwfrosch@rish.b17c.ingr.com>
;;   Guy Gascoigne-Piggford <ggp@informix.com>
;;   Brian Gorka <gorkab@sanchez.com>
;;   Nicolai Henriksen <nhe@lyngso-industri.dk>
;;   Thomas Herchenroeder <the@software-ag.de>
;;   Alexander Hinds <ahinds@thegrid.net>
;;   Stefan Hornburg <sth@hacon.de>
;;   Theodore Jump <tjump@cais.com>
380
;;   David Kastrup <dak@gnu.org>
Eli Zaretskii's avatar
Eli Zaretskii committed
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
;;   Paul Kinnucan <paulk@mathworks.com>
;;   Jonas Linde <jonas@init.se>
;;   Andrew McRae <andrewm@optimation.co.nz>
;;   Howard Melman <howard@silverstream.com>
;;   Dennis Pixton <dennis@math.binghamton.edu>
;;   T. V. Raman <raman@Adobe.COM>
;;   Bruce Ravel <bruce.ravel@nist.gov>
;;   Benjamin Riefenstahl <benny@crocodial.de>
;;   Kevin Ruland <kruland@seistl.com>
;;   Tom Schutter <tom@platte.com>
;;   Wei-Xue Shi <wxshi@ma.neweb.ne.jp>
;;   Fabio Somenzi <fabio@joplin.colorado.edu>
;;   Karel Sprenger <ks@ic.uva.nl>
;;   Chris Szurgot <szurgot@itribe.net>
;;   Paul A. Thompson <pat@po.cwru.edu>
396
;;   Arrigo Triulzi <arrigo@maths.qmw.ac.uk>
Eli Zaretskii's avatar
Eli Zaretskii committed
397
;;   Geoff Voelker <voelker@cs.washington.edu>
Eli Zaretskii's avatar
Eli Zaretskii committed
398
;;   Eli Zaretskii <eliz@gnu.org>
Eli Zaretskii's avatar
Eli Zaretskii committed
399 400 401 402


;;; Code:

403 404
(defvar woman-version "0.551 (beta)" "WoMan version information.")

Eli Zaretskii's avatar
Eli Zaretskii committed
405
(require 'man)
406
(require 'button)
407
(define-button-type 'WoMan-xref-man-page
408
  :supertype 'Man-abstract-xref-man-page
409 410 411 412 413
  'func (lambda (arg)
	  (woman
	   ;; `woman' cannot deal with arguments that contain a
	   ;; section name, like close(2), so strip the section name.
	   (if (string-match Man-reference-regexp arg)
414
	       (substring arg 0 (match-end 1))
415
	     arg))))
416

Eli Zaretskii's avatar
Eli Zaretskii committed
417
(eval-when-compile			; to avoid compiler warnings
Mario Lang's avatar
Mario Lang committed
418
  (require 'cl-lib)
Eli Zaretskii's avatar
Eli Zaretskii committed
419 420 421
  (require 'dired)
  (require 'apropos))

422 423 424
(defun woman-parse-colon-path (paths)
  "Explode search path string PATHS into a list of directory names.
Allow Cygwin colon-separated search paths on Microsoft platforms.
425
Replace null components by calling `woman-parse-man.conf'.
426 427
As a special case, if PATHS is nil then replace it by calling
`woman-parse-man.conf'."
428
  ;; Based on suggestions by Jari Aalto and Eli Zaretskii.
429 430 431 432 433
  ;; parse-colon-path returns nil for a null path component and
  ;; an empty substring of MANPATH denotes the default list.
  (if (memq system-type '(windows-nt ms-dos))
      (cond ((null paths)
	     (mapcar 'woman-Cyg-to-Win (woman-parse-man.conf)))
434
	    ((string-match-p ";" paths)
435
	     ;; Assume DOS-style path-list...
Mario Lang's avatar
Mario Lang committed
436
	     (mapcan			; splice list into list
437 438 439 440 441
	      (lambda (x)
		(if x
		    (list x)
		  (mapcar 'woman-Cyg-to-Win (woman-parse-man.conf))))
	      (parse-colon-path paths)))
442
	    ((string-match-p "\\`[a-zA-Z]:" paths)
443
	     ;; Assume single DOS-style path...
444
	     (list paths))
445 446
	    (t
	     ;; Assume UNIX/Cygwin-style path-list...
Mario Lang's avatar
Mario Lang committed
447
	     (mapcan			; splice list into list
448 449 450 451 452 453
	      (lambda (x)
		(mapcar 'woman-Cyg-to-Win
			(if x (list x) (woman-parse-man.conf))))
	      (let ((path-separator ":"))
		(parse-colon-path paths)))))
    ;; Assume host-default-style path-list...
Mario Lang's avatar
Mario Lang committed
454
    (mapcan				; splice list into list
455 456 457 458 459
     (lambda (x) (if x (list x) (woman-parse-man.conf)))
     (parse-colon-path (or paths "")))))

(defun woman-Cyg-to-Win (file)
  "Convert an absolute filename FILE from Cygwin to Windows form."
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
  ;; MANPATH_MAP conses are not converted since they presumably map
  ;; Cygwin to Cygwin form.
  (if (consp file)
      file
    ;; Code taken from w32-symlinks.el
    (if (eq (aref file 0) ?/)
	;; Try to use Cygwin mount table via `cygpath.exe'.
	(condition-case nil
	    (with-temp-buffer
	      ;; cygpath -m file
	      (call-process "cygpath" nil t nil "-m" file)
	      (buffer-substring 1 (buffer-size)))
	  (error
	   ;; Assume no `cygpath' program available.
	   ;; Hack /cygdrive/x/ or /x/ or (obsolete) //x/ to x:/
	   (when (string-match "\\`\\(/cygdrive\\|/\\)?/./" file)
476
	     (if (match-beginning 1)		; /cygdrive/x/ or //x/ -> /x/
477 478 479 480 481
		 (setq file (substring file (match-end 1))))
	     (aset file 0 (aref file 1))	; /x/ -> xx/
	     (aset file 1 ?:))		; xx/ -> x:/
	   file))
      file)))
Eli Zaretskii's avatar
Eli Zaretskii committed
482 483 484 485 486 487 488 489 490


;;; User options:

;; NB: Group identifiers must be lowercase!

(defgroup woman nil
  "Browse UNIX manual pages `wo (without) man'."
  :tag "WoMan"
491 492
  :link '(custom-manual "(woman) Top")
  :link '(emacs-commentary-link :tag "Commentary" "woman.el")
Eli Zaretskii's avatar
Eli Zaretskii committed
493 494 495
  :group 'help)

(defcustom woman-show-log nil
496
  "If non-nil then show the *WoMan-Log* buffer if appropriate.
Eli Zaretskii's avatar
Eli Zaretskii committed
497 498 499 500 501
I.e. if any warning messages are written to it.  Default is nil."
  :type 'boolean
  :group 'woman)

(defcustom woman-pre-format-hook nil
502
  "Hook run by WoMan immediately before formatting a buffer.
Eli Zaretskii's avatar
Eli Zaretskii committed
503 504 505 506 507
Change only via `Customization' or the function `add-hook'."
  :type 'hook
  :group 'woman)

(defcustom woman-post-format-hook nil
508
  "Hook run by WoMan immediately after formatting a buffer.
Eli Zaretskii's avatar
Eli Zaretskii committed
509 510 511 512 513 514 515 516 517 518 519 520
Change only via `Customization' or the function `add-hook'."
  :type 'hook
  :group 'woman)


;; Interface options

(defgroup woman-interface nil
  "Interface options for browsing UNIX manual pages `wo (without) man'."
  :tag "WoMan Interface"
  :group 'woman)

521
(defcustom woman-man.conf-path
522
  (let ((path '("/usr/lib" "/etc")))
523 524 525 526 527
    (cond ((eq system-type 'windows-nt)
	   (mapcar 'woman-Cyg-to-Win path))
	  ((eq system-type 'darwin)
	   (cons "/usr/share/misc" path))
	  (t path)))
528
  "List of dirs to search and/or files to try for man config file.
529 530 531 532 533 534
A trailing separator (`/' for UNIX etc.) on directories is
optional, and the filename is used if a directory specified is
the first to start with \"man\" and has an extension starting
with \".conf\".  If MANPATH is not set but a config file is found
then it is parsed instead to provide a default value for
`woman-manpath'."
535 536 537 538
  :type '(repeat string)
  :group 'woman-interface)

(defun woman-parse-man.conf ()
539
  "Parse if possible configuration file for man command.
540
Used only if MANPATH is not set or contains null components.
541 542
Look in `woman-man.conf-path' and return a value for `woman-manpath'.
Concatenate data from all lines in the config file of the form
543
  MANPATH  /usr/man
544
or
545 546
  MANDATORY_MANPATH  /usr/man
or
547 548 549
  OPTIONAL_MANPATH  /usr/man
or
  MANPATH_MAP /opt/bin /opt/man"
550 551 552 553 554 555 556 557 558 559 560
  ;; Functionality suggested by Charles Curley.
  (let ((path woman-man.conf-path)
	file manpath)
    (while (and
	    path
	    (not (and
		  (file-readable-p (setq file (car path)))
		  ;; If not a file then find the file:
		  (or (not (file-directory-p file))
		      (and
		       (setq file
561
			     (directory-files file t "\\`man.*\\.conf[a-z]*\\'" t))
562 563 564 565 566
		       (file-readable-p (setq file (car file)))))
		  ;; Parse the file -- if no MANPATH data ignore it:
		  (with-temp-buffer
		    (insert-file-contents file)
		    (while (re-search-forward
567 568
			    ;; `\(?: ... \)' is a "shy group"
			    "\
569 570
^[ \t]*\\(?:\\(?:MANDATORY_\\|OPTIONAL_\\)?MANPATH[ \t]+\\(\\S-+\\)\\|\
MANPATH_MAP[ \t]+\\(\\S-+\\)[ \t]+\\(\\S-+\\)\\)" nil t)
571 572 573 574
                      (cl-pushnew (if (match-beginning 1)
                                      (match-string 1)
                                    (cons (match-string 2)
                                          (match-string 3)))
Mark Oteiza's avatar
Mark Oteiza committed
575
                                  manpath :test #'equal))
576 577 578 579 580
		    manpath))
		 ))
      (setq path (cdr path)))
    (nreverse manpath)))

581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625
;; Autoload so set-locale-environment can operate on it.
;;;###autoload
(defcustom woman-locale nil
  "String specifying a manual page locale, or nil.
If a manual page is available in the specified locale
\(e.g. \"sv_SE.ISO8859-1\"), it will be offered in preference to the
default version.  Normally, `set-locale-environment' sets this at startup."
  :type '(choice string (const nil))
  :group 'woman-interface
  :version "23.1")

;; FIXME Is this a sensible list of alternatives?
(defun woman-expand-locale (locale)
  "Expand a locale into a list suitable for man page lookup.
Expands a locale of the form LANGUAGE_TERRITORY.CHARSET into the list:
LANGUAGE_TERRITORY.CHARSET LANGUAGE_TERRITORY LANGUAGE.CHARSET LANGUAGE.
The TERRITORY and CHARSET portions may be absent."
  (string-match "\\([^._]*\\)\\(_[^.]*\\)?\\(\\..*\\)?" locale)
  (let ((lang (match-string 1 locale))
        (terr (match-string 2 locale))
        (charset (match-string 3 locale)))
    (delq nil (list locale
                    (and charset terr (concat lang terr))
                    (and charset terr (concat lang charset))
                    (if (or charset terr) lang)))))

(defun woman-manpath-add-locales (manpath)
  "Add locale-specific subdirectories to the elements of MANPATH.
MANPATH is a list of the form of `woman-manpath'.  Returns a list
with those locale-specific subdirectories specified by the action
of `woman-expand-locale' on `woman-locale' added, where they exist."
  (if (zerop (length woman-locale))
      manpath
    (let ((subdirs (woman-expand-locale woman-locale))
          lst dir)
      (dolist (elem manpath (nreverse lst))
        (dolist (sub subdirs)
          (when (file-directory-p
                 (setq dir
                       ;; Use f-n-a-d because parse-colon-path does.
                       (file-name-as-directory
                        (expand-file-name sub (substitute-in-file-name
                                               (if (consp elem)
                                                   (cdr elem)
                                                 elem))))))
626 627 628
            (cl-pushnew (if (consp elem)
                            (cons (car elem) dir)
                          dir)
Mark Oteiza's avatar
Mark Oteiza committed
629
                        lst :test #'equal)))
630
        ;; Non-locale-specific has lowest precedence.
Mark Oteiza's avatar
Mark Oteiza committed
631
        (cl-pushnew elem lst :test #'equal)))))
632

Eli Zaretskii's avatar
Eli Zaretskii committed
633
(defcustom woman-manpath
634 635 636
  ;; Locales could also be added in woman-expand-directory-path.
  (or (woman-manpath-add-locales
       (woman-parse-colon-path (getenv "MANPATH")))
637
      '("/usr/man" "/usr/share/man" "/usr/local/man"))
638
  "List of DIRECTORY TREES to search for UN*X manual files.
Eli Zaretskii's avatar
Eli Zaretskii committed
639 640 641
Each element should be the name of a directory that contains
subdirectories of the form `man?', or more precisely subdirectories
selected by the value of `woman-manpath-man-regexp'.  Non-directory
642 643
and unreadable files are ignored.

644 645 646 647 648
Elements can also be a cons cell indicating a mapping from PATH
to manual trees: if such an element's car is equal to a path
element of the environment variable PATH, the cdr of the cons
cell is included in the directory tree search.

649 650
If not set then the environment variable MANPATH is used.  If no such
environment variable is found, the default list is determined by
651 652 653
consulting the man configuration file if found, which is determined by
the user option `woman-man.conf-path'.  An empty substring of MANPATH
denotes the default list.
Eli Zaretskii's avatar
Eli Zaretskii committed
654

655
Any environment variables (names must have the UN*X-style form $NAME,
656
e.g. $HOME, $EMACSDATA, $emacs_dir) are evaluated first but each
Eli Zaretskii's avatar
Eli Zaretskii committed
657 658 659 660 661 662
element must evaluate to a SINGLE directory name.  Trailing `/'s are
ignored.  (Specific directories in `woman-path' are also searched.)

Microsoft platforms:
I recommend including drive letters explicitly, e.g.

663
  (\"C:/Cygwin/usr/man/\" \"C:/Cygwin/usr/local/man\").
Eli Zaretskii's avatar
Eli Zaretskii committed
664 665

The MANPATH environment variable may be set using DOS semi-colon-
666
separated or UN*X/Cygwin colon-separated syntax (but not mixed)."
667
  :type '(repeat (choice string (cons string string)))
668
  :version "23.1"                    ; added woman-manpath-add-locales
Eli Zaretskii's avatar
Eli Zaretskii committed
669 670 671 672 673 674 675 676 677 678 679 680 681
  :group 'woman-interface)

(defcustom woman-manpath-man-regexp "[Mm][Aa][Nn]"
  "Regexp to match man directories UNDER `woman-manpath' directories.
These normally have names of the form `man?'.  Its default value is
\"[Mm][Aa][Nn]\", which is case-insensitive mainly for the benefit of
Microsoft platforms.  Its purpose is to avoid `cat?', `.', `..', etc."
  ;; Based on a suggestion by Wei-Xue Shi.
  :type 'string
  :group 'woman-interface)

(defcustom woman-path
  (if (eq system-type 'ms-dos) '("$DJDIR/info" "$DJDIR/man/cat[1-9onlp]"))
682
  "List of SPECIFIC DIRECTORIES to search for UN*X manual files.
Eli Zaretskii's avatar
Eli Zaretskii committed
683 684 685 686 687 688 689 690 691 692 693 694 695
For example

  (\"/emacs/etc\").

These directories are searched in addition to the directory trees
specified in `woman-manpath'.  Each element should be a directory
string or nil, which represents the current directory when the path is
expanded and cached.  However, the last component (only) of each
directory string is treated as a regexp \(Emacs, not shell) and the
string is expanded into a list of matching directories.  Non-directory
and unreadable files are ignored.  The default value is nil.

Any environment variables (which must have the UN*X-style form $NAME,
696
e.g. $HOME, $EMACSDATA, $emacs_dir) are evaluated first but each
Eli Zaretskii's avatar
Eli Zaretskii committed
697 698 699
element must evaluate to a SINGLE directory name (regexp, see above).
For example

700
  (\"$EMACSDATA\") [or equivalently (\"$emacs_dir/etc\")].
Eli Zaretskii's avatar
Eli Zaretskii committed
701 702 703 704 705 706 707 708

Trailing `/'s are discarded.  (The directory trees in `woman-manpath'
are also searched.)  On Microsoft platforms I recommend including
drive letters explicitly."
  :type '(repeat (choice string (const nil)))
  :group 'woman-interface)

(defcustom woman-cache-level 2
709
  "The level of topic caching.
Eli Zaretskii's avatar
Eli Zaretskii committed
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
1 - cache only the topic and directory lists
    (the only level before version 0.34 - only for compatibility);
2 - cache also the directories for each topic
    (faster, without using much more memory);
3 - cache also the actual filenames for each topic
    (fastest, but uses twice as much memory).
The default value is currently 2, a good general compromise.
If the `woman' command is slow to find files then try 3, which may be
particularly beneficial with large remote-mounted man directories.
Run the `woman' command with a prefix argument or delete the cache
file `woman-cache-filename' for a change to take effect.
\(Values < 1 behave like 1; values > 3 behave like 3.)"
  :type '(choice (const :tag "Minimal" 1)
		 (const :tag "Default" 2)
		 (const :tag "Maximal" 3))
  :group 'woman-interface)

(defcustom woman-cache-filename nil
728
  "The full pathname of the WoMan directory and topic cache file.
Eli Zaretskii's avatar
Eli Zaretskii committed
729 730 731 732 733 734 735 736 737 738 739
It is used to save and restore the cache between sessions.  This is
especially useful with remote-mounted man page files!  The default
value of nil suppresses this action.  The `standard' non-nil
filename is \"~/.wmncach.el\".  Remember that a prefix argument forces
the `woman' command to update and re-write the cache."
  :type '(choice (const :tag "None" nil)
		 (const :tag "~/.wmncach.el" "~/.wmncach.el")
		 file)
  :group 'woman-interface)

(defcustom woman-dired-keys t
740
  "List of `dired' mode keys to define to run WoMan on current file.
741
E.g. (\"w\" \"W\"), or any non-null atom to automatically define
Eli Zaretskii's avatar
Eli Zaretskii committed
742 743 744 745 746 747 748 749 750 751
\"w\" and \"W\" if they are unbound, or nil to do nothing.
Default is t."
  :type '(choice (const :tag "None" nil)
		 (repeat string)
		 (other :tag "Auto" t))
  :group 'woman-interface)

(defcustom woman-imenu-generic-expression
  '((nil "\n\\([A-Z].*\\)" 1) ; SECTION, but not TITLE
    ("*Subsections*" "^   \\([A-Z].*\\)" 1))
752
  "Imenu support for Sections and Subsections.
Eli Zaretskii's avatar
Eli Zaretskii committed
753 754
An alist with elements of the form (MENU-TITLE REGEXP INDEX) --
see the documentation for `imenu-generic-expression'."
755 756 757 758
  :type '(alist :key-type (choice :tag "Title" (const nil) string)
                :value-type (group (choice (string :tag "Regexp")
                                           function)
                                   integer))
Eli Zaretskii's avatar
Eli Zaretskii committed
759 760 761
  :group 'woman-interface)

(defcustom woman-imenu nil
762
  "If non-nil then WoMan adds a Contents menu to the menubar.
763
It does this by calling `imenu-add-to-menubar'.  Default is nil."
Eli Zaretskii's avatar
Eli Zaretskii committed
764 765 766 767
  :type 'boolean
  :group 'woman-interface)

(defcustom woman-imenu-title "CONTENTS"
768
  "The title to use if WoMan adds a Contents menu to the menubar.
Eli Zaretskii's avatar
Eli Zaretskii committed
769 770 771 772
Default is \"CONTENTS\"."
  :type 'string
  :group 'woman-interface)

773 774 775
(defcustom woman-use-topic-at-point-default nil
  ;; `woman-use-topic-at-point' may be let-bound when woman is loaded,
  ;; in which case its global value does not get defined.
Eli Zaretskii's avatar
Eli Zaretskii committed
776
  ;; `woman-file-name' sets it to this value if it is unbound.
777
  "Default value for `woman-use-topic-at-point'."
Eli Zaretskii's avatar
Eli Zaretskii committed
778
  :type '(choice (const :tag "Yes" t)
779
		 (const :tag "No" nil))
Eli Zaretskii's avatar
Eli Zaretskii committed
780 781
  :group 'woman-interface)

782
(defcustom woman-use-topic-at-point woman-use-topic-at-point-default
783
  "Control use of the word at point as the default topic.
784 785
If non-nil the `woman' command uses the word at point automatically,
without interactive confirmation, if it exists as a topic."
Eli Zaretskii's avatar
Eli Zaretskii committed
786
  :type '(choice (const :tag "Yes" t)
787
		 (const :tag "No" nil))
Eli Zaretskii's avatar
Eli Zaretskii committed
788 789 790 791
  :group 'woman-interface)

(defvar woman-file-regexp nil
  "Regexp used to select (possibly compressed) man source files, e.g.
792
\"\\.\\([0-9lmnt]\\w*\\)\\(\\.\\(g?z\\|bz2\\|xz\\)\\)?\\\\='\".
Eli Zaretskii's avatar
Eli Zaretskii committed
793 794 795 796 797 798 799 800
Built automatically from the customizable user options
`woman-uncompressed-file-regexp' and `woman-file-compression-regexp'.")

(defvar woman-uncompressed-file-regexp)	; for the compiler
(defvar woman-file-compression-regexp)	; for the compiler

(defun set-woman-file-regexp (symbol value)
  "Bind SYMBOL to VALUE and set `woman-file-regexp' as per user customizations.
801
Used as :set cookie by Customize when customizing the user options
Eli Zaretskii's avatar
Eli Zaretskii committed
802 803 804 805 806 807 808 809 810 811 812 813
`woman-uncompressed-file-regexp' and `woman-file-compression-regexp'."
  (set-default symbol value)
  (and (boundp 'woman-uncompressed-file-regexp)
       (boundp 'woman-file-compression-regexp)
       (setq woman-file-regexp
	     (concat woman-uncompressed-file-regexp
		     "\\("
		     (substring woman-file-compression-regexp 0 -2)
		     "\\)?\\'"))))

(defcustom woman-uncompressed-file-regexp
  "\\.\\([0-9lmnt]\\w*\\)"		; disallow no extension
814
  "Do not change this unless you are sure you know what you are doing!
Eli Zaretskii's avatar
Eli Zaretskii committed
815 816 817 818 819 820 821
Regexp used to select man source files (ignoring any compression extension).

The SysV standard man pages use two character suffixes, and this is
becoming more common in the GNU world.  For example, the man pages
in the ncurses package include `toe.1m', `form.3x', etc.

Note: an optional compression regexp will be appended, so this regexp
822
MUST NOT end with any kind of string terminator such as $ or \\\\='."
Eli Zaretskii's avatar
Eli Zaretskii committed
823 824 825 826 827
  :type 'regexp
  :set 'set-woman-file-regexp
  :group 'woman-interface)

(defcustom woman-file-compression-regexp
828
  "\\.\\(g?z\\|bz2\\|xz\\)\\'"
829
  "Do not change this unless you are sure you know what you are doing!
Eli Zaretskii's avatar
Eli Zaretskii committed
830
Regexp used to match compressed man file extensions for which
831
decompressors are available and handled by auto-compression mode,
832 833
e.g. \"\\\\.\\\\(g?z\\\\|bz2\\\\|xz\\\\)\\\\\\='\" for `gzip', `bzip2', or `xz'.
Should begin with \\. and end with \\\\=' and MUST NOT be optional."
834 835 836 837
  ;; Should be compatible with car of
  ;; `jka-compr-file-name-handler-entry', but that is unduly
  ;; complicated, includes an inappropriate extension (.tgz) and is
  ;; not loaded by default!
838
  :version "24.1"                       ; added xz
Eli Zaretskii's avatar
Eli Zaretskii committed
839 840 841 842
  :type 'regexp
  :set 'set-woman-file-regexp
  :group 'woman-interface)

843
(defcustom woman-use-own-frame nil
844
  "If non-nil then use a dedicated frame for displaying WoMan windows.
845 846 847 848
Only useful when run on a graphic display such as X or MS-Windows."
  :type 'boolean
  :group 'woman-interface)

Eli Zaretskii's avatar
Eli Zaretskii committed
849 850 851 852 853 854 855 856 857

;; Formatting options

(defgroup woman-formatting nil
  "Formatting options for browsing UNIX manual pages `wo (without) man'."
  :tag "WoMan Formatting"
  :group 'woman)

(defcustom woman-fill-column 65
858
  "Right margin for formatted text -- default is 65."
Eli Zaretskii's avatar
Eli Zaretskii committed
859 860 861 862 863
  :type 'integer
  :group 'woman-formatting)

(defcustom woman-fill-frame nil
  ;; Based loosely on a suggestion by Theodore Jump:
864
  "If non-nil then most of the window width is used."
Eli Zaretskii's avatar
Eli Zaretskii committed
865 866 867 868
  :type 'boolean
  :group 'woman-formatting)

(defcustom woman-default-indent 5
869
  "Default prevailing indent set by -man macros -- default is 5.
870
Set this variable to 7 to emulate GNU man formatting."
Eli Zaretskii's avatar
Eli Zaretskii committed
871 872 873 874
  :type 'integer
  :group 'woman-formatting)

(defcustom woman-bold-headings t
875
  "If non-nil then embolden section and subsection headings.  Default is t.
876
Heading emboldening is NOT standard `man' behavior."
Eli Zaretskii's avatar
Eli Zaretskii committed
877 878 879 880
  :type 'boolean
  :group 'woman-formatting)

(defcustom woman-ignore t
Glenn Morris's avatar
Glenn Morris committed
881
  "If non-nil then unrecognized requests etc. are ignored.  Default is t.
882
This gives the standard ?roff behavior.  If nil then they are left in
Eli Zaretskii's avatar
Eli Zaretskii committed
883 884 885 886
the buffer, which may aid debugging."
  :type 'boolean
  :group 'woman-formatting)

887
(defcustom woman-preserve-ascii t
888
  "If non-nil, preserve ASCII characters in the WoMan buffer.
889 890 891 892 893 894 895
Otherwise, to save time, some backslashes and spaces may be
represented differently (as the values of the variables
`woman-escaped-escape-char' and `woman-unpadded-space-char'
respectively) so that the buffer content is strictly wrong even though
it should display correctly.  This should be irrelevant unless the
buffer text is searched, copied or saved to a file."
  ;; This option should probably be removed!
Eli Zaretskii's avatar
Eli Zaretskii committed
896 897 898
  :type 'boolean
  :group 'woman-formatting)

899
(defcustom woman-emulation 'nroff
900
  "WoMan emulation, currently either nroff or troff.  Default is nroff.
901 902 903 904 905
Troff emulation is experimental and largely untested.
\(Add groff later?)"
  :type '(choice (const nroff) (const troff))
  :group 'woman-formatting)

Eli Zaretskii's avatar
Eli Zaretskii committed
906 907 908 909 910 911 912 913 914 915 916 917 918

;; Faces:

(defgroup woman-faces nil
  "Face options for browsing UNIX manual pages `wo (without) man'."
  :tag "WoMan Faces"
  :group 'woman
  :group 'faces)

(defcustom woman-fontify
  (or (and (fboundp 'display-color-p) (display-color-p))
      (and (fboundp 'display-graphic-p) (display-graphic-p))
      (x-display-color-p))
919
  "If non-nil then WoMan assumes that face support is available.
Eli Zaretskii's avatar
Eli Zaretskii committed
920 921 922 923 924
It defaults to a non-nil value if the display supports either colors
or different fonts."
  :type 'boolean
  :group 'woman-faces)

925
(defface woman-italic
926
  '((t :inherit italic))
927
  "Face for italic font in man pages."
Eli Zaretskii's avatar
Eli Zaretskii committed
928 929
  :group 'woman-faces)

930
(defface woman-bold
931
  '((t :inherit bold))
932
  "Face for bold font in man pages."
Eli Zaretskii's avatar
Eli Zaretskii committed
933 934
  :group 'woman-faces)

935
(defface woman-unknown
936
  '((t :inherit font-lock-warning-face))
937
  "Face for all unknown fonts in man pages."
Eli Zaretskii's avatar
Eli Zaretskii committed
938 939
  :group 'woman-faces)

940
(defface woman-addition
941
  '((t :inherit font-lock-builtin-face))
942
  "Face for all WoMan additions to man pages."
Eli Zaretskii's avatar
Eli Zaretskii committed
943 944
  :group 'woman-faces)

945
(defun woman-default-faces ()
946
  "Set foreground colors of italic and bold faces to their default values."
Glenn Morris's avatar
Glenn Morris committed
947
  (declare (obsolete "customize the woman-* faces instead." "24.4"))
Eli Zaretskii's avatar
Eli Zaretskii committed
948
  (interactive)
949 950
  (face-spec-set 'woman-italic (face-user-default-spec 'woman-italic))
  (face-spec-set 'woman-bold (face-user-default-spec 'woman-bold)))
Eli Zaretskii's avatar
Eli Zaretskii committed
951

952
(defun woman-monochrome-faces ()
953
  "Set foreground colors of italic and bold faces to that of the default face.
954
This is usually either black or white."
Glenn Morris's avatar
Glenn Morris committed
955
  (declare (obsolete "customize the woman-* faces instead." "24.4"))
Eli Zaretskii's avatar
Eli Zaretskii committed
956
  (interactive)
957 958
  (set-face-foreground 'woman-italic 'unspecified)
  (set-face-foreground 'woman-bold 'unspecified))
Eli Zaretskii's avatar
Eli Zaretskii committed
959 960

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
961 962
;; Experimental font support, initially only for MS-Windows.
(defconst woman-font-support
963
  (eq window-system 'w32)		; Support X later!
964 965 966 967 968 969 970
  "If non-nil then non-ASCII characters and symbol font supported.")

(defun woman-select-symbol-fonts (fonts)
  "Select symbol fonts from a list FONTS of font name strings."
  (let (symbol-fonts)
    ;; With NTEmacs 20.5, the PATTERN option to `x-list-fonts' does
    ;; not seem to work and fonts may be repeated, so ...
971
    (dolist (font fonts)
972
      (and (string-match-p "-Symbol-" font)
973 974
	   (not (member font symbol-fonts))
	   (setq symbol-fonts (cons font symbol-fonts))))
975 976
    symbol-fonts))

977 978 979
(declare-function x-list-fonts "xfaces.c"
		  (pattern &optional face frame maximum width))

980
(when woman-font-support
981
  (make-face 'woman-symbol)
Eli Zaretskii's avatar
Eli Zaretskii committed
982

983
  ;; Set the symbol font only if `woman-use-symbol-font' is true, to
Eli Zaretskii's avatar
Eli Zaretskii committed
984 985
  ;; avoid unnecessarily upsetting the line spacing in NTEmacs 20.5!

986
  (defcustom woman-use-extended-font t
987
    "If non-nil then may use non-ASCII characters from the default font."
988 989 990 991
    :type 'boolean
    :group 'woman-faces)

  (defcustom woman-use-symbol-font nil
992
    "If non-nil then may use the symbol font.
993 994
It is off by default, mainly because it may change the line spacing
\(in NTEmacs 20.5)."
Eli Zaretskii's avatar
Eli Zaretskii committed
995 996 997 998
    :type 'boolean
    :group 'woman-faces)

  (defconst woman-symbol-font-list
999 1000 1001
    (or (woman-select-symbol-fonts (x-list-fonts "*" 'default))
	(woman-select-symbol-fonts (x-list-fonts "*")))
    "Symbol font(s), preferably same size as default when WoMan was loaded.")
Eli Zaretskii's avatar
Eli Zaretskii committed
1002 1003

  (defcustom woman-symbol-font (car woman-symbol-font-list)
1004
    "A string describing the symbol font to use for special characters.
Eli Zaretskii's avatar
Eli Zaretskii committed
1005 1006 1007 1008
It should be compatible with, and the same size as, the default text font.
Under MS-Windows, the default is
  \"-*-Symbol-normal-r-*-*-*-*-96-96-p-*-ms-symbol\"."
    :type `(choice
1009
	    ,@(mapcar (lambda (x) (list 'const x))
Eli Zaretskii's avatar
Eli Zaretskii committed
1010 1011 1012 1013 1014 1015
		      woman-symbol-font-list)
	    string)
    :group 'woman-faces)

  )

1016 1017 1018 1019
;; For non windows-nt ...
(defvar woman-use-extended-font nil)
(defvar woman-use-symbol-font nil)
(defvar woman-symbol-font nil)
Eli Zaretskii's avatar
Eli Zaretskii committed
1020 1021 1022 1023 1024
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;; Internal variables:

1025
(defconst woman-justify-styles [left right center full]
Eli Zaretskii's avatar
Eli Zaretskii committed
1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
  "Justify styles for `fill-region-as-paragraph'.")
(defconst woman-adjust-left 0		; == adjust off, noadjust
  "Adjustment indicator `l' -- adjust left margin only.")
(defconst woman-adjust-right 1
  "Adjustment indicator `r' -- adjust right margin only.")
(defconst woman-adjust-center 2
  "Adjustment indicator `c' -- center.")
(defconst woman-adjust-both 3		; default -- adj,both
  "Adjustment indicator `b' or `n' -- adjust both margins.")

(defvar woman-adjust woman-adjust-both
  "Current adjustment number-register value.")
(defvar woman-adjust-previous woman-adjust
  "Previous adjustment number-register value.")
1040
(defvar woman-justify (aref woman-justify-styles woman-adjust)
Eli Zaretskii's avatar
Eli Zaretskii committed
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064
  "Current justification style for `fill-region-as-paragraph'.")
(defvar woman-justify-previous woman-justify
  "Previous justification style for `fill-region-as-paragraph'.")

(defvar woman-left-margin woman-default-indent
  "Current left margin.")
(defvar woman-prevailing-indent woman-default-indent
  "Current prevailing indent.")
(defvar woman-interparagraph-distance 1
  "Interparagraph distance in lines.
Set by .PD; used by .SH, .SS, .TP, .LP, .PP, .P, .IP, .HP.")
(defvar woman-leave-blank-lines nil
  "Blank lines to leave as vertical space.")
(defconst woman-tab-width 5
  "Default tab width set by -man macros.")
(defvar woman-nofill nil
  "Current fill mode: nil for filling.")
(defvar woman-RS-left-margin nil
  "Left margin stack for nested use of `.RS/.RE'.")
(defvar woman-RS-prevailing-indent nil
  "Prevailing indent stack for nested use of `.RS/.RE'.")
(defvar woman-nospace nil
  "Current no-space mode: nil for normal spacing.
Set by `.ns' request; reset by any output or `.rs' request")
1065 1066 1067
;; Used for message logging
(defvar WoMan-current-file nil)		; bound in woman-really-find-file
(defvar WoMan-Log-header-point-max nil)
Eli Zaretskii's avatar
Eli Zaretskii committed
1068 1069

(defsubst woman-reset-nospace ()
1070
  "Set `woman-nospace' to nil."
Eli Zaretskii's avatar
Eli Zaretskii committed
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
  (setq woman-nospace nil))

(defconst woman-request-regexp "^[.'][ \t]*\\(\\S +\\) *"
  ;; Was "^\\.[ \t]*\\([a-z0-9]+\\) *" but cvs.1 uses a macro named
  ;; "`" and CGI.man uses a macro named "''"!
  ;; CGI.man uses ' as control character in places -- it *should*
  ;; suppress breaks!
  ;; Could end with "\\( +\\|$\\)" instead of " *"
  "Regexp to match a ?roff request plus trailing white space.")

(defvar woman-imenu-done nil
1082
  "Buffer-local: set to true if function `woman-imenu' has been called.")
Eli Zaretskii's avatar
Eli Zaretskii committed
1083 1084 1085 1086 1087 1088 1089 1090 1091
(make-variable-buffer-local 'woman-imenu-done)

;; From imenu.el -- needed when reformatting a file in its old buffer.
;; The latest buffer index used to update the menu bar menu.
(eval-when-compile
  (require 'imenu))
(make-variable-buffer-local 'imenu--last-menubar-index-alist)

(defvar woman-buffer-alist nil
1092 1093
  "An alist representing WoMan buffers that are already decoded.
Each element is of the form (FILE-NAME . BUFFER-NAME).")
Eli Zaretskii's avatar
Eli Zaretskii committed
1094 1095 1096 1097 1098

(defvar woman-buffer-number 0
  "Ordinal number of current buffer entry in `woman-buffer-alist'.
The ordinal numbers start from 0.")

1099 1100 1101
(defvar woman-if-conditions-true '(?n ?e ?o)
  "List of one-character built-in condition names that are true.
Should include ?e, ?o (page even/odd) and either ?n (nroff) or ?t (troff).
1102
Default is (?n ?e ?o).  Set via `woman-emulation'.")
1103

Eli Zaretskii's avatar
Eli Zaretskii committed
1104 1105 1106 1107 1108 1109

;;; Specialized utility functions:

;;; Fast deletion without saving on the kill ring (cf. simple.el):

(defun woman-delete-line (&optional arg)
1110
  "Delete rest of current line; if all blank then delete thru newline.
Eli Zaretskii's avatar
Eli Zaretskii committed
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156
With a numeric argument ARG, delete that many lines from point.
Negative arguments delete lines backward."
  ;; This is a non-interactive version of kill-line in simple.el that
  ;; deletes instead of killing and assumes kill-whole-line is nil,
  ;; which is essential!
  (delete-region (point)
		 (progn
		   (if arg
		       (forward-line arg)
		     (if (eobp)
			 (signal 'end-of-buffer nil))
		     (if (looking-at "[ \t]*$")
			 (forward-line 1)
		       (end-of-line)))
		 (point))))

(defsubst woman-delete-whole-line ()
  "Delete current line from beginning including eol."
  (beginning-of-line)
  (woman-delete-line 1))

(defsubst woman-delete-following-space ()
  "Delete all spaces and tabs FOLLOWING point (cf. `delete-horizontal-space')."
  ;; cf. delete-horizontal-space in simple.el:
  (delete-region (point) (progn (skip-chars-forward " \t") (point))))

(defsubst woman-delete-match (subexp)
  "Delete subexpression SUBEXP of buffer text matched by last search."
  (delete-region (match-beginning subexp) (match-end subexp)))

;; delete-char does not kill by default
;; delete-backward-char does not kill by default
;; delete-horizontal-space does not kill
;; delete-blank-lines does not kill


;;; File handling:

(defvar woman-expanded-directory-path nil
  "Expanded directory list cache.  Resetting to nil forces update.")

(defvar woman-topic-all-completions nil
  "Expanded topic alist cache.  Resetting to nil forces update.")

;;;###autoload
(defun woman (&optional topic re-cache)
1157
  "Browse UN*X man page for TOPIC (Without using external Man program).
Eli Zaretskii's avatar
Eli Zaretskii committed
1158 1159 1160 1161
The major browsing mode used is essentially the standard Man mode.
Choose the filename for the man page using completion, based on the
topic selected from the directories specified in `woman-manpath' and
`woman-path'.  The directory expansions and topics are cached for
1162
speed.  With a prefix argument, force the caches to be
Eli Zaretskii's avatar
Eli Zaretskii committed
1163 1164
updated (e.g. to re-interpret the current directory).

1165 1166
Used non-interactively, arguments are optional: if given then TOPIC
should be a topic string and non-nil RE-CACHE forces re-caching."
Eli Zaretskii's avatar
Eli Zaretskii committed
1167 1168
  (interactive (list nil current-prefix-arg))
  ;; The following test is for non-interactive calls via gnudoit etc.
1169
  (if (or (not (stringp topic)) (string-match-p "\\S " topic))
Eli Zaretskii's avatar
Eli Zaretskii committed
1170 1171 1172 1173 1174
      (let ((file-name (woman-file-name topic re-cache)))
	(if file-name
	    (woman-find-file file-name)
	  (message
	   "WoMan Error: No matching manual files found in search path")
1175
	  (ding)))
Eli Zaretskii's avatar
Eli Zaretskii committed
1176
    (message "WoMan Error: No topic specified in non-interactive call")
1177
    (ding)))
Eli Zaretskii's avatar
Eli Zaretskii committed
1178

1179
;; Allow WoMan to be called via the standard Help menu:
Eli Zaretskii's avatar
Eli Zaretskii committed
1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192
(define-key-after menu-bar-manuals-menu [woman]
  '(menu-item "Read Man Page (WoMan)..." woman
	:help "Man-page documentation Without Man") t)

(defvar woman-cached-data nil
  "A list of cached data used to determine cache validity.
Set from the cache by `woman-read-directory-cache'.")

(defun woman-cached-data ()
  "Generate a list of data used to determine cache validity.
Called both to generate and to check the cache!"
  ;; Must use substituted paths because values of env vars may change!
  (list woman-cache-level
1193 1194 1195 1196 1197 1198 1199
	(let (lst path)
	  (dolist (dir woman-manpath (nreverse lst))
	    (when (consp dir)
	      (unless path
		(setq path
		      (split-string (getenv "PATH") path-separator t)))
	      (setq dir (and (member (car dir) path) (cdr dir))))
Mark Oteiza's avatar
Mark Oteiza committed
1200 1201
	    (when dir
              (cl-pushnew (substitute-in-file-name dir) lst :test #'equal))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1202 1203 1204 1205
	(mapcar 'substitute-in-file-name woman-path)))

(defun woman-read-directory-cache ()
  "Load the directory and topic cache.
1206 1207
It is loaded from the file named by the variable `woman-cache-filename'.
Return t if the file exists, nil otherwise."
Eli Zaretskii's avatar
Eli Zaretskii committed
1208 1209 1210 1211 1212 1213 1214
  (and
   woman-cache-filename
   (load woman-cache-filename t nil t)	; file exists
   (equal woman-cached-data (woman-cached-data)))) ; cache valid

(defun woman-write-directory-cache ()
  "Save the directory and topic cache.
1215
It is saved to the file named by the variable `woman-cache-filename'."
Eli Zaretskii's avatar
Eli Zaretskii committed
1216
  (if woman-cache-filename
1217
      (with-current-buffer (generate-new-buffer "WoMan tmp buffer")
Eli Zaretskii's avatar
Eli Zaretskii committed
1218
	;; Make a temporary buffer; name starting with space "hides" it.
1219
	(let ((standard-output (current-buffer))
Eli Zaretskii's avatar
Eli Zaretskii committed
1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237
	      (backup-inhibited t))
	  ;; (switch-to-buffer standard-output t) ; only for debugging
	  (buffer-disable-undo standard-output)
	  (princ
	   ";;; WoMan directory and topic cache -- generated automatically\n")
	  (print
	   ;; For data validity check:
	   `(setq woman-cached-data ',(woman-cached-data)))
	  (print
	   `(setq woman-expanded-directory-path
		  ',woman-expanded-directory-path))
	  (print
	   `(setq woman-topic-all-completions
		  ',woman-topic-all-completions))
	  (write-file woman-cache-filename) ; write CURRENT buffer
	  (kill-buffer standard-output)
	  ))))

1238
(defvaralias 'woman-topic-history 'Man-topic-history)
Eli Zaretskii's avatar
Eli Zaretskii committed
1239 1240 1241 1242
(defvar woman-file-history nil "File-name read history.")

(defun woman-file-name (topic &optional re-cache)
  "Get the name of the UN*X man-page file describing a chosen TOPIC.
1243 1244 1245 1246 1247
When `woman' is called interactively, the word at point may be
automatically used as the topic, if the value of the user option
`woman-use-topic-at-point' is non-nil.  Return nil if no file can
be found.  Optional argument RE-CACHE, if non-nil, forces the
cache to be re-read."
Eli Zaretskii's avatar
Eli Zaretskii committed
1248
  ;; Handle the caching of the directory and topic lists:
1249 1250 1251 1252
  (unless (and (not re-cache)
	       (or
		(and woman-expanded-directory-path woman-topic-all-completions)
		(woman-read-directory-cache)))
Eli Zaretskii's avatar
Eli Zaretskii committed
1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263
    (message "Building list of manual directory expansions...")
    (setq woman-expanded-directory-path
	  (woman-expand-directory-path woman-manpath woman-path))
    (message "Building completion list of all manual topics...")
    (setq woman-topic-all-completions
	  (woman-topic-all-completions woman-expanded-directory-path))
    (woman-write-directory-cache))
  ;; There is a problem in that I want to offer case-insensitive
  ;; completions, but to return only a case-sensitive match.  This
  ;; does not seem to work properly by default, so I re-do the
  ;; completion if necessary.
1264
  (let (files)
Eli Zaretskii's avatar
Eli Zaretskii committed
1265
    (or (stringp topic)
1266 1267 1268 1269 1270 1271
	(and (if (boundp 'woman-use-topic-at-point)
		 woman-use-topic-at-point
	       ;; Was let-bound when file loaded, so ...
	       (setq woman-use-topic-at-point woman-use-topic-at-point-default))
	     (setq topic (or (current-word t) "")) ; only within or adjacent to word
	     (test-completion topic woman-topic-all-completions))
Eli Zaretskii's avatar
Eli Zaretskii committed
1272
	(setq topic
1273 1274 1275 1276 1277 1278 1279 1280
	      (let* ((word-at-point (current-word))
		     (default
		       (when (and word-at-point
				  (test-completion
				   word-at-point woman-topic-all-completions))
			 word-at-point)))
		(completing-read
		 (if default
1281
		     (format "Manual entry (default %s): " default)
1282 1283 1284 1285 1286
		   "Manual entry: ")
		 woman-topic-all-completions nil 1
		 nil
		 'woman-topic-history
		 default))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1287
    ;; Note that completing-read always returns a string.
1288
    (unless (= (length topic) 0)
Eli Zaretskii's avatar
Eli Zaretskii committed
1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301
      (cond
       ((setq files (woman-file-name-all-completions topic)))
       ;; Complete topic more carefully, i.e. use the completion
       ;; rather than the string entered by the user:
       ((setq files (all-completions topic woman-topic-all-completions))
	(while (/= (length topic) (length (car files)))
	  (setq files (cdr files)))
	(setq files (woman-file-name-all-completions (car files)))))
      (cond
       ((null files) nil)		; no file found for topic.
       ((null (cdr files)) (car (car files))) ; only 1 file for topic.
       (t
	;; Multiple files for topic, so must select 1.
1302 1303 1304 1305 1306 1307
	;; Run the command `minibuffer-complete' in order to automatically
	;; complete the minibuffer contents as far as possible.
        (minibuffer-with-setup-hook
            (lambda () (let ((this-command this-command)) (minibuffer-complete)))
          (completing-read "Manual file: " files nil 1
                           (try-completion "" files) 'woman-file-history)))))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324

(defun woman-select (predicate list)
  "Select unique elements for which PREDICATE is true in LIST.
\(Note that this function changes the value of LIST.)"
  ;; Intended to be fast by avoiding recursion and list copying.
  (while (and list
	      (or
	       (member (car list) (cdr list))
	       (not (funcall predicate (car list)))))
    (setq list (cdr list)))
  (if list
      (let ((newlist list) cdr_list)
	(while (setq cdr_list (cdr list))
	  (if (and
	       (not (member (car cdr_list) (cdr cdr_list)))
	       (funcall predicate (car cdr_list)))
	      (setq list cdr_list)
1325
	    (setcdr list (cdr cdr_list))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1326 1327 1328 1329 1330 1331 1332 1333
	newlist)))

(defun woman-file-readable-p (dir)
  "Return t if DIR is readable, otherwise log a warning."
  (or (file-readable-p dir)
      (WoMan-warn "Ignoring unreadable `manpath' directory tree `%s'!" dir)))

(defun woman-directory-files (head dir)
1334
  "Return a sorted list of files in directory HEAD matching regexp in DIR.
Eli Zaretskii's avatar
Eli Zaretskii committed
1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348
Value is a sorted list of the absolute pathnames of all the files in
directory HEAD, or the current directory if HEAD is nil, that match the
regexp that is the final component of DIR.  Log a warning if list is empty."
  (or (directory-files
       (or head (directory-file-name default-directory)) ; was "."
       t
       (file-name-nondirectory dir))
      (WoMan-warn "No directories match `woman-path' entry `%s'!" dir)))

(defun woman-file-accessible-directory-p (dir)
  "Return t if DIR is accessible, otherwise log a warning."
  (or (file-accessible-directory-p dir)
      (WoMan-warn "Ignoring inaccessible `man-page' directory `%s'!" dir)))

1349 1350 1351 1352 1353
(defun woman-expand-directory-path (path-dirs path-regexps)
  "Expand the manual directories in PATH-DIRS and PATH-REGEXPS.
PATH-DIRS should be a list of general manual directories (like
`woman-manpath'), while PATH-REGEXPS should be a list of specific
manual directory regexps (like `woman-path').
Eli Zaretskii's avatar
Eli Zaretskii committed
1354 1355
Ignore any paths that are unreadable or not directories."
  ;; Allow each path to be a single string or a list of strings:
1356 1357
  (if (not (listp path-dirs)) (setq path-dirs (list path-dirs)))
  (if (not (listp path-regexps)) (setq path-regexps (list path-regexps)))
1358
  (let (head dirs path)
1359
    (dolist (dir path-dirs)
1360 1361 1362 1363 1364
      (when (consp dir)
	(unless path
	  (setq path (split-string (getenv "PATH") path-separator t)))
	(setq dir (and (member (car dir) path)
		       (cdr dir))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1365 1366 1367 1368 1369 1370 1371 1372
      (if (and dir (woman-file-readable-p dir))
	  ;; NB: `parse-colon-path' creates null elements for
	  ;; redundant (semi-)colons and trailing `/'s!
	  ;; If does not actually matter here if dir ends with `/'.
	  ;; Need regexp "man" here to avoid "cat?", `.', `..', etc.
	  (setq dir (woman-canonicalize-dir dir)
		dirs (nconc dirs (directory-files
				  dir t woman-manpath-man-regexp)))))
1373
    (dolist (dir path-regexps)
Eli Zaretskii's avatar
Eli Zaretskii committed
1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386
      (if (or (null dir)
	      (null (setq dir (woman-canonicalize-dir dir)
			  head (file-name-directory dir)))
	      (woman-file-readable-p head))
	  (setq dirs
		(if dir
		    (nconc dirs (woman-directory-files head dir))
		  (cons (directory-file-name default-directory) dirs))
		;; was "." -- at head of list for later filtering
		)))
    (woman-select 'woman-file-accessible-directory-p dirs)))

(defun woman-canonicalize-dir (dir)
1387
  "Canonicalize the directory name DIR.
Eli Zaretskii's avatar
Eli Zaretskii committed
1388 1389 1390 1391 1392 1393
Any UN*X-style environment variables are evaluated first."
  (setq dir (expand-file-name (substitute-in-file-name dir)))
  ;; A path that ends with / matches all directories in it,
  ;; including `.' and `..', so remove any trailing / !!!
  (if (string= (substring dir -1) "/")
      (setq dir (substring dir 0 -1)))
Juanma Barranquero's avatar
Juanma Barranquero committed
1394
  (if (memq system-type '(windows-nt ms-dos cygwin)) ; what else?
Eli Zaretskii's avatar
Eli Zaretskii committed
1395 1396 1397 1398 1399 1400
      ;; Match capitalization used by `file-name-directory':
      (setq dir (concat (file-name-directory dir)
			(file-name-nondirectory dir))))
  dir)

(defsubst woman-not-member (dir path)
1401
  "Return t if DIR is not a member of the list PATH, nil otherwise.
Eli Zaretskii's avatar
Eli Zaretskii committed
1402 1403 1404 1405 1406 1407 1408 1409 1410 1411
If DIR is `.' it is first replaced by the current directory."
  (not (member dir path)))

(defun woman-topic-all-completions (path)
  "Return an alist of the man files in all man directories in the list PATH.
The cdr of each alist element is the path-index / filename."
  ;; Support 3 levels of caching: each element of the alist `files'
  ;; will be a list of the first `woman-cache-level' elements of the
  ;; following list: (topic path-index filename).  This alist `files'
  ;; is re-processed by `woman-topic-all-completions-merge'.
1412
  (let (dir files (path-index 0))	; indexing starts at zero
Eli Zaretskii's avatar
Eli Zaretskii committed
1413
    (while path
1414
      (setq dir (pop path))
Eli Zaretskii's avatar
Eli Zaretskii committed
1415
      (if (woman-not-member dir path)	; use each directory only once!
1416 1417
	  (push (woman-topic-all-completions-1 dir path-index)
		files))
Eli Zaretskii's avatar
Eli Zaretskii committed
1418
      (setq path-index (1+ path-index)))
Paul Eggert's avatar
Paul Eggert committed
1419
    ;; Uniquify topics:
Paul Eggert's avatar
Paul Eggert committed
1420
    ;; Concatenate all lists with a single nconc call to
1421 1422 1423
    ;; avoid retraversing the first lists repeatedly  -- dak
    (woman-topic-all-completions-merge
     (apply #'nconc files))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1424 1425

(defun woman-topic-all-completions-1 (dir path-index)
1426 1427 1428 1429 1430 1431 1432 1433 1434 1435
  "Return an alist of the man topics in directory DIR with index PATH-INDEX.
A topic is a filename sans type-related extensions.
Support 3 levels of caching: each element of the alist will be a list
of the first `woman-cache-level' elements from the following list:
\(topic path-index filename)."
  ;; This function used to check that each file in the directory was
  ;; not itself a directory, but this is very slow and should be
  ;; unnecessary.  So let us assume that `woman-file-regexp' will
  ;; filter out any directories, which probably should not be there
  ;; anyway, i.e. it is a user error!
1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457
  ;;
  ;; Don't sort files: we do that when merging, anyway.  -- dak
  (let (newlst (lst (directory-files dir nil woman-file-regexp t))
	       ;; Make an explicit regexp for stripping extension and
	       ;; compression extension: file-name-sans-extension is a
	       ;; far too costly function.  -- dak
	       (ext (format "\\(\\.[^.\\/]*\\)?\\(%s\\)?\\'"
			    woman-file-compression-regexp)))
    ;; Use a loop instead of mapcar in order to avoid the speed
    ;; penalty of binding function arguments.  -- dak
      (dolist (file lst newlst)
	(push
	 (cons
	  (if (string-match ext file)
	      (substring file 0 (match-beginning 0))
	    file)
	  (and (> woman-cache-level 1)
	       (cons
		path-index
		(and (> woman-cache-level 2)
		     (list file)))))
	 newlst))))
Eli Zaretskii's avatar
Eli Zaretskii committed
1458 1459 1460

(defun woman-topic-all-completions-merge (alist)
  "Merge the alist ALIST so that the keys are unique.
1461
Also make each path-info component into a list.
Eli Zaretskii's avatar
Eli Zaretskii committed
1462
\(Note that this function changes the value of ALIST.)"
1463 1464
  ;; Replaces unreadably "optimized" O(n^2) implementation.
  ;; Instead we use sorting to merge stuff efficiently.  -- dak
1465
  (let (newalist)
1466 1467 1468 1469
    ;; Sort list into reverse order
    (setq alist (sort alist (lambda(x y) (string< (car y) (car x)))))
    ;; merge duplicate keys.
    (if (> woman-cache-level 1)
1470
	(dolist (elt alist)
1471 1472 1473 1474 1475 1476
	  (if (equal (car elt) (caar newalist))
	      (unless (member (cdr elt) (cdar newalist))
		(setcdr (car newalist) (cons (cdr elt)
					     (cdar newalist))))
	    (setcdr elt (list (cdr elt)))
	    (push elt newalist)))
1477 1478
      ;; woman-cache-level = 1 => elements are single-element lists ...
      (dolist (elt alist)
1479 1480 1481
	(unless (equal (car elt) (caar newalist))
	  (push elt newalist))))
    newalist))
Eli Zaretskii's avatar
Eli Zaretskii committed
1482 1483 1484 1485 1486 1487 1488 1489

(defun woman-file-name-all-completions (topic)
  "Return an alist of the files in all man directories that match TOPIC."
  ;; Support 3 levels of caching: each element of
  ;; woman-topic-all-completions is a list of one of the forms:
  ;;   (topic)
  ;;   (topic (path-index) (path-index) ... )
  ;;   (topic (path-index filename) (path-index filename) ... )
Juanma Barranquero's avatar
Juanma Barranquero committed
1490
  ;; where there are no duplicates in the value lists.
Eli Zaretskii's avatar
Eli Zaretskii committed
1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503
  ;; Topic must match first `word' of filename, so ...
  (let ((topic-regexp
	 (concat
	  "\\`" (regexp-quote topic)	; first `word'
	  "\\(\\..+\\)*"		; optional subsequent `words'
	  woman-file-regexp))		; extension
	(topics woman-topic-all-completions)
	(path woman-expanded-directory-path)
	dir files)
    (if (cdr (car topics))
	;; Use cached path-info to locate files for each topic:
	(let ((path-info (cdr (assoc topic topics)))
	      filename)
1504 1505 1506
	  (dolist (elt path-info)
	    (setq dir (nth (car elt) path)
		  filename (car (cdr elt))
Eli Zaretskii's avatar
Eli Zaretskii committed
1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517