From 73d90fab7c6ebae9a3f4a8663d3d9017d10d176f Mon Sep 17 00:00:00 2001 From: Igor Shymko Date: Sun, 11 Dec 2016 18:44:45 +0200 Subject: [PATCH] Support multiple preview from same http port --- README.md | 2 - markdown-preview-mode.el | 158 +++++++++++++++++++++++++++------------ preview.html | 1 + 3 files changed, 113 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 3ffd4b5..689281a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ Markdown preview mode =========================== - [![MELPA Stable](http://stable.melpa.org/packages/markdown-preview-mode-badge.svg)](http://stable.melpa.org/#/markdown-preview-mode) [![MELPA](http://melpa.org/packages/markdown-preview-mode-badge.svg)](http://melpa.org/#/markdown-preview-mode) @@ -69,7 +68,6 @@ Adjustable by `M-x customize-option` -> `markdown-preview-port`. * `M-x el-get-self-update` * `M-x el-get-install` -> `markdown-preview-mode` - ## Dependencies * [markdown-mode.el](https://github.com/defunkt/markdown-mode) diff --git a/markdown-preview-mode.el b/markdown-preview-mode.el index 82bd50a..1f51b04 100644 --- a/markdown-preview-mode.el +++ b/markdown-preview-mode.el @@ -6,7 +6,7 @@ ;; URL: https://github.com/ancane/markdown-preview-mode ;; Keywords: markdown, gfm, convenience ;; Version: 0.8 -;; Package-Requires: ( (emacs "24.3") (websocket "1.6") (markdown-mode "2.0") (cl-lib "0.5") (web-server "0.1.1")) +;; Package-Requires: ((emacs "24.3") (websocket "1.6") (markdown-mode "2.0") (cl-lib "0.5") (web-server "0.1.1") (uuidgen "0.3")) ;; This file is not part of GNU Emacs. @@ -34,6 +34,7 @@ (require 'websocket) (require 'markdown-mode) (require 'web-server) +(require 'uuidgen) (defgroup markdown-preview nil "Markdown preview mode." @@ -83,11 +84,14 @@ (defvar markdown-preview--websocket-server nil "`markdown-preview' Websocket server.") +(defvar markdown-preview--http-server nil + "`markdown-preview' http server.") + (defvar markdown-preview--local-client nil "`markdown-preview' local client.") -(defvar markdown-preview--remote-clients nil - "List of `markdown-preview' websocket remote clients.") +(defvar markdown-preview--remote-clients (make-hash-table :test 'equal) + "Remote clients hashtable. UUID -> WS") (defvar markdown-preview--home-dir (file-name-directory load-file-name) "`markdown-preview-mode' home directory.") @@ -95,6 +99,12 @@ (defvar markdown-preview--idle-timer nil "Preview idle timer.") +(defvar markdown-preview--uuid nil + "Unique preview identifier.") + +(defvar markdown-preview--preview-buffers (make-hash-table :test 'equal) + "Preview buffers hashtable. UUID -> buffer-name.") + (defun markdown-preview--stop-idle-timer () "Stop the `markdown-preview' idle timer." (when (timerp markdown-preview--idle-timer) @@ -103,8 +113,8 @@ (defun markdown-preview--css-links () "Get list of styles for preview in backward compatible way." (let* ((custom-style (list markdown-preview-style)) - (all-styles - (mapc (lambda (x) (add-to-list 'custom-style x t)) markdown-preview-stylesheets))) + (all-styles + (mapc (lambda (x) (add-to-list 'custom-style x t)) markdown-preview-stylesheets))) (mapconcat (lambda (x) (concat "")) @@ -122,8 +132,9 @@ markdown-preview-javascript "\n")) -(defun markdown-preview--read-preview-template (preview-file) - "Read preview template and writes rendered copy to PREVIEW-FILE, ready to be open in browser." +(defun markdown-preview--read-preview-template (preview-uuid preview-file) + "Read preview template and writes identified by PREVIEW-UUID +rendered copy to PREVIEW-FILE, ready to be open in browser." (with-temp-file preview-file (insert-file-contents (expand-file-name "preview.html" markdown-preview--home-dir)) (if (search-forward "${MD_STYLE}" nil t) @@ -134,49 +145,84 @@ (replace-match markdown-preview-ws-host t)) (if (search-forward "${WS_PORT}" nil t) (replace-match (format "%s" markdown-preview-ws-port) t)) + (if (search-forward "${MD_UUID}" nil t) + (replace-match (format "%s" preview-uuid) t)) (buffer-string))) (defun markdown-preview--start-http-server (port) "Start http server at PORT to serve preview file via http." - (lexical-let ((docroot default-directory)) - (ws-start - (lambda (request) - (with-slots (process headers) request - (let* ((path (substring (cdr (assoc :GET headers)) 1)) - (filename (expand-file-name path docroot))) - ;;(message (format "===> Path: [%s]" path)) - (if (string= path "") - (ws-send-file process (expand-file-name markdown-preview-file-name docroot)) - (if (string= path "favicon.ico") - (ws-send-file process (expand-file-name path markdown-preview--home-dir)) - (if (and (not (file-directory-p filename)) (file-exists-p filename)) - (ws-send-file process filename) - (ws-send-404 process) - )))))) - markdown-preview-http-port))) + (unless markdown-preview--http-server + (lexical-let ((docroot default-directory)) + (setq markdown-preview--http-server + (ws-start + (lambda (request) + (with-slots (process headers) request + (let* ((path (substring (cdr (assoc :GET headers)) 1)) + (filename (expand-file-name path docroot))) + (if (string= path "") + (progn + (ws-send-file + process + (expand-file-name + markdown-preview-file-name + (with-current-buffer + (gethash (markdown-preview--parse-uuid headers) + markdown-preview--preview-buffers) + default-directory + )))) + (if (string= path "favicon.ico") + (ws-send-file process (expand-file-name path markdown-preview--home-dir)) + (if (and (not (file-directory-p filename)) (file-exists-p filename)) + (ws-send-file process filename) + (ws-send-404 process) + )))))) + markdown-preview-http-port))))) + +(defun markdown-preview--parse-uuid (headers) + "Find uuid query param in HEADERS." + (let ((found (cl-find-if (lambda (x) + (when (stringp (car x)) + (equal "uuid" (format "%s" (car x))))) + headers))) + (when found (cdr found)))) + (defun markdown-preview--open-browser-preview () - "Open the markdown preview in the browser." + "Open the markdown preview in the browser." (when (eq markdown-preview-auto-open 'file) (browse-url (expand-file-name markdown-preview-file-name default-directory))) (when (eq markdown-preview-auto-open 'http) - (browse-url (format "http://localhost:%d" markdown-preview-http-port))) + (browse-url + (format "http://localhost:%d/?uuid=%s" markdown-preview-http-port markdown-preview--uuid))) (unless markdown-preview-auto-open - (message (format "Preview address: http://0.0.0.0:%d" markdown-preview-http-port)))) + (message + (format + "Preview address: http://0.0.0.0:%d/?uuid=%s" + markdown-preview-http-port + markdown-preview--uuid)))) (defun markdown-preview--stop-websocket-server () "Stop the `markdown-preview' websocket server." + (clrhash markdown-preview--preview-buffers) (when markdown-preview--local-client (websocket-close markdown-preview--local-client)) (when markdown-preview--websocket-server (delete-process markdown-preview--websocket-server) - (setq markdown-preview--websocket-server nil - markdown-preview--remote-clients nil))) + (setq markdown-preview--websocket-server nil) + (clrhash markdown-preview--remote-clients))) + +(defun markdown-preview--stop-http-server () + "Stop the `markdown-preview' http server." + (when markdown-preview--http-server + (ws-stop markdown-preview--http-server) + (setq markdown-preview--http-server nil))) (defun markdown-preview--drop-closed-clients () "Clean closed clients in `markdown-preview--remote-clients' list." - (setq markdown-preview--remote-clients - (cl-remove-if-not #'websocket-openp markdown-preview--remote-clients))) + (maphash (lambda (ws-uuid websocket) + (unless (websocket-openp websocket)) + (remhash ws-uuid markdown-preview--remote-clients)) + markdown-preview--remote-clients)) (defun markdown-preview--start-websocket-server () "Start `markdown-preview' websocket server." @@ -186,15 +232,21 @@ markdown-preview-ws-port :host markdown-preview-ws-host :on-message (lambda (websocket frame) - (mapc (lambda (ws) (websocket-send ws frame)) - markdown-preview--remote-clients)) - :on-open (lambda (websocket) - (push websocket markdown-preview--remote-clients) - (markdown-preview--send-preview-to websocket)) + (let ((ws-frame-text (websocket-frame-payload frame))) + (if (string-prefix-p "MDPM-Register-UUID: " ws-frame-text) + (let ((ws-uuid (substring ws-frame-text 20))) + (puthash ws-uuid websocket markdown-preview--remote-clients) + (markdown-preview--send-preview-to websocket ws-uuid)) + (progn + (websocket-send + (gethash markdown-preview--uuid markdown-preview--remote-clients) + frame)) + ))) + :on-open (lambda (websocket) (message "Websocket opened")) :on-error (lambda (websocket type err) (message (concat "====> Error:" err))) :on-close (lambda (websocket) (markdown-preview--drop-closed-clients)))) - (add-hook 'kill-emacs-hook 'markdown-preview--stop-websocket-server) - (markdown-preview--open-browser-preview))) + (add-hook 'kill-emacs-hook 'markdown-preview--stop-websocket-server)) + (markdown-preview--open-browser-preview)) (defun markdown-preview--start-local-client () "Start the `markdown-preview' local client." @@ -207,12 +259,12 @@ :on-close (lambda (websocket) (setq markdown-preview--local-client nil)))))) -(defun markdown-preview--send-preview () +(defun markdown-preview--send-preview (preview-uuid) "Send the `markdown-preview' preview to clients." (when (bound-and-true-p markdown-preview-mode) - (markdown-preview--send-preview-to markdown-preview--local-client))) + (markdown-preview--send-preview-to markdown-preview--local-client preview-uuid))) -(defun markdown-preview--send-preview-to (websocket) +(defun markdown-preview--send-preview-to (websocket preview-uuid) "Send the `markdown-preview' to a specific WEBSOCKET." (let ((mark-position-percent (number-to-string @@ -221,8 +273,13 @@ (/ (float (- (line-number-at-pos) (/ (count-screen-lines (window-start) (point)) 2))) (count-lines (point-min) (point-max)))))))) - (markdown markdown-output-buffer-name) - (with-current-buffer (get-buffer markdown-output-buffer-name) + + (let ((md-buffer (gethash preview-uuid markdown-preview--preview-buffers))) + (when md-buffer + (with-current-buffer md-buffer + (markdown markdown-output-buffer-name)))) + + (with-current-buffer markdown-output-buffer-name ;; get-buffer (websocket-send-text websocket (concat "
" @@ -237,20 +294,28 @@ (defun markdown-preview--start () "Start `markdown-preview' mode." + (setq-local markdown-preview--uuid (uuidgen-1)) + (puthash markdown-preview--uuid (buffer-name) markdown-preview--preview-buffers) + ;; (gethash markdown-preview--uuid markdown-preview--preview-buffers) (markdown-preview--read-preview-template + markdown-preview--uuid (expand-file-name markdown-preview-file-name default-directory)) (markdown-preview--start-websocket-server) (markdown-preview--start-local-client) (markdown-preview--start-http-server markdown-preview-http-port) (setq markdown-preview--idle-timer - (run-with-idle-timer 2 t (lambda () (markdown-preview--send-preview)))) - (add-hook 'after-save-hook 'markdown-preview--send-preview nil t)) + (run-with-idle-timer 2 t (lambda () + (when markdown-preview--uuid + (markdown-preview--send-preview markdown-preview--uuid))))) + (add-hook 'after-save-hook (lambda () + (when markdown-preview--uuid + (markdown-preview--send-preview markdown-preview--uuid))) nil t)) (defun markdown-preview--stop () "Stop `markdown-preview' mode." (remove-hook 'after-save-hook 'markdown-preview--send-preview t) (markdown-preview--stop-idle-timer) - (ws-stop-all) + (remhash markdown-preview--uuid markdown-preview--preview-buffers) (let ((preview-file (concat (file-name-directory (buffer-file-name)) markdown-preview-file-name))) (if (file-exists-p preview-file) (delete-file preview-file)))) @@ -265,7 +330,8 @@ (defun markdown-preview-cleanup () "Cleanup `markdown-preview' mode." (interactive) - (markdown-preview--stop-websocket-server)) + (markdown-preview--stop-websocket-server) + (markdown-preview--stop-http-server)) ;;;###autoload (define-minor-mode markdown-preview-mode diff --git a/preview.html b/preview.html index f46182c..4efd7dd 100644 --- a/preview.html +++ b/preview.html @@ -12,6 +12,7 @@ var socket = new WebSocket("ws://${WS_HOST}:${WS_PORT}"); socket.onopen = function() { console.log("Connection established."); + socket.send("MDPM-Register-UUID: ${MD_UUID}"); }; socket.onclose = function(event) { if (event.wasClean) {