;;; markdown-preview-mode.el --- markdown realtime preview minor mode. ;; Copyright (C) 2014 ;; Author: Igor Shymko ;; URL: https://github.com/ancane/markdown-preview-mode ;; Keywords: markdown, preview ;; Version: 0.8 ;; Package-Requires: ((websocket "1.6") (markdown-mode "2.1") (cl-lib "0.5") (web-server "0.1.1")) ;; This file is not part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3, or (at your option) ;; any later version. ;; ;; This program 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 ;; along with GNU Emacs. If not, see . ;;; Commentary: ;; ;; This package makes use of websockets to deliver rendered markdown to a web browser. ;; Updates happen upon buffer save or on idle. ;; ;;; Code: (require 'cl-lib) (require 'websocket) (require 'markdown-mode) (require 'web-server) (defgroup markdown-preview nil "Markdown preview mode." :group 'text :prefix "markdown-preview-" :link '(url-link "https://github.com/ancane/markdown-preview-mode")) (defcustom markdown-preview-ws-host "localhost" "Markdown preview websocket server address." :group 'markdown-preview :type 'string) (defcustom markdown-preview-ws-port 7379 "Markdown preview websocket server port." :group 'markdown-preview :type 'integer) (defcustom markdown-preview-http-port 9000 "Markdown preview http server port." :group 'markdown-preview :type 'integer) (defcustom markdown-preview-style "http://thomasf.github.io/solarized-css/solarized-dark.min.css" "Markdown preview style URI." :group 'markdown-preview :type 'string) (defcustom markdown-preview-file-name ".markdown-preview.html" "Markdown preview file name." :group 'markdown-preview :type 'string) (defvar markdown-preview-javascript (list "http://code.jquery.com/jquery-1.11.0.min.js") "List of javascript libs for preview.") (defvar markdown-preview--websocket-server nil "`markdown-preview' Websocket 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--preview-template (concat (file-name-directory load-file-name) "preview.html") "Location of `markdown-preview' html template file.") (defvar markdown-preview--idle-timer nil "Preview idle timer.") (defun markdown-preview--stop-idle-timer () "Stop the `markdown-preview' idle timer." (when (timerp markdown-preview--idle-timer) (cancel-timer markdown-preview--idle-timer))) (defun markdown-preview--open-browser-preview () "Open the markdown preview in the browser." (let* ((dir-of-buffer-to-preview (file-name-directory (buffer-file-name))) (preview-file (concat dir-of-buffer-to-preview markdown-preview-file-name))) (with-temp-file preview-file (insert-file-contents markdown-preview--preview-template) (if (search-forward "${MD_STYLE}" nil t) (replace-match markdown-preview-style t)) (if (search-forward "${MD_JS}" nil t) (replace-match (mapconcat (lambda (x) (concat "" )) markdown-preview-javascript "\n") t)) (if (search-forward "${WS_HOST}" nil t) (replace-match markdown-preview-ws-host t)) (if (search-forward "${WS_PORT}" nil t) (replace-match (format "%s" markdown-preview-ws-port) t)) (buffer-string)) (lexical-let ((docroot default-directory)) (ws-start (lambda (request) (with-slots (process headers) request (let ((path (substring (cdr (assoc :GET headers)) 1))) (if (ws-in-directory-p docroot path) (if (file-directory-p path) (ws-send-directory-list process (expand-file-name path docroot) "^[^\.]") (ws-send-file process (expand-file-name path docroot))) (ws-send-404 process))))) markdown-preview-http-port)) (setq markdown-preview-http-port (+ markdown-preview-http-port 1)) (browse-url preview-file))) (defun markdown-preview--stop-websocket-server () "Stop the `markdown-preview' websocket server." (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))) (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))) (defun markdown-preview--start-websocket-server () "Start `markdown-preview' websocket server." (when (not markdown-preview--websocket-server) (setq markdown-preview--websocket-server (websocket-server 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)) :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))) (defun markdown-preview--start-local-client () "Start the `markdown-preview' local client." (when (not markdown-preview--local-client) (setq markdown-preview--local-client (websocket-open (format "ws://%s:%d" markdown-preview-ws-host markdown-preview-ws-port) :on-error (lambda (ws type err) (message "error connecting")) :on-close (lambda (websocket) (setq markdown-preview--local-client nil)))))) (defun markdown-preview--send-preview () "Send the `markdown-preview' preview to clients." (when (bound-and-true-p markdown-preview-mode) (markdown-preview--send-preview-to markdown-preview--local-client))) (defun markdown-preview--send-preview-to (websocket) "Send the `markdown-preview' to a specific WEBSOCKET." (let ((mark-position-percent (number-to-string (truncate (* 100 (/ (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) (websocket-send-text websocket (concat "
" "" mark-position-percent "" "
" (buffer-substring-no-properties (point-min) (point-max)) "
" "
") )))) (defun markdown-preview--start () "Start `markdown-preview' mode." (markdown-preview--start-websocket-server) (markdown-preview--start-local-client) (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)) (defun markdown-preview--stop () "Stop `markdown-preview' mode." (remove-hook 'after-save-hook 'markdown-preview--send-preview t) (markdown-preview--stop-idle-timer) (let ((preview-file (concat (file-name-directory (buffer-file-name)) markdown-preview-file-name))) (if (file-exists-p preview-file) (delete-file preview-file)))) ;;;###autoload (defun markdown-preview-open-browser () "Open the `markdown-preview' in the browser." (interactive) (markdown-preview--open-browser-preview)) ;;;###autoload (defun markdown-preview-cleanup () "Cleanup `markdown-preview' mode." (interactive) (markdown-preview--stop-websocket-server)) ;;;###autoload (define-minor-mode markdown-preview-mode "Markdown preview mode." :group 'markdown-preview :init-value nil (when (not (or (equal major-mode 'markdown-mode) (equal major-mode 'gfm-mode))) (markdown-mode)) (if markdown-preview-mode (markdown-preview--start) (markdown-preview--stop))) (provide 'markdown-preview-mode) ;;; markdown-preview-mode.el ends here