mirror of
https://github.com/vale981/emacs-jupyter
synced 2025-03-05 15:41:37 -05:00
278 lines
12 KiB
EmacsLisp
278 lines
12 KiB
EmacsLisp
;; TODO: Encapsulate the notebook state in some struct so it is easier to
|
|
;; reason about which parts are stateful.
|
|
|
|
(require 'jupyter-messages) ; For `jupyter--empty-dict'
|
|
(require 'jupyter-kernelspec)
|
|
(eval-when-compile
|
|
(require 'ob-core); For `org-babel-map-src-blocks'
|
|
(require 'subr-x))
|
|
|
|
(org-export-define-derived-backend 'jupyter 'md
|
|
:translate-alist
|
|
'((src-block . jupyter-notebook-src-block)
|
|
(paragraph . jupyter-notebook-paragraph)
|
|
(section . jupyter-notebook-section)
|
|
(headline . jupyter-notebook-headline)
|
|
(fixed-width . jupyter-notebook-cell)
|
|
(inner-template . jupyter-notebook-json))
|
|
:menu-entry
|
|
'(?j "Export to Jupyter Notebook"
|
|
((?J "As Notebook" jupyter-org-export-as-notebook))))
|
|
|
|
(defsubst jupyter-notebook--markdown-cell-p (info)
|
|
(equal "markdown" (plist-get (jupyter-notebook--current-cell info) :cell_type)))
|
|
|
|
(defsubst jupyter-notebook--code-cell-p (info)
|
|
(equal "code" (plist-get (jupyter-notebook--current-cell info) :cell_type)))
|
|
|
|
(defsubst jupyter-notebook--result-drawer-p (elem)
|
|
(and (eq (org-element-type elem) 'drawer)
|
|
(equal "RESULTS"
|
|
(upcase (org-element-property :drawer-name elem)))))
|
|
|
|
;; From the `org-mode' documentation
|
|
;;
|
|
;; A section contains directly any greater element or element. Only a headline
|
|
;; can contain a section. As an exception, text before the first headline in
|
|
;; the document also belongs to a section.
|
|
;; (document
|
|
;; (section)
|
|
;; (headline
|
|
;; (section)
|
|
;; (headline)
|
|
;; (headline
|
|
;; (headline))))
|
|
;;
|
|
;; Once a section is complete, the headline nodes gets called so I need to have
|
|
;; some state that allows me to put the headline at the front.
|
|
|
|
|
|
;; Take the current cells and put them in a section, the node that gets called
|
|
;; right after this one is the headline node which is then responsible for
|
|
;; modifying the head of the section to add the headline
|
|
(defun jupyter-notebook-section (_data contents info)
|
|
(message "section %s" contents)
|
|
(let ((cells (plist-get info :jupyter-cells)))
|
|
(plist-put info :jupyter-sections
|
|
(cons cells (plist-get info :jupyter-sections)))
|
|
(plist-put info :jupyter-cells nil))
|
|
"")
|
|
|
|
;; TODO: Figure out when this gets called
|
|
(defun jupyter-notebook-headline (data contents info)
|
|
(message "headline %s" contents)
|
|
(let* ((cell (jupyter-notebook--current-cell info))
|
|
(source (plist-get cell :source)))
|
|
(plist-put cell :source
|
|
(nconc source
|
|
;; TODO: Be able to control when to create a new markdown
|
|
;; cell? At what level of subtree should markdown cells
|
|
;; be created? For top-level subtrees or have a subtree
|
|
;; property that states when to create a subtree?
|
|
(list "\n\n")
|
|
(list (concat (make-string (org-element-property :level data) ?#) " "
|
|
(org-element-property :raw-value data))))))
|
|
"")
|
|
|
|
(defun jupyter-notebook-paragraph (elem contents info)
|
|
(message "paragraph %s" contents)
|
|
(let (context)
|
|
(cond
|
|
((jupyter-notebook--markdown-cell-p info)
|
|
(let* ((cell (jupyter-notebook--current-cell info))
|
|
(source (plist-get cell :source)))
|
|
(plist-put cell :source
|
|
(nconc source
|
|
;; TODO: Be able to control when to create a new
|
|
;; markdown cell? At what level of subtree should
|
|
;; markdown cells be created? For top-level subtrees
|
|
;; or have a subtree property that states when to
|
|
;; create a subtree?
|
|
(list "\n\n")
|
|
(jupyter-notebook-split-source contents)))))
|
|
((jupyter-notebook--result-p
|
|
(save-excursion
|
|
(goto-char (jupyter-org-element-begin-after-affiliated elem))
|
|
;; To handle links we need to get the context
|
|
(setq context (org-element-context)))
|
|
info)
|
|
;; TODO: Rename this function to better represent what it does. It is a
|
|
;; node in the parse tree that gets a source block's results.
|
|
(jupyter-notebook-cell context nil info))
|
|
(t
|
|
(jupyter-notebook--begin-cell
|
|
(jupyter-notebook-markdown-cell
|
|
:source (jupyter-notebook-split-source contents))
|
|
info))))
|
|
"")
|
|
|
|
(defun jupyter-notebook--complete-cell (info)
|
|
(when-let* ((elem (plist-get info :jupyter-current-cell)))
|
|
;; TODO: Handle the execute-result output. The last output should be
|
|
;; translated into an execute result if need be. This will be somewhat
|
|
;; difficult to do since there is no way to distinguish between an execute
|
|
;; result or a stream or display-data result. So don't really know what the
|
|
;; strategy should be.
|
|
(let ((cells (plist-get info :jupyter-cells)))
|
|
(push elem cells)
|
|
(plist-put info :jupyter-cells cells)
|
|
(plist-put info :jupyter-current-cell nil))))
|
|
|
|
(defun jupyter-notebook--begin-cell (elem info)
|
|
(when (plist-get info :jupyter-current-cell)
|
|
(jupyter-notebook--complete-cell info))
|
|
(plist-put info :jupyter-current-cell elem))
|
|
|
|
(defun jupyter-notebook--current-cell (info)
|
|
(plist-get info :jupyter-current-cell))
|
|
|
|
(defun jupyter-notebook-src-block (src-block contents info)
|
|
(message "src-block %s" contents)
|
|
(jupyter-notebook--begin-cell
|
|
(jupyter-notebook-code-cell
|
|
;; TODO: Global execution count
|
|
:execution-count 0
|
|
:source (string-trim-right (org-element-property :value src-block)))
|
|
info)
|
|
""
|
|
;; TODO: (1) If INFO contains intermediate output, complete the previous cell
|
|
;; object
|
|
;;
|
|
;; TODO: (2) Start a source block object and store it in INFO as the
|
|
;; :jupyter-intermediate object. On the next element transcoded, if it
|
|
;; doesn't contain a RESULTS post-affiliated keyword, complete the source
|
|
;; block as not containing any outputs.
|
|
;;
|
|
;; TODO: (3) So source blocks act as boundaries on when to complete objects.
|
|
)
|
|
|
|
(defun jupyter-notebook--result-p (elem info)
|
|
"Return non-nil if ELEM corresponds to the results of a source block.
|
|
ELEM is the results of a source block if the current cell being
|
|
processed in INFO is a code cell, ELEM has a RESULTS affiliated
|
|
keyword, and ELEM passes `jupyter-org-babel-result-p'."
|
|
(and (jupyter-notebook--code-cell-p info)
|
|
;; If ELEM is associated with a #+RESULTS: keyword, it is considered a
|
|
;; result
|
|
(cl-loop
|
|
with el = elem
|
|
while el thereis (org-element-property :results el)
|
|
do (setq el (org-element-property :parent el)))
|
|
(or (jupyter-org-babel-result-p elem)
|
|
;; TODO: Isn't a result drawer also a babel result?
|
|
(jupyter-notebook--result-drawer-p elem))))
|
|
|
|
(defun jupyter-notebook--mime-bundle (elem)
|
|
;; TODO: Is there a better way?
|
|
;; NOTE: All elements are initially considered as display-data output, when
|
|
;; the corresponding source block cell is completed, the last element in the
|
|
;; :outputs key is converted into an execute-result if it should be.
|
|
(cl-case (org-element-type elem)
|
|
(table
|
|
(save-restriction
|
|
(narrow-to-region (org-element-property :begin elem)
|
|
(org-element-property :end elem))
|
|
(let ((org-html-format-table-no-css t))
|
|
(jupyter-notebook-display-data-output
|
|
:data (list :text/html (org-export-as 'html nil nil 'body-only))))))
|
|
;; Ambiguity between stream results and final result output
|
|
((fixed-width example-block)
|
|
(jupyter-notebook-stream-output
|
|
;; TODO: How to distinguish between stdout and stderr reliably? Maybe
|
|
;; hijack the switches of an example block or add as affiliated keywords
|
|
;; like ATTR_JUPYTER.
|
|
;;
|
|
;; The example block way would be
|
|
;;
|
|
;; #+BEGIN_EXAMPLE :stream err
|
|
;; #+END_EXAMPLE
|
|
:name "stdout"
|
|
:text (org-element-property :value elem)))
|
|
;; Image links
|
|
(link
|
|
(let* ((path (org-element-property :path elem))
|
|
(ext (file-name-extension path))
|
|
;; TODO: Generalize this into a function and also use in
|
|
;; `jupyter-org-client.el'
|
|
(mime (pcase ext
|
|
("png" :image/png)
|
|
("jpg" :image/jpeg)
|
|
("svg" :image/svg+xml)
|
|
(_ (error "Unsupported file name extension (%s)" ext)))))
|
|
(list mime (with-temp-buffer
|
|
(insert-file-contents-literally path)
|
|
(when (memq mime '(:image/png :image/jpeg))
|
|
(base64-encode-region (point-min) (point-max) 'no-line-breaks))
|
|
(buffer-string))))
|
|
)
|
|
(_
|
|
;; A result that passes `jupyter-org-babel-result-p'
|
|
;; TODO: One issue here is that results that look like tables get
|
|
;; converted into `org-mode' tables and information is lost there.
|
|
;; Probably the best option here is to convert into an `html' table.
|
|
|
|
)))
|
|
|
|
;; TODO: Remember that the export process is depth first, so the higher level
|
|
;; nodes like drawers and sections will already have parsed contents of the
|
|
;; elements they contain. What I need to do is tag with text properties parts
|
|
;; of the buffer that correspond to notebook elements, then during the export
|
|
;; process, check those properties to parse them differently.
|
|
;;
|
|
;; 1. Add a hook function to org-export-before-parsing-hook that marks org
|
|
;; elements that correspond to source block output as such.
|
|
;;
|
|
;; 2. During export, if an element has been marked as a source block output,
|
|
;; add it to the outputs of the current source block cell.
|
|
(defun jupyter-notebook-cell (elem contents info)
|
|
(message "cell %s" contents)
|
|
(if (jupyter-notebook--result-p elem info)
|
|
(let ((cell (jupyter-notebook--current-cell info)))
|
|
(plist-put
|
|
cell :outputs (vconcat
|
|
(plist-get cell :outputs)
|
|
(list (jupyter-notebook--mime-bundle elem))))
|
|
"")
|
|
(let (org-export-with-toc)
|
|
(org-export-data-with-backend elem 'md info))))
|
|
|
|
(defvar jupyter-notebook--kernelspec nil)
|
|
|
|
(defun jupyter-notebook-verify-kernelspec (backend)
|
|
"When BACKEND is jupyter, ensure all source blocks use the same kernelspec.
|
|
Raise an error if a source block uses a different kernel,
|
|
otherwise return the kernelspec used for the notebook."
|
|
(when (eq backend 'jupyter)
|
|
(let (spec)
|
|
(org-babel-map-src-blocks nil
|
|
(when (org-babel-jupyter-language-p lang)
|
|
(let* ((info (org-babel-get-src-block-info))
|
|
(block-spec
|
|
(car (jupyter-find-kernelspecs
|
|
(alist-get :kernel (nth 2 info))))))
|
|
(or spec (setq spec block-spec))
|
|
;; TODO: Go to error location
|
|
(unless (equal spec block-spec)
|
|
(user-error "Using different kernels in same notebook")))))
|
|
(setq jupyter-notebook--kernelspec
|
|
(cl-destructuring-bind (name _ . spec) spec
|
|
(list :name name
|
|
:display_name (plist-get spec :display_name)
|
|
:language (plist-get spec :language)))))))
|
|
|
|
(add-hook 'org-export-before-processing-hook #'jupyter-notebook-verify-kernelspec)
|
|
|
|
(defun jupyter-notebook-json (_ info)
|
|
"From the Jupyter related properties in INFO, return the notebook JSON."
|
|
;; Finish up the last cell
|
|
(jupyter-notebook--complete-cell info)
|
|
(json-encode
|
|
(list :cells (apply #'vector (nreverse (plist-get info :jupyter-cells)))
|
|
:metadata (list :kernelspec (prog1 (or jupyter-notebook--kernelspec
|
|
jupyter--empty-dict)
|
|
(setq jupyter-notebook--kernelspec nil))
|
|
:language_info jupyter--empty-dict
|
|
:widgets jupyter--empty-dict)
|
|
:nbformat 4
|
|
:nbformat_minor 1)))
|
|
|