From 685cbdc205b3e7755b1d12b5cd692dd3ca5153da Mon Sep 17 00:00:00 2001 From: John Doty Date: Wed, 18 Oct 2017 09:00:58 -0700 Subject: [PATCH] Generate diff commands --- site-lisp/ox-quip.el | 291 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 248 insertions(+), 43 deletions(-) diff --git a/site-lisp/ox-quip.el b/site-lisp/ox-quip.el index f6ef75b..c799aa2 100644 --- a/site-lisp/ox-quip.el +++ b/site-lisp/ox-quip.el @@ -26,11 +26,6 @@ compare properly." (and (stringp elem) (equal 0 (length (string-trim elem))))) -(defun org-quip--clean-element-contents (contents) - "Clean up element contents CONTENTS." - (mapcar #'org-quip--clean-element-tree - (cl-remove-if 'org-quip--remove-element-p (cddr elem)))) - (defun org-quip--clean-string (string tag) "Clean up the text in the STRING contained in TAG, to match good text." (let ((string string)) @@ -79,8 +74,8 @@ get all the strings, nested or not, and that includes inside (org-quip--clean-element-strings (cdr contents) tag)))) )) -(defun org-quip--clean-element-contents (contents tag) - "Clean up the CONTENTS of DOM element with tag TAG. +(defun org-quip--clean-element-contents (contents tag strip) + "Clean up the CONTENTS of element with tag TAG, optionally STRIP IDs. We extract and flatten children from wrapper elements like , , some , &c. The goal is to be able to run @@ -96,12 +91,12 @@ correspondence." ; them in another pass. ((stringp (car contents)) (cons (car contents) - (org-quip--clean-element-contents (cdr contents) tag))) + (org-quip--clean-element-contents (cdr contents) tag strip))) ;
is "\n". ((eq (caar contents) 'br) (cons "\n" - (org-quip--clean-element-contents (cdr contents) tag))) + (org-quip--clean-element-contents (cdr contents) tag strip))) ; Is this an element we're unfolding? ((or (member (caar contents) '(div html body span)) @@ -109,24 +104,28 @@ correspondence." (cdar contents) (not (alist-get 'href (cdar contents))))) (org-quip--clean-element-contents (append (cddar contents) (cdr contents)) - tag)) + tag + strip)) ; This is an element we're keeping. (t (letrec ((new-tag (caar contents)) - (attribs (when (member new-tag '(h1 h2 h3 h4 h5 h6)) + (attribs (when (or (not strip) + (member new-tag '(h1 h2 h3 h4 h5 h6))) (list (cons 'id (org-quip--get-element-id (car contents)))))) - (body (org-quip--clean-element-contents (cddar contents) new-tag)) + (body (org-quip--clean-element-contents (cddar contents) + new-tag + strip)) (new-elem (append (list new-tag attribs) body))) (cons new-elem - (org-quip--clean-element-contents (cdr contents) tag)))) + (org-quip--clean-element-contents (cdr contents) tag strip)))) )) -(defun org-quip--get-cleaned-dom (html) +(defun org-quip--get-cleaned-dom (html &optional strip) "Clean HTML as a list of cleaned DOM elements. The return value is a list of elements, each one having been @@ -137,7 +136,8 @@ to produce comparable results." (insert html) (letrec ((parsed-html (libxml-parse-html-region (point-min) (point-max))) (phase-one (org-quip--clean-element-contents (list parsed-html) - nil)) + nil + strip)) (phase-two (org-quip--clean-element-strings phase-one nil))) (cl-remove-if #'stringp phase-two)))) @@ -380,6 +380,14 @@ Returns the published thread structure." (let ((org-html-toplevel-hlevel 1)) (org-export-as 'html nil nil t)))) +(defun org-quip--export-html-fragment (buffer start end) + (save-excursion + (with-current-buffer buffer + (save-restriction + (narrow-to-region start end) + (let ((org-html-toplevel-hlevel 1)) + (org-export-as 'html nil nil t)))))) + (defun org-quip-publish-to-quip () "Publish the current buffer to Quip." (interactive) @@ -433,46 +441,243 @@ This replaces what's in the buffer so I hope you're OK with that." (with-temp-buffer (mapcar (lambda (elem) + (insert (or (org-quip--get-element-id elem) "nil")) + (insert " ") (prin1 elem (current-buffer)) (insert "\n")) - (org-quip--get-cleaned-dom html)) + (org-quip--get-cleaned-dom html t)) (buffer-string))) +(defun org-quip--find-with-custom-id (custom-id) + "Find the entry with the specified CUSTOM-ID and return the point." + (save-excursion + (goto-char (point-min)) + (when (re-search-forward (concat "^[ \t]*:CUSTOM_ID:[ \t]+" + (regexp-quote custom-id) + "[ \t]*$") + nil t) + (org-back-to-heading t) + (point)))) + +(defun org-quip--get-element-end (element) + (let ((type (org-element-type element)) + (next-element)) + (cond ((eq type 'headline) + (save-excursion + (goto-char (1+ (org-element-property :contents-begin element))) + (setq next-element (org-element-at-point)) + (if (eq (org-element-type next-element) 'property-drawer) + (org-element-property :end next-element) + (org-element-property :contents-begin element)))) + + (t + (org-element-property :end element)) + ))) + +(defun org-quip--get-positions-from-ids (buffer org-id-list) + "With the specified BUFFER and ORG-ID-LIST, compute a list of (start, end) pairs." + (save-excursion + (with-current-buffer buffer + (let ((id-list org-id-list) + (pos-list nil)) + (while id-list + ;; Go to either the start of the element in the ID list, if we have + ;; an ID, or to the end of the previous element if we + ;; don't. (Assuming that this is how it goes.) + (goto-char (if (car id-list) + (org-quip--find-with-custom-id (car id-list)) + (cdar pos-list))) + + ;; Examine the element at point and push the start and end position + ;; of the element. + (letrec ((element (org-element-at-point)) + (begin (org-element-property :begin element)) + (end (org-quip--get-element-end element))) + + ;; (message "SCANNING: %s %s %s %s %s" + ;; (car id-list) + ;; (point) + ;; begin + ;; end + ;; (org-element-type element)) + (setq pos-list (cons (cons begin end) pos-list))) + + (setq id-list (cdr id-list))) + + ;; We were pushing onto pos-list so we gotta reverse it. + (nreverse pos-list))))) + +(defun org-quip--diff-org-and-quip (quip-html org-html) + "Generate a diff between the QUIP-HTML string and ORG-HTML string. + +The strings are 'cleaned' and formatted a little before they are diffed. +Further processing will parse this diff text and turn it into a series of +commands." + (let ((old-diff-text (org-quip--get-cleaned-html-string quip-html)) + (new-diff-text (org-quip--get-cleaned-html-string org-html)) + (old-diff-file (make-temp-file "qod")) + (new-diff-file (make-temp-file "qnd"))) + + (unwind-protect + (progn + (with-temp-file old-diff-file (insert old-diff-text)) + (with-temp-file new-diff-file (insert new-diff-text)) + + (with-temp-buffer + (call-process "diff" + nil (current-buffer) nil + old-diff-file new-diff-file + "-d" "-U" "1") + + (buffer-string))) + + (ignore-errors (delete-file old-diff-file)) + (ignore-errors (delete-file new-diff-file))))) + +(defun org-quip--make-publish-diff (org-buffer quip-html) + "Generate a diff between the specified ORG-BUFFER and QUIP-HTML. + +The return value is a list of commands to execute against Quip." + ;; + ;; In order to make this work we need to generate a correspondence between + ;; the quip document and the org buffer. Quip gives us back a wodge of + ;; HTML, so what we do is we generate a correspondence between org's HTML + ;; and quip's HTML. + ;; + ;; That works more or less as follows: + ;; + ;; 1. Clean up the quip HTML into a list of "top-level" elements. Each + ;; element is on its own "line". + ;; + ;; 2. Extract a list of element IDs from the quip HTML, one per line. + ;; + ;; 3. Generate HTML from org and do the same thing. + ;; + ;; 4. Extract a list of element IDs from the org HTML, again one per line. + ;; + ;; 5. Extract a list of element "positions" from the org buffer, one per + ;; line. We do this by walking the org tree in a manner that's produces + ;; equivalent results to the HTML walk. This is the sketchiest part of + ;; the whole deal, and we do it because we can't reliably round-trip + ;; HTML through Emacs' DOM. (That is, we can't just use the DOM we + ;; extracted IDs from in step 4 to generate HTML to push to Quip because + ;; the parser escapes things in ways I can't re-escape.) + ;; + ;; 6. Diff the quip HTML and org HTML. + ;; + ;; 7. Parse the diff, tracking "lines" and using the ID and position lists + ;; we built up to generate commands. + ;; + (letrec ((old-dom (org-quip--get-cleaned-dom quip-html)) + (old-id-list (mapcar #'org-quip--get-element-id old-dom)) + + (new-html (org-quip--export-html buffer)) + (new-dom (org-quip--get-cleaned-dom new-html)) + (new-id-list (mapcar #'org-quip--get-element-id new-dom)) + (new-pos-list (org-quip--get-positions-from-ids buffer + new-id-list)) + + (diff-text (org-quip--diff-org-and-quip quip-html new-html)) + + (last-id nil) + (remove-ids nil) + (insert-commands nil) + (old-line 0) + (new-line 0)) + + (with-temp-buffer + (insert diff-text) + (goto-char (point-min)) + (forward-line 2) + (while (< (point) (point-max)) + (cond + ;; Processing a resync line, e.g. `@@ -4,4 +4,4 @@' + ;; + ;; Just reset both line counters. Remember that diff line numbers + ;; are 1-based but since we use the counters as list indices they + ;; need to be 0-based. + ((looking-at "@@ -\\([0-9]+\\),[0-9]+ \\+\\([0-9]+\\),[0-9]+ @@") + (setq old-line (1- (string-to-int (match-string 1)))) + (setq new-line (1- (string-to-int (match-string 2)))) + ;; (message "Resync: %s %s" old-line new-line) + ) + + ;; Processing a remove. + ;; + ;; Remember all the removes come, then all the adds. We don't know + ;; if a remove is actually a remove or a replace yet so just batch + ;; up the removes. + ;; + ;; Also move the "old" line counter forward. + ((looking-at "-") + (let ((old-id (nth old-line old-id-list))) + (setq remove-ids (cons old-id remove-ids)) + (setq last-id old-id) + (setq old-line (1+ old-line)) + ;; (message "Old : %s" old-id) + )) + + ;; Processing an add. + ;; + ;; Adds might actually be "replace", so check to see if the ID we're + ;; adding is in the set of ID's we're removing. If it is, remove it, + ;; and the command is a "replace". If it isn't, then it's just an + ;; "insert-after". + ;; + ;; Also move the "new" line counter forward. + ((looking-at "\\+") + (letrec ((new-id (nth new-line new-id-list)) + (new-pos (nth new-line new-pos-list)) + (new-html (org-quip--export-html-fragment buffer + (car new-pos) + (cdr new-pos)))) + (setq insert-commands + (cons (cond ((member new-id remove-ids) + (setq remove-ids (remove new-id remove-ids)) + (list 'replace new-id new-html)) + + (t + (list 'insert-after last-id new-html))) + insert-commands)) + + (setq new-line (1+ new-line)) + ;; (message "New : %s" new-id) + )) + + ;; Processing a shared line. + ;; + ;; Just tick both line counters forward. Also remember, we know the + ;; old ID for this line. + ((looking-at " ") + (setq last-id (nth old-line old-id-list)) + (setq new-line (1+ new-line)) + (setq old-line (1+ old-line)) + + ;; (message "Common: %s" last-id) + ) + ) + + ;; (message "Tick : %s %s %s %s" (point) last-id old-line new-line) + (forward-line)) + + ;; Convert all of our accumulated adds and removes into actual commands + ;; to run. + (append insert-commands + (mapcar (lambda (id) (list 'remove id)) remove-ids)) + ))) + (defun org-quip--test-blarg () "This is a test blarg." - (letrec ((buffer (get-buffer "leavingconfigerator.org")) (thread-id (org-quip--get-buffer-thread-id buffer)) - (old-html (alist-get 'html (quip-get-thread thread-id))) - (old-diff-text (org-quip--get-cleaned-html-string old-html)) - (new-html (org-quip--export-html buffer)) - (new-diff-text (org-quip--get-cleaned-html-string new-html)) - (old-diff-file (make-temp-file "qod")) - (new-diff-file (make-temp-file "qnd")) - - ) - - (with-temp-file old-diff-file - (insert old-diff-text)) - (with-temp-file new-diff-file - (insert new-diff-text)) - - (with-current-buffer (get-buffer-create "*What*") - (erase-buffer) - (call-process "diff" - nil (current-buffer) nil - old-diff-file new-diff-file - "-d" "-U" "9999") - - (delete-file old-diff-file) - (delete-file new-diff-file) - - (diff-mode) - (display-buffer (current-buffer)))) + (old-html (alist-get 'html (quip-get-thread thread-id)))) + ;(org-quip--diff-org-and-quip old-html (org-quip--export-html buffer)) + (org-quip--make-publish-diff buffer old-html) + ) (third '(1)) )