sbbs.el

Check-in [9ba1b42fbc]
Login
Overview
Comment:reworked link handling
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256: 9ba1b42fbc4dac2f762a881fbbf550d9bff6033cd610589fdb81d6a2a5fb07f4
User & Date: philip@warpmail.net on 2020-03-07 20:02:43
Other Links: branch diff | manifest | tags
Context
2020-03-07
20:52
Add sbbs-recenter-to-top variable check-in: 0376b0426c user: philip@warpmail.net tags: master, trunk
20:02
reworked link handling check-in: 9ba1b42fbc user: philip@warpmail.net tags: master, trunk
2020-02-29
09:52
interpret server responses as utf-8 check-in: b95de752f7 user: philip@warpmail.net tags: master, trunk
Changes
Hide Diffs Side-by-Side Diffs Ignore Whitespace Patch

Modified sbbs.el from [54b681e7c1] to [7633d1775c].

    22     22   
    23     23   ;;; Code:
    24     24   
    25     25   (require 'tabulated-list)
    26     26   (require 'button)
    27     27   (require 'url)
    28     28   (require 'hl-line)
           29  +(require 'rx)
    29     30   
    30     31    ;; CUSTOMIZABLE DATA
    31     32   
    32     33   (defgroup sbbs nil
    33     34     "SchemeBBS client."
    34     35     :group 'applications
    35     36     :prefix "sbbs-")
................................................................................
    36     37   
    37     38   (defcustom sbbs-boards
    38     39     '(("textboard.org" ("sol" "prog") nil))
    39     40     "List of SchemeBBS sites and boards."
    40     41     :type '(repeat (list (string :tag "Board Domain")
    41     42                          (repeat (string :tag "Board Name"))
    42     43                          (boolean :tag "Use TLS?"))))
           44  +
           45  +(defcustom sbbs-jump-to-link t
           46  +  "Jump to first link after narrowing posts."
           47  +  :type 'boolean)
    43     48   
    44     49   (defface sbbs--spoiler-face
    45     50     '((((background light)) :background "black" :foreground "black")
    46     51       (((background dark)) :background "white" :foreground "white"))
    47     52     "Face for spoiler text in threads.")
    48     53   
    49     54   (defface sbbs--code-face
................................................................................
    58     63   
    59     64   See `sbbs-make-board'.")
    60     65   
    61     66   (defvar-local sbbs--thread-id nil
    62     67     "Buffer local reference to current thread id.
    63     68   
    64     69   Used in thread and reply buffers.")
           70  +
           71  +(defvar-local sbbs--limit-stack nil
           72  +  "Stack of last limit specs.")
    65     73   
    66     74    ;; BOARD OBJECT AND FUNCTIONS
    67     75   
    68     76   (defun sbbs-make-board (domain name &optional tls)
    69     77     "Create board object, using DOMAIN, NAME and TLS flag."
    70     78     (vector domain name tls))
    71     79   
................................................................................
    88     96     (let ((board (or board sbbs--board)))
    89     97       (format "%s://%s/%s%s/%s"
    90     98               (sbbs--board-protocol board)
    91     99               (sbbs--board-domain board)
    92    100               (if api-p "sexp/" "")
    93    101               (sbbs--board-name board)
    94    102               (or path ""))))
          103  +
          104  +(defun sbbs--list-boards ()
          105  +  (let (boards)
          106  +    (dolist (ent sbbs-boards)
          107  +      (dolist (board (cadr ent))
          108  +        (push (sbbs-make-board (car ent) board (caddr ent))
          109  +              boards)))
          110  +    boards))
    95    111   
    96    112   (defun sbbs-read-board ()
    97    113     "Read in a board using `completing-read'.
    98    114   
    99    115   The list will be generated using `sbbs-boards', and the result
   100    116   will be a board object generated with `sbbs-make-board'."
   101    117     (let (boards)
   102         -    (dolist (ent sbbs-boards)
   103         -      (dolist (board (cadr ent))
   104         -        (push (cons (format "/%s/ (%s)" board (car ent))
   105         -                    (sbbs-make-board (car ent) board (caddr ent)))
   106         -              boards)))
          118  +    (dolist (b (sbbs--list-boards))
          119  +      (push (cons (format "/%s/ (%s)"
          120  +                          (sbbs--board-name b)
          121  +                          (sbbs--board-domain b))
          122  +                  b)
          123  +            boards))
   107    124       (cdr (assoc (completing-read "Board: " boards nil t) boards))))
   108    125   
   109    126    ;; UTILITY FUNCTIONS
   110    127   
   111    128   (defun sbbs--reload-thread (&optional _ignore-auto _noconfirm)
   112    129     "Function to reload an opened thread."
   113    130     (when sbbs--thread-id (sbbs-view-open sbbs--thread-id)))
................................................................................
   114    131   
   115    132   (defun sbbs--reload-board ()
   116    133     "Function to regenerate thread index.
   117    134   
   118    135   Called by `tabulated-list-mode' hooks."
   119    136     (when sbbs--board (sbbs-browse sbbs--board t)))
   120    137   
          138  +(defun sbbs--parse-number-range (desc limit)
          139  +  "Generate list of numbers, as specified by DESC.
          140  +
          141  +To avoid memory overflows, limit number of entries to LIMIT."
          142  +  (save-match-data
          143  +    (apply #'nconc
          144  +		   (mapcar
          145  +		    (lambda (range)
          146  +			  (cond ((string-match "\\`\\([[:digit:]]+\\)-\\([[:digit:]]+\\)\\'" range)
          147  +				     (number-sequence (string-to-number (match-string 1 range))
          148  +                                      (min limit (string-to-number (match-string 2 range)))))
          149  +				    ((string-match "\\`\\([[:digit:]]+\\)\\'" range)
          150  +				     (list (string-to-number (match-string 1 range))))
          151  +				    (t (error "invalid range"))))
          152  +		    (split-string desc ",")))))
          153  +
          154  +(defun sbbs--read-jump-to (nr)
          155  +  "Set point to first character of post with number NR."
          156  +  (let ((up (point-min)) (down (point-max)) current)
          157  +    (while (progn
          158  +             (goto-char (+ up (/ (- down up) 2)))
          159  +             (setq current (get-text-property (point) 'sbbs-thread-nr))
          160  +             (/= nr current))
          161  +      (cond ((< nr current) (setq down (point)))
          162  +            ((> nr current) (setq up (point))))))
          163  +  (unless (and (eq 'highlight (get-text-property (point) 'face))
          164  +               (looking-at-p "\\`#[[:digit:]]+"))
          165  +    ;; in case we are on the first character of a post, we shouldn't
          166  +    ;; jump back, since that would mean setting to point to NR-1.
          167  +    (sbbs-read-previous 1)))
          168  +
   121    169    ;; UI GENERATOR
   122    170   
          171  +(defconst sbbs--link-regexp
          172  +  (rx-to-string
          173  +   `(: bos
          174  +       (or (: "/" (group-n 2 (+ alnum))
          175  +              "/" (group-n 3 (+ digit))
          176  +              "/" (group-n 4 (: (+ digit) (? "-" (+ digit)))
          177  +                           (* "," (+ digit) (? "-" (+ digit)))))
          178  +           (: "http" (? "s") "://"
          179  +              (group-n 1 (or ,@(mapcar #'sbbs--board-domain
          180  +                                       (sbbs--list-boards))))
          181  +              "/" (group-n 2 (+ alnum))
          182  +              "/" (group-n 3 (+ digit))
          183  +              (? "#t" (backref 3)
          184  +                 "p" (group-n 4 (+ digit)))))
          185  +       eos))
          186  +  "Regular expression to destruct internal links.")
          187  +
          188  +(defun sbbs--limit-to-range (spec &optional no-push-p)
          189  +  "Hide all posts in the current thread, that aren't in SPEC.
          190  +
          191  +Unless NO-PUSH-P is non-nil, SPEC will be pushed onto
          192  +`sbbs--limit-stack', as to be popped off again by
          193  +`sbbs-show-pop'."
          194  +  (let ((inhibit-read-only t))
          195  +    (remove-list-of-text-properties
          196  +     (point-min) (point-max) '(invisible intangible))
          197  +    (when spec
          198  +      (unless no-push-p
          199  +        (push (cons (point) spec) sbbs--limit-stack))
          200  +      (save-excursion
          201  +        (let ((last (point-max)))
          202  +          (goto-char last)
          203  +          (while (not (bobp))
          204  +            (sbbs-read-previous 1)
          205  +            (unless (memq (get-text-property (point) 'sbbs-thread-nr)
          206  +                          spec)
          207  +              (add-text-properties
          208  +               (point) last '(invisible t intangible t)))
          209  +            (setq last (point)))))
          210  +      (goto-char (point-min))
          211  +      (when spec
          212  +        (sbbs--read-jump-to (car spec)))
          213  +      (let ((point (point)))
          214  +        (when sbbs-jump-to-link
          215  +          (forward-button 1)
          216  +          (when (invisible-p (point))
          217  +            (goto-char point)))))))
          218  +
   123    219   (defun sbbs--insert-link (text link)
   124    220     "Insert link to LINK as TEXT into buffer.
   125    221   
   126    222   If LINK is a (board, thread or site) local link, modify opening
   127    223   behaviour accordingly."
   128         -  (let ((domain (sbbs--board-domain sbbs--board))
   129         -        (name (sbbs--board-name sbbs--board))
   130         -        (id sbbs--thread-id)
   131         -        (local-re "\\`/%s/%d/\\([[:digit:]]+\\)")
   132         -        (thread-re "\\`/%s/\\([[:digit:]]+\\)")
   133         -        (board-re "\\`http\\(s?\\)://\\([-._[:alnum:]]+\\)/sexp/\\([[:alnum:]]+\\)/\\([[:digit:]]+\\)"))
   134         -    (save-match-data
   135         -      (cond ((string-match (format local-re name id) link)
   136         -             (insert-button
   137         -              text
   138         -              'action (lambda (x)
   139         -                        (goto-char (point-min))
   140         -                        (sbbs-read-next (1- (button-get x 'sbbs-post-id))))
   141         -              'sbbs-post-id (string-to-number (match-string 1 link))))
   142         -            ((string-match (format thread-re name id) link)
   143         -             (insert-button
   144         -              text
   145         -              'action (lambda (x)
   146         -                        (sbbs-view-open (button-get x 'sbbs-link-id)))
   147         -              'sbbs-link-id (string-to-number (match-string 1 link))))
   148         -            ((string-match (format board-re domain name) link)
   149         -             (insert-button
   150         -              text
   151         -              'action (lambda (x)
   152         -                        (let ((sbbs--board (button-get x 'sbbs-board)))
   153         -                          (sbbs-view-open (button-get x 'sbbs-link-id))))
   154         -              'sbbs-board (sbbs-make-board (match-string 2 link)
   155         -                                           (match-string 3 link)
   156         -                                           (string= (match-string 1 link) "s"))
   157         -              'sbbs-link-id (string-to-number (match-string 3 link))))
   158         -            (t (insert-button
   159         -                text
   160         -                'action (lambda (x) (browse-url (button-get x 'url)))
   161         -                'url link))))))
          224  +  (save-match-data
          225  +    (let ((match (string-match sbbs--link-regexp link))
          226  +          range id)
          227  +      (when match
          228  +        (when (match-string 4 link)
          229  +          (setq range (sbbs--parse-number-range (match-string 4 link) 300)))
          230  +        (setq id (string-to-number (match-string 3 link))))
          231  +      (let* ((board sbbs--board)
          232  +             (domain (sbbs--board-domain board))
          233  +             (name (sbbs--board-name board))
          234  +             (other (sbbs-make-board (match-string 1 link)
          235  +                                     (match-string 2 link)
          236  +                                     (string-match-p "\\`https://" link)))
          237  +             (func (lambda (&optional _)
          238  +                     (cond ((not match) (browse-url link))
          239  +                           ;; other supported board
          240  +                           ((or (and (sbbs--board-domain other)
          241  +                                     (not (string= (sbbs--board-domain other)
          242  +                                                   domain)))
          243  +                                (not (string= name (sbbs--board-name other))))
          244  +                            (let ((sbbs--board other))
          245  +                              (sbbs-view-open id range)))
          246  +                           ;; other thread
          247  +                           ((/= id sbbs--thread-id)
          248  +                            (let ((sbbs--board board))
          249  +                              (sbbs-view-open id range)))
          250  +                           ;; this thread
          251  +                           (range (sbbs--limit-to-range range))))))
          252  +        (insert-button text 'action func)))))
   162    253   
   163    254   (defun sbbs--insert-sxml-par (sxml)
   164    255     "Insert paragraph contents SXML at point."
   165    256     (dolist (it sxml)
   166    257       (cond ((stringp it)
   167    258              (insert (propertize it 'face 'variable-pitch)))
   168    259             ((eq (car it) 'br)
................................................................................
   223    314   
   224    315   Load results into buffer BUF. STATUS is used to check for
   225    316   errors."
   226    317     (when (buffer-live-p buf)
   227    318       (when (plist-get status :error)
   228    319         (error "Error while loading: %s"
   229    320                (cdr (plist-get status :error))))
   230         -    (forward-paragraph)
   231    321       (decode-coding-region (point-min) (point-max) 'utf-8)
          322  +    (forward-paragraph)
   232    323       (let ((list (read (current-buffer))))
   233    324         (kill-buffer)
   234    325         (with-current-buffer buf
   235    326           (let (ent)
   236    327             (dolist (thread list)
   237    328               (push (list (car thread)
   238    329                           (vector (substring (cdr (assq 'date (cdr thread)))
................................................................................
   243    334                                    (cdr (assq 'headline (cdr thread)))
   244    335                                    'face 'variable-pitch)))
   245    336                     ent))
   246    337             (setq-local tabulated-list-entries ent)
   247    338             (tabulated-list-print t t)
   248    339             (hl-line-highlight))))))
   249    340   
   250         -(defun sbbs--thread-loader (status id buf)
          341  +(defun sbbs--thread-loader (status id buf range)
   251    342     "Callback function for `url-retrieve' when loading thread.
   252    343   
   253    344   The attribute ID determines what thread from board BOARD to
   254    345   load. STATUS is used to check for errors."
   255    346     (when (buffer-live-p buf)
   256    347       (when (plist-get status :error)
   257    348         (error "Error while loading: %s"
................................................................................
   276    367             (erase-buffer)
   277    368             (setq header-line-format
   278    369                   (format "Thread %d: %s" id
   279    370                           (cdr (assq 'headline thread))))
   280    371             (dolist (post (cadr (assq 'posts thread)))
   281    372               (sbbs--thread-insert-post post))
   282    373             (delete-blank-lines)
          374  +          (when range
          375  +            (sbbs--limit-to-range range))
   283    376             (goto-char (point-min)))))))
   284    377   
   285    378    ;; INTERACTIVE FUNCTIONS
   286    379   
   287         -(defun sbbs-view-open (id)
          380  +(defun sbbs-show-all ()
          381  +  "Show all hidden posts."
          382  +  (interactive)
          383  +  (sbbs-show-pop -1))
          384  +
          385  +(defun sbbs-show-pop (&optional n)
          386  +  "Show all hidden posts.
          387  +
          388  +A prefix argument N, repeats this N times. If negative or zero,
          389  +pop all the way up."
          390  +  (interactive "P")
          391  +  (let ((n (or n 1)))
          392  +    (dotimes (_ (if (> n 0) n (length sbbs--limit-stack)))
          393  +      (let ((point (car (pop sbbs--limit-stack))))
          394  +        (sbbs--limit-to-range (cdar sbbs--limit-stack) t)
          395  +        (when point (goto-char point))))))
          396  +
          397  +(defun sbbs-view-open (id &optional range)
   288    398     "Open thread ID in new buffer."
   289    399     (interactive (list (tabulated-list-get-id)))
   290    400     (let ((url (sbbs--board-url (format "/%d" id) t))
   291    401           (headline (or (and (not (tabulated-list-get-entry))
   292    402                              header-line-format)
   293    403                         (substring-no-properties
   294    404                          (aref (tabulated-list-get-entry) 2))))
................................................................................
   299    409                         id))))
   300    410       (with-current-buffer buf
   301    411         (sbbs-read-mode)
   302    412         (when headline
   303    413           (setq header-line-format (format "Thread %d: %s" id headline)))
   304    414         (setq sbbs--board board
   305    415               sbbs--thread-id id))
   306         -    (url-retrieve url #'sbbs--thread-loader (list id buf))
          416  +    (url-retrieve url #'sbbs--thread-loader (list id buf range))
   307    417       (switch-to-buffer buf)))
   308    418   
   309    419   (defun sbbs-view-compose ()
   310    420     "Create buffer to start a new thread."
   311    421     (interactive)
   312    422     (let ((board sbbs--board))
   313    423       (with-current-buffer (generate-new-buffer "*new thread*")
................................................................................
   363    473   (defun sbbs-read-next (arg)
   364    474     "Move point ARG posts forward."
   365    475     (interactive "p")
   366    476     (dotimes (_ arg)
   367    477       (end-of-line)
   368    478       (catch 'found
   369    479         (while (search-forward-regexp "^#" nil t)
   370         -        (when (eq 'highlight (get-text-property (point) 'face))
          480  +        (when (and (eq 'highlight (get-text-property (point) 'face))
          481  +                   (not (get-text-property (point) 'invisible)))
   371    482             (throw 'found t)))))
   372    483     (beginning-of-line))
   373    484   
   374    485   (defun sbbs-read-previous (arg)
   375    486     "Move point ARG posts backwards."
   376    487     (interactive "p")
   377    488     (dotimes (_ arg)
   378    489       (catch 'found
   379    490         (while (search-backward-regexp "^#" nil t)
   380         -        (when (eq 'highlight (get-text-property (point) 'face))
          491  +        (when (and (eq 'highlight (get-text-property (point) 'face))
          492  +                   (not (get-text-property (point) 'invisible)))
   381    493             (throw 'found t)))))
   382    494     (beginning-of-line))
   383    495   
   384    496   ;;;###autoload
   385    497   (defun sbbs-browse (board reload)
   386    498     "Open thread overview for BOARD."
   387    499     (interactive (list (sbbs-read-board) nil))
................................................................................
   426    538     (let ((map (make-sparse-keymap)))
   427    539       (suppress-keymap map)
   428    540       (define-key map (kbd "<tab>") #'forward-button)
   429    541       (define-key map (kbd "<backtab>") #'backward-button)
   430    542       (define-key map (kbd "r") #'sbbs-read-reply)
   431    543       (define-key map (kbd "n") #'sbbs-read-next)
   432    544       (define-key map (kbd "p") #'sbbs-read-previous)
          545  +    (define-key map (kbd "a") #'sbbs-show-pop)
          546  +    (define-key map (kbd "A") #'sbbs-show-all)
   433    547       map))
   434    548   
   435    549   (define-derived-mode sbbs-read-mode special-mode "SchemeBBS Read"
   436    550     "Major mode for reading a thread."
   437    551     (buffer-disable-undo)
   438    552     (visual-line-mode t)
   439    553     (setq-local revert-buffer-function #'sbbs--reload-thread))