diff options
-rw-r--r-- | sx-question-mode.el | 2 | ||||
-rw-r--r-- | sx-question-print.el | 234 | ||||
-rw-r--r-- | sx-request.el | 47 | ||||
-rw-r--r-- | test/test-printing.el | 6 |
4 files changed, 192 insertions, 97 deletions
diff --git a/sx-question-mode.el b/sx-question-mode.el index 2e06de6..561ae23 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -245,7 +245,7 @@ on the current buffer use (set-window-parameter nil 'quit-restore `(other window nil ,(current-buffer)))) - ;; We call font-lock-region manually. See `sx-question-mode--fill-and-fontify' + ;; We call font-lock-region manually. See `sx-question-mode--insert-markdown'. (font-lock-mode -1) (remove-hook 'after-change-functions 'markdown-check-change-for-wiki-link t) (remove-hook 'window-configuration-change-hook diff --git a/sx-question-print.el b/sx-question-print.el index 5799c96..454285d 100644 --- a/sx-question-print.el +++ b/sx-question-print.el @@ -95,6 +95,11 @@ Some faces of this mode might be defined in the `sx-user' group." "Face used for downvoted score in the question buffer." :group 'sx-question-mode-faces) +(defface sx-question-mode-sub-sup + '((t :height 0.7)) + "Face used on <sub> and <sup> tags." + :group 'sx-question-mode-faces) + (defcustom sx-question-mode-header-tags "\nTags: " "String used before the question tags at the header." :type 'string @@ -282,10 +287,9 @@ DATA can represent a question or an answer." 'face 'sx-question-mode-header)) (sx--wrap-in-overlay '(face sx-question-mode-content-face) + (insert "\n") + (sx-question-mode--insert-markdown .body_markdown) (insert "\n" - (sx-question-mode--fill-and-fontify - .body_markdown) - "\n" (propertize sx-question-mode-separator 'face 'sx-question-mode-header))) ;; Clean up commments manually deleted. The `append' call is @@ -335,10 +339,13 @@ The comment is indented, filled, and then printed according to (format sx-question-mode-comments-format (sx-user--format "%d" .owner) (substring - (sx-question-mode--fill-and-fontify - ;; We fill with three spaces at the start, so the comment is - ;; slightly indented. - (concat " " (sx--squash-whitespace .body_markdown))) + ;; We use temp buffer, so that image overlays don't get + ;; inserted with the comment. + (with-temp-buffer + ;; We fill with three spaces at the start, so the comment is + ;; slightly indented. + (sx-question-mode--insert-markdown (concat " " (sx--squash-whitespace .body_markdown))) + (buffer-string)) ;; Then we remove the spaces from the first line, since we'll ;; add the username there anyway. 3)))))) @@ -385,42 +392,106 @@ E.g.: (>= 2 (any lower numeric "/._%&#?=;")))))) "Regexp matching markdown links.") -(defun sx-question-mode--fill-and-fontify (text) - "Return TEXT filled according to `markdown-mode'." - (with-temp-buffer - (insert text) - (delay-mode-hooks (markdown-mode)) - (font-lock-mode -1) - (when sx-question-mode-bullet-appearance - (font-lock-add-keywords ;; Bullet items. - nil - `((,(rx line-start (0+ blank) (group-n 1 (any "*+-")) blank) - 1 '(face nil display ,sx-question-mode-bullet-appearance) prepend)))) - (font-lock-add-keywords ;; Highlight usernames. - nil - `((,(rx (or blank line-start) - (group-n 1 (and "@" (1+ (not space)))) - symbol-end) - 1 font-lock-builtin-face))) - ;; Everything. - (font-lock-fontify-region (point-min) (point-max)) - ;; Compact links. - (sx-question-mode--process-links-in-buffer) - ;; And now the filling - (goto-char (point-min)) - (while (null (eobp)) - ;; Don't fill pre blocks. - (unless (sx-question-mode--dont-fill-here) - (let ((beg (point))) - (skip-chars-forward "\r\n[:blank:]") - (forward-paragraph) - (fill-region beg (point))))) - (replace-regexp-in-string "[[:blank:]]+\\'" "" (buffer-string)))) +(defun sx-question-mode--process-markdown-in-region (beg end) + "Process Markdown text between BEG and END. +This does not do Markdown font-locking. Instead, it fills text, +propertizes links, inserts images, cleans up html comments, and +font-locks code-blocks according to mode." + ;; Paragraph filling + (let ((paragraph-start + "\f\\|[ \t]*$\\|[ \t]*[*+-] \\|[ \t]*[0-9]+\\.[ \t]\\|[ \t]*: ") + (paragraph-separate "\\(?:[ \t\f]*\\|.* \\)$") + (adaptive-fill-first-line-regexp "\\`[ \t]*>[ \t]*?\\'") + (adaptive-fill-function #'markdown-adaptive-fill-function)) + (save-restriction + (save-excursion + (narrow-to-region beg end) + ;; Compact links. + (sx-question-mode--process-links-in-buffer) + (sx-question-mode--process-html-tags (point-min) (point-max)) + ;; And now the filling and other handlings. + (goto-char (point-min)) + (while (null (eobp)) + ;; Don't fill pre blocks. + (unless (sx-question-mode--dont-fill-here) + (let ((beg (point))) + (skip-chars-forward "\r\n[:blank:]") + (forward-paragraph) + (fill-region beg (point))))))))) + +(defun sx-question-mode--insert-markdown (text) + "Return TEXT fontified according to `markdown-mode'." + (let ((beg (point))) + (insert + ;; Font-locking needs to be done in a temp buffer, because it + ;; affects the entire buffer even if we narrow. + (with-temp-buffer + (insert text) + (delay-mode-hooks (markdown-mode)) + (font-lock-mode -1) + (when sx-question-mode-bullet-appearance + (font-lock-add-keywords ;; Bullet items. + nil + `((,(rx line-start (0+ blank) (group-n 1 (any "*+-")) blank) + 1 '(face nil display ,sx-question-mode-bullet-appearance) prepend)))) + (font-lock-add-keywords ;; Highlight usernames. + nil + `((,(rx (or blank line-start) + (group-n 1 (and "@" (1+ (not space)))) + symbol-end) + 1 font-lock-builtin-face))) + ;; Everything. + (font-lock-fontify-region (point-min) (point-max)) + (replace-regexp-in-string "[[:blank:]]+\\'" "" (buffer-string)))) + ;; This part can and should be done in place, this way it can + ;; create overlays. + (sx-question-mode--process-markdown-in-region beg (point)))) + + +;;; HTML tags +(defconst sx-question-mode--html-tag-regexp + (rx "<" (group-n 1 "%s") (* (not (any ">"))) ">")) + +(defun sx-question-mode--process-html-tags (beg end) + "Hide all html tags between BEG and END and possibly interpret them. +END should be a marker." + ;; This code understands nested html, but not if the same tag is + ;; nested in itself (e.g., <kbd><kbd></kbd></kbd>). + (goto-char beg) + (while (search-forward-regexp + (format sx-question-mode--html-tag-regexp "[[:alpha:]]+") + end 'noerror) + (unless (save-match-data (markdown-code-at-point-p)) + (let ((tag (match-string 1)) + (l (match-beginning 0))) + (replace-match "") + (when (search-forward-regexp + (format sx-question-mode--html-tag-regexp (concat "/" tag)) + ;; Searching for a match has no bounds. + nil 'noerror) + (let ((r (copy-marker (match-beginning 0)))) + ;; The code tag is special, because it quotes everything inside. + (if (string= tag "code") + (progn (replace-match "`") + (save-excursion (goto-char l) (insert "`"))) + (replace-match "") + ;; Handle stuff between the two tags. + (save-match-data (sx-question-mode--process-html-tags l r)) + (cond + ((string= tag "kbd") + (add-text-properties l r '(face markdown-inline-code-face))) + ((string= tag "sub") + (add-text-properties + l r '(face sx-question-mode-sub-sup display (raise -0.3)))) + ((string= tag "sup") + (add-text-properties + l r '(face sx-question-mode-sub-sup display (raise +0.3)))))))))))) ;;; Handling links (defun sx-question-mode--process-links-in-buffer () - "Turn all markdown links in this buffer into compact format. + "Turn all markdown links between BEG and ENG into compact format. +END must be a marker. Image links are downloaded and displayed, if `sx-question-mode-use-images' is non-nil." (save-excursion @@ -430,8 +501,7 @@ Image links are downloaded and displayed, if (let ((tag (match-string-no-properties 5))) (if (and tag (> (length tag) 0)) (progn (replace-match "") - ;; `match-string' 6 is the "meta-" prefix. - (sx-tag--insert tag (match-string 6))) + (sx-tag--insert tag)) ;; Other links are link-buttons. (let* ((text (match-string-no-properties 1)) (url (or (match-string-no-properties 2) @@ -439,30 +509,39 @@ Image links are downloaded and displayed, if (sx-question-mode-find-reference (match-string-no-properties 3) text))) - (full-text (match-string-no-properties 0))) + (full-text (match-string-no-properties 0)) + (image-p (and sx-question-mode-use-images + (eq ?! (elt full-text 0))))) (when (stringp url) (replace-match "") (sx-question-mode--insert-link - (if (and sx-question-mode-use-images (eq ?! (elt full-text 0))) - ;; Is it an image? - (sx-question-mode--create-image url) - ;; Or a regular link - (or (if sx-question-mode-pretty-links text full-text) url)) - url)))))))) - -(defun sx-question-mode--create-image (url) - "Get and create an image from URL. + (unless image-p + (or (if sx-question-mode-pretty-links text full-text) + url)) + url) + (when image-p + (sx-question-mode--create-image url (- (point) 2)))))))))) + +(defun sx-question-mode--create-image (url point) + "Get and create an image from URL and insert it at POINT. +The image will take the place of the character at POINT. Its size is bound by `sx-question-mode-image-max-width' and `window-body-width'." - (let* ((image - (create-image (sx-request-get-url url) 'imagemagick t)) - (image-width (car (image-size image 'pixels)))) - (append image - (list :width (min sx-question-mode-image-max-width - (window-body-width nil 'pixel) - image-width))))) - -(defun sx-question-mode--insert-link (text-or-image url) + (let* ((ov (make-overlay point (1+ point) (current-buffer) t nil)) + (callback + (lambda (data) + (let* ((image (create-image data 'imagemagick t)) + (image-width (car (image-size image 'pixels)))) + (overlay-put + ov 'display + (append image + (list :width (min sx-question-mode-image-max-width + (window-body-width nil 'pixel) + image-width)))))))) + (sx-request-get-url url callback) + (overlay-put ov 'face 'default))) + +(defun sx-question-mode--insert-link (text url) "Return a link propertized version of TEXT-OR-IMAGE. URL is used as 'help-echo and 'url properties." ;; For now, the only way to handle nested links is to remove them. @@ -473,26 +552,21 @@ URL is used as 'help-echo and 'url properties." (replace-match "") (forward-char 1) (delete-char 1))) - (let ((imagep (not (stringp text-or-image)))) - ;; Images need to be at the start of a line. - (when (and imagep (not (looking-at-p "^"))) - (insert "\n")) - (apply #'insert-text-button - (if imagep " " text-or-image) - ;; Mouse-over - 'help-echo - (format sx-button--link-help-echo - (propertize (sx--shorten-url url) - 'face 'font-lock-function-name-face)) - ;; For visiting and stuff. - 'sx-button-url url - 'sx-button-copy url - :type 'sx-button-link - ;; The last argument of `apply' is a list. - (when imagep - `(face default display ,text-or-image))) - ;; Images need to be at the end of a line too. - (insert "\n"))) + ;; Images need to be at the start of a line. + (unless (or text (looking-at-p "^")) + (insert "\n")) + (insert-text-button (or text " ") + ;; Mouse-over + 'help-echo + (format sx-button--link-help-echo + (propertize (sx--shorten-url url) + 'face 'font-lock-function-name-face)) + ;; For visiting and stuff. + 'sx-button-url url + 'sx-button-copy url + :type 'sx-button-link) + ;; Images need to be at the end of a line too. + (unless text (insert "\n"))) (defun sx-question-mode-find-reference (id &optional fallback-id) "Find url identified by reference ID in current buffer. diff --git a/sx-request.el b/sx-request.el index d7fd058..3bcea21 100644 --- a/sx-request.el +++ b/sx-request.el @@ -221,25 +221,44 @@ Currently returns nil." "https://raw.githubusercontent.com/vermiculus/sx.el/data/data/%s.el" "Url of the \"data\" directory inside the SX `data' branch.") -(defun sx-request-get-url (url) - "Fetch and return data stored online at URL." +(defun sx-request--read-buffer-data () + "Return the buffer contents after any url headers. +Error if url headers are absent or if they indicate something +went wrong." + (goto-char (point-min)) + (unless (string-match "200" (thing-at-point 'line)) + (error "Page not found.")) + (if (not (search-forward "\n\n" nil t)) + (error "Headers missing; response corrupt") + (prog1 (buffer-substring (point) (point-max)) + (kill-buffer (current-buffer))))) + +(defun sx-request-get-url (url &optional callback) + "Fetch and return data stored online at URL. +If CALLBACK is nil, fetching is done synchronously and the +data (buffer contents sans headers) is returned as a string. + +Otherwise CALLBACK must be a function of a single argument. Then +`url-retrieve' is called asynchronously and CALLBACK is passed +the retrieved data." (let* ((url-automatic-caching t) (url-inhibit-uncompression t) (url-request-method "GET") (url-request-extra-headers '(("Content-Type" . "application/x-www-form-urlencoded"))) - (response-buffer (url-retrieve-synchronously url))) - (if (not response-buffer) - (error "Something went wrong in `url-retrieve-synchronously'") - (with-current-buffer response-buffer - (progn - (goto-char (point-min)) - (unless (string-match "200" (thing-at-point 'line)) - (error "Page not found.")) - (if (not (search-forward "\n\n" nil t)) - (error "Headers missing; response corrupt") - (prog1 (buffer-substring (point) (point-max)) - (kill-buffer (current-buffer))))))))) + (callback-internal + (when callback + ;; @TODO: Error check in STATUS. + (lambda (_status) + (funcall callback (sx-request--read-buffer-data))))) + (response-buffer + (if callback (url-retrieve url callback-internal nil 'silent) + (url-retrieve-synchronously url)))) + (unless callback + (if (not response-buffer) + (error "Something went wrong in `url-retrieve-synchronously'") + (with-current-buffer response-buffer + (sx-request--read-buffer-data)))))) (defun sx-request-get-data (file) "Fetch and return data stored online by SX. diff --git a/test/test-printing.el b/test/test-printing.el index 8016444..850edd8 100644 --- a/test/test-printing.el +++ b/test/test-printing.el @@ -167,8 +167,9 @@ after being run through `sx-tag--format'." "Check complicated questions are filled correctly." (should (equal - (sx-question-mode--fill-and-fontify - "Creating an account on a new site requires you to log into that site using *the same credentials you used on existing sites.* For instance, if you used the Stack Exchange login method, you'd... + (with-temp-buffer + (sx-question-mode--insert-markdown + "Creating an account on a new site requires you to log into that site using *the same credentials you used on existing sites.* For instance, if you used the Stack Exchange login method, you'd... 1. Click the \"Log in using Stack Exchange\" button: @@ -192,6 +193,7 @@ after being run through `sx-tag--format'." [1]: http://i.stack.imgur.com/ktFTs.png [2]: http://i.stack.imgur.com/5l2AY.png [3]: http://i.stack.imgur.com/22myl.png") + (buffer-string)) "Creating an account on a new site requires you to log into that site using *the same credentials you used on existing sites.* For instance, if you used the Stack Exchange login method, you'd... |