changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / emacs/lib/graph.el

changeset 678: 2b7d5a8d63ac
parent: bb8aa1eda12b
author: Richard Westhaver <ellis@rwest.io>
date: Wed, 25 Sep 2024 21:39:39 -0400
permissions: -rw-r--r--
description: alien octets fix, workin with org-graph-db
1 ;;; graph.el --- Graph-oriented Extensions -*- lexical-binding: t; -*-
2 
3 ;; Copyright (C) 2024 The Compiler Company
4 ;; Version: "0.2.0"
5 ;; Author: Richard Westhaver <richard.westhaver@gmail.com>
6 ;; Keywords: docs, maint, outlines, extensions
7 
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
12 
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
17 
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
20 
21 ;;; Commentary:
22 
23 ;;
24 
25 ;;; Code:
26 (require 'org)
27 (require 'org-agenda)
28 (require 'default)
29 (require 'ulang)
30 
31 (defgroup graph nil
32  "CC Graph")
33 
34 (defcustom org-graph-db-directory (join-paths user-org-stash-directory "graph")
35  "graph database storage directory."
36  :type 'directory
37  :group 'graph)
38 
39 (defcustom org-graph-locations (list (join-paths company-org-directory "notes/"))
40  "List of directories to check for nodes."
41  :type '(list directory)
42  :group 'graph)
43 
44 (defcustom org-graph-include-agenda-files nil
45  "When non-nil, include `org-agenda-files' in the graph."
46  :type 'boolean
47  :group 'graph)
48 
49 (defcustom org-graph-include-archive nil
50  "When non-nil, include `org-arhive-location' in the graph."
51  :type 'boolean
52  :group 'graph)
53 
54 (defcustom org-graph-include-org-directory nil
55  "When non-nil, include `org-directory' files in the graph."
56  :type 'boolean
57  :group 'graph)
58 
59 (defcustom org-graph-compaction-hook nil
60  "Hook run when a graph is compacted to `org-graph-db'."
61  :type 'hook
62  :group 'graph)
63 
64 (defcustom org-graph-capture-hook nil
65  "Hook run when a node is added to the graph."
66  :type 'hook
67  :group 'graph)
68 
69 (defcustom org-graph-db-init-script (join-paths company-source-directory "infra/scripts/org-db-init.lisp")
70  "Path to a lisp script responsible for initializing the `org-graph-db-directory'.")
71 
72 (cl-defstruct org-graph-db-handle
73  (type :rocksdb)
74  (name "org-graph-db")
75  init
76  get
77  put
78  delete
79  merge
80  compact
81  shutdown)
82 
83 (defcustom org-graph-db (make-org-graph-db-handle)
84  "A handle to the database backend which stores nodes and edges."
85  :type 'org-graph-db-handle
86  :group 'graph)
87 
88 (defun org-graph-from-id-locations (&optional edges local)
89  "Populate the `org-graph' from `org-id-locations', filtering out any
90 entries not under a member of `org-graph-locations'. When EDGES is
91 non-nil visit each node and collect all edges found."
92  (interactive "P")
93  (save-excursion
94  (let* ((node-ids (copy-hash-table (org-id-locations-load))) ;; don't overwrite `org-id-locations'
95  (graph (make-org-graph :nodes node-ids)))
96  (maphash
97  (lambda (k v)
98  (if-let ((ok (cl-loop for l in org-graph-locations
99  when (string-prefix-p l (file-truename v))
100  return t)))
101  (let ((pos (cdr (org-id-find-id-in-file k v))))
102  (message "%s %s" k v)
103  (org-with-file-buffer v
104  (goto-char pos)
105  (org-graph-node-at-point graph)
106  (when edges (org-graph-edges-at-point graph))))
107  (remhash k (org-graph-nodes graph))))
108  (org-graph-nodes graph))
109  (if local
110  (setq-local org-graph graph)
111  (setq org-graph graph)))))
112 
113 (defun org-graph-files ()
114  (org-list-files org-graph-locations org-agenda-extensions))
115 
116 (cl-defstruct org-graph
117  ;; TODO 2024-09-17: use integers instead of string
118  (nodes (make-hash-table :test 'equal))
119  (edges (make-hash-table :test 'equal)))
120 
121 (defvar org-graph (make-org-graph)
122  "The Emacs-native org-graph. Should be assigned to an `org-graph' instance.")
123 
124 (cl-defstruct org-graph-node id name file point)
125 (cl-defstruct org-graph-edge (type 'link) in properties timestamp point out)
126 
127 (defun org-graph--file-hash (file)
128  "Compute the hash of FILE."
129  (with-temp-buffer
130  (set-buffer-multibyte nil)
131  (insert-file-contents-literally file)
132  (secure-hash 'md5 (current-buffer))))
133 
134 (defun org-graph-node-at-point (&optional update)
135  "Return the `org-graph-node' at point. When UPDATE is non-nil insert or
136 update the node into the org-graph object specified or when 't' use the
137 currently active org-graph."
138  (let* ((file (buffer-file-name))
139  (node (make-org-graph-node :point (point) :file file)))
140  (if (derived-mode-p 'org-mode)
141  (progn
142  (if (org-before-first-heading-p)
143  (setf (org-graph-node-name node) (org-get-title)
144  ;; use the filename, create a hash as id
145  (org-graph-node-id node) (org-graph--file-hash file))
146  (setf (org-graph-node-id node) (org-id-get)
147  (org-graph-node-name node) (elt (org-heading-components) 4))))
148  (setf (org-graph-node-id node) (org-graph--file-hash file)
149  (org-graph-node-name node) (file-name-nondirectory file)))
150  (when update
151  (puthash (org-graph-node-id node) node (org-graph-nodes (if (eql t update) org-graph update))))
152  node))
153 
154 ;; TODO 2024-09-22: properties
155 (defun org-graph-collect-edge ()
156  "Collect the edge at point which should be a line created with `org-graph-edge--insert'."
157  (org-with-point-at (beginning-of-line)
158  (when (org-at-timestamp-p 'lax)
159  (let ((ep (point))
160  (ts (match-string-no-properties 0))
161  (end (match-end 0)))
162  (goto-char (1+ end))
163  ;; next 2 chars are the arrow
164  (let ((arrow (org-graph-edge-arrow* (buffer-substring-no-properties (point) (+ 2 (point))))))
165  (goto-char (+ (point) 4))
166  (make-org-graph-edge :in (org-id-get)
167  :type arrow
168  :point ep
169  :timestamp (org-parse-time-string ts t)
170  :out (string-trim (org--link-at-point) "id:")))))))
171 
172 (defun org-graph-map-edges (function)
173  "Eval FUNCTION once for each edge in node at point with point at start of the edge."
174  (with-org-graph-edge-drawer (end)
175  (re-search-backward (rx bol ?: (literal (org-graph-edge-drawer)) ?: eol) nil t)
176  (goto-char (1+ (match-end 0)))
177  (cl-loop while (> (point-max) end (point))
178  collect (funcall function)
179  do (next-line))))
180 
181 ;; TODO 2024-09-23:
182 (defun org-link-info (link)
183  (let ((path (org-element-property :path link))
184  (type (org-element-property :type link))
185  (desc (substring-no-properties (nth 2 link))))
186  (list type path desc)))
187 
188 ;; TODO 2024-09-22:
189 (defun org-graph-infer-edges ()
190  "Infer edges from the contents of the node at point. The result of this
191 function is a list of org-graph-edge objects."
192  ;; collect links
193  (with-org-graph-edge-drawer (beg)
194  (org-element-map (org-element-parse-buffer) 'link
195  (lambda (link)
196  (print link)
197  ;; (org-graph-edge-link-builder (funcall 'org-element-create link))
198  ))))
199 
200 (defun org-graph-reduce-edges (function)
201  "Same as `cl-reduce' where SEQ is the list of edges at point. FUNCTION
202 takes two `org-graph-edge' objects as input."
203  (let ((edges (org-graph-map-edges 'org-graph-collect-edge)))
204  (cl-reduce function edges)))
205 
206 (defun org-graph-collect-edges-at-point (&optional update)
207  "Collect the contents of the EDGES drawer from node at point. When UPDATE
208 is non-nil insert or update the node into the org-graph object specified
209 or when 't' use the currently active org-graph."
210  (let ((edges (org-graph-map-edges 'org-graph-collect-edge)))
211  (when update
212  (mapc (lambda (e)
213  (puthash
214  (org-graph-edge-in e)
215  e
216  (org-graph-edges (if (eql t update) org-graph update))))
217  edges))
218  edges))
219 
220 (defun org-graph-edge-equal (a b)
221  "Return non-nil if A and B are 'equal' org-graph-edge objects."
222  (equal (org-graph-edge-out a) (org-graph-edge-out b)))
223 
224 (defun org-graph-edge-remove-duplicates ()
225  "Remove duplicate edge entries from node at point."
226  (org-graph-reduce-edges
227  (lambda (a b)
228  (when (org-graph-edge-equal a b)
229  (let ((tsa (org-graph-edge-timestamp a))
230  (tsb (org-graph-edge-timestamp b)))
231  (goto-char (org-graph-edge-point (if (org-time> tsa tsb) b a)))
232  (delete-line))))))
233 
234 (defun org-graph-edges-at-point (&optional update)
235  "Return a list of `org-graph-edge' instances associated with the node at
236 point. When UPDATE is non-nil insert or update the edges into the
237 currently active org-graph."
238  (interactive)
239  (when (derived-mode-p 'org-mode)
240  (org-graph-collect-edges-at-point update)))
241 
242 (defun org-graph-buffer-update (&optional buffer)
243  "Map over an org buffer adding all nodes to the active org-graph."
244  (interactive)
245  (save-excursion
246  (with-current-buffer (or buffer (current-buffer))
247  ;; capture file node
248  (goto-char (point-min))
249  (org-graph-node-at-point t)
250  (when (derived-mode-p 'org-mode)
251  (org-map-entries (lambda () (org-graph-node-at-point t)))))))
252 
253 ;;; Edges
254 ;; See https://github.com/toshism/org-super-links/blob/develop/org-super-links.el
255 (declare-function org-make-link-description-function "ext:org-mode")
256 
257 (defvar org-graph-edge-drawer "EDGES"
258  "Controls how/where to insert edges. If nil edges will just be inserted
259 under the heading.")
260 
261 ;; TODO 2024-09-16: edge properties
262 (defvar org-graph-edge-prefix 'org-graph-edge-prefix-timestamp
263  "Prefix to insert before the edge.
264 This can be a string, nil, or a function that takes no arguments and
265 returns a string.
266 
267 Default is the function `org-graph-edge-prefix-timestamp'
268 which returns an inactive timestamp formatted according to the variable
269 `org-time-stamp-formats'.")
270 
271 ;; TODO 2024-09-16: do we need this? what sort of information for a
272 ;; given edge would go in the postfix? this may be better suited as a
273 ;; per-edge value rather than global - maybe use for comments.
274 (defvar org-graph-edge-postfix nil
275  "Postfix to insert after the edge.
276 This can be a string, nil, or a function that takes no arguments and
277 returns a string")
278 
279 (defvar org-graph-edge-link-prefix nil
280  "Prefix to insert before the link.
281 This can be a string, nil, or a function that takes no arguments and
282 returns a string")
283 
284 (defvar org-graph-edge-link-postfix nil
285  "Postfix to insert after the link.
286 This can be a string, nil, or a function that takes no arguments and
287 returns a string")
288 
289 (defvar org-graph-edge-default-description-formatter org-make-link-description-function
290  "What to use if no description is provided.
291 This can be a string, nil or a function that accepts two arguments
292 LINK and DESC and returns a string.
293 
294 nil will return the default desciption or the link.
295 string will be used only as a default fall back if set.
296 function will be called for every link.
297 
298 Default is the variable `org-make-link-desciption-function'.")
299 
300 (defvar org-graph-edge-search-function 'org-graph-edge-get-location
301  "The interface to use for finding target links. If you provide a custom
302 function it will be called with the `point` at the location the link
303 should be inserted. The only other requirement is that it should call
304 the function `org-graph-edge-insert-link-marker' with a marker to the target
305 link. AKA the place you want the edge.
306 
307 `org-graph-edge-get-location' internally uses `org-refile-get-location'.")
308 
309 (defvar org-graph-edge-pre-link-hook nil
310  "Hook called before storing the link on the link side.
311 This is called with point at the location where it was called.")
312 
313 (defvar org-graph-edge-pre-backlink-hook nil
314  "Hook called before storing the link on the backlink side.
315 This is called with point in the heading of the backlink.")
316 
317 (defvar org-graph-edge-indicator-alist
318  '((link . "->")
319  (backlink . "<-")
320  (relation . "--")
321  (parent . ">>")
322  (child . "<<"))
323  "An alist of (EDGE-TYPE . INDICATOR) pairs. Each INDICATOR is a string
324 which will be printed between the properties and backlink of the
325 associated EDGE-TYPE.")
326 
327 (defun org-graph-edge-arrow (sym)
328  (cdr (assoc sym org-graph-edge-indicator-alist)))
329 
330 (defun org-graph-edge-arrow* (str)
331  "Reverse lookup of edge arrow symbol."
332  (car (rassoc str org-graph-edge-indicator-alist)))
333 
334 (defun org-graph-edge-get-location ()
335  "Default for function `org-graph-edge-search-function' that reuses the `org-refile' machinery."
336  (org-refile-get-location "Node"))
337 
338 (cl-defmacro with-org-graph-edge-drawer ((start &optional create) &rest body)
339  "START is a symbol which is bound to the start of the edge drawer."
340  (declare (indent 1))
341  `(save-excursion
342  (org-with-wide-buffer
343  (let ((org-log-into-drawer (org-graph-edge-drawer)))
344  (org-graph-narrow-to-node)
345  (let ((,start (org-log-beginning ,create)))
346  (when (or (re-search-forward (rx bol ?: "END" ?: eol) nil t)
347  (re-search-backward (rx bol ?: "END" ?: eol) nil t))
348  (goto-char ,start)
349  ,@body))))))
350 
351 (defun org-graph-edge-search-function ()
352  "Call the search interface specified in variable `org-graph-edge-search-function'."
353  (funcall org-graph-edge-search-function))
354 
355 (defun org-graph-edge-prefix ()
356  "Return an appropriate string based on variable `org-graph-edge-prefix'."
357  (cond ((equal org-graph-edge-prefix nil) "")
358  ((stringp org-graph-edge-prefix) org-graph-edge-prefix)
359  (t (funcall org-graph-edge-prefix))))
360 
361 (defun org-graph-edge-postfix ()
362  "Return an appropriate string based on variable `org-graph-edge-postfix'."
363  (cond ((equal org-graph-edge-postfix nil) "\n")
364  ((stringp org-graph-edge-postfix) org-graph-edge-postfix)
365  (t (funcall org-graph-edge-postfix))))
366 
367 (defun org-graph-edge-link-prefix ()
368  "Return an appropriate string based on variable `org-graph-edge-link-prefix'."
369  (cond ((equal org-graph-edge-link-prefix nil) "")
370  ((stringp org-graph-edge-link-prefix) org-graph-edge-link-prefix)
371  (t (funcall org-graph-edge-link-prefix))))
372 
373 (defun org-graph-edge-link-postfix ()
374  "Return an appropriate string based on variable `org-graph-edge-link-postfix'."
375  (cond ((equal org-graph-edge-link-postfix nil) "")
376  ((stringp org-graph-edge-link-postfix) org-graph-edge-link-postfix)
377  (t (funcall org-graph-edge-link-postfix))))
378 
379 ;; TODO 2024-09-16: edge-properties
380 (defun org-graph-edge-prefix-timestamp ()
381  "Return the default prefix string for an edge.
382 Inactive timestamp formatted according to `org-time-stamp-formats'."
383  (format-time-string (org-time-stamp-format t t) (current-time)))
384 
385 (defun org-graph-edge-default-description-formatter (link desc)
386  "Return a string to use as the link desciption.
387 LINK is the link target. DESC is the provided desc."
388  (let ((p org-graph-edge-default-description-formatter))
389  (cond ((equal p nil) (or desc link))
390  ((stringp p) (or desc p))
391  ((fboundp p) (funcall p link desc))
392  (t desc))))
393 
394 (defun org-graph-edge-drawer ()
395  "Name of the edge drawer, as a string, or nil.
396 This is the value of variable
397 `org-graph-edge-drawer'. However, if the current
398 entry has or inherits a EDGE_DRAWER property, it will be
399 used instead of the default value."
400  (let ((p (org-entry-get nil "EDGE_DRAWER" 'inherit t)))
401  (cond ((equal p "nil") nil)
402  ((stringp p) p)
403  (t org-graph-edge-drawer))))
404 
405 (defun org-graph-narrow-to-node ()
406  "Narrow to current heading, excluding subheadings."
407  (org-narrow-to-subtree)
408  (save-excursion
409  (org-next-visible-heading 1)
410  (narrow-to-region (point-min) (point))))
411 
412 ;; delete related functions
413 (defun org-graph-find-links (id)
414  "Return link elements for ID."
415  (org-graph-narrow-to-node)
416  (let ((links
417  (org-element-map (org-element-parse-buffer) 'link
418  (lambda (link)
419  (when (string= (org-element-property :path link) id)
420  link)))))
421  (widen)
422  links))
423 
424 (defun org-graph-edge--in-drawer-p ()
425  "Return non-nil if point is in drawer. Value is element at point."
426  (let ((element (org-element-at-point)))
427  (while (and element
428  (not (memq (org-element-type element) '(drawer property-drawer))))
429  (setq element (org-element-property :parent element)))
430  element))
431 
432 (defun org-graph-edge--delete-link (link)
433  "Delete the LINK. If point is in edges drawer, delete the entire line."
434  (save-excursion
435  (goto-char (org-element-property :begin link))
436  (if (org-graph-edge--in-drawer)
437  (progn
438  (kill-whole-line 1)
439  (org-remove-empty-drawer-at (point)))
440  (delete-region (org-element-property :begin link) (org-element-property :end link)))))
441 
442 (defun org-graph-edge--insert (link desc arrow &rest props)
443  "Insert an edge at point. ARROW is a symbol representing the type of
444 arrow to insert. The rest of the arguments are parsed as :KEY VAL pairs
445 which are inserted with the edge."
446  (insert (format "%s %s " (org-graph-edge-prefix)
447  (org-graph-edge-arrow arrow)))
448  (org-insert-link nil link desc)
449  (insert (org-graph-edge-link-postfix))
450  (newline))
451 
452 (defun org-graph-edge-insert-related (link desc)
453  "Insert a relation edge."
454  (with-org-graph-edge-drawer (beg t)
455  (org-graph-edge--insert link desc 'relation)
456  (org-indent-region beg (point))))
457 
458 (defun org-graph-edge-insert-backlink (link desc)
459  "Insert a backlink edge."
460  (with-org-graph-edge-drawer (beg t)
461  (let ((description (org-graph-edge-default-description-formatter link desc)))
462  (org-graph-edge--insert link description 'backlink)
463  (org-indent-region beg (point)))))
464 
465 (defun org-graph-edge-insert-link (link desc)
466  "insert a forward link edge. When BACKLINK is non-nil also create a
467 backlink at the node specified in LINK."
468  (interactive)
469  (with-org-graph-edge-drawer (beg t)
470  (let ((description (org-graph-edge-default-description-formatter link desc)))
471  (org-graph-edge--insert link desc 'link)
472  (org-indent-region beg (point)))))
473 
474 (defun org-graph-edge-links-action (marker hooks)
475  "Go to MARKER, run HOOKS and store a link."
476  (with-current-buffer (marker-buffer marker)
477  (save-excursion
478  (save-restriction
479  (widen) ;; buffer could be narrowed
480  (goto-char (marker-position marker))
481  (run-hooks hooks)
482  (call-interactively #'org-store-link)
483  (pop org-stored-links)))))
484 
485 (defun org-graph-edge-link-builder (link)
486  "Format link description for LINK."
487  (let* ((link-ref (car link))
488  (pre-desc (cadr link))
489  (description (org-graph-edge-default-description-formatter link-ref pre-desc)))
490  (cons link-ref description)))
491 
492 (defun org-graph-edge-insert-link-marker (target &optional no-forward no-backward)
493  "Insert link to marker TARGET and create an edge.
494 Only create edges in files in `org-mode' or a derived mode, otherwise just
495 act like a normal link.
496 
497 If NO-FORWARD is non-nil skip creating the forward link. If NO-BACKWARD
498 is non-nil skip creating the backlink."
499  (let* ((source (point-marker))
500  (source-link (org-graph-edge-links-action source 'org-graph-edge-pre-link-hook))
501  (target-link (org-graph-edge-links-action target 'org-graph-edge-pre-backlink-hook))
502  (source-formatted-link (org-graph-edge-link-builder source-link))
503  (target-formatted-link (org-graph-edge-link-builder target-link)))
504  (unless no-backward
505  (with-current-buffer (marker-buffer target)
506  (save-excursion
507  (save-restriction
508  (widen) ;; buffer could be narrowed
509  (goto-char (marker-position target))
510  (when (derived-mode-p 'org-mode)
511  (org-graph-edge-insert-backlink (car source-formatted-link) (cdr source-formatted-link)))))))
512  (unless no-forward
513  (with-current-buffer (marker-buffer source)
514  (save-excursion
515  (goto-char (marker-position source))
516  (print target-formatted-link)
517  (org-graph-edge-insert-link (car target-formatted-link) (cdr target-formatted-link)))))))
518 
519 ;;;###autoload
520 (defun org-graph-edge-convert-link (&optional arg)
521  "Convert a normal `org-mode' link at `point' to a graph link, ARG prefix.
522 When called interactively with a `C-u' prefix argument do not modify
523 existing link."
524  (interactive "P")
525  (let ((from-m (point-marker))
526  (target (save-window-excursion
527  (with-current-buffer (current-buffer)
528  (save-excursion
529  (org-open-at-point)
530  (point-marker))))))
531  (org-graph-edge-insert-link-marker target arg)
532  (goto-char (marker-position from-m)))
533  (when (not arg)
534  (let ((begin (org-element-property :begin (org-element-context)))
535  (end (org-element-property :end (org-element-context))))
536  (delete-region begin end))))
537 
538 ;;;###autoload
539 (defun org-graph-edge-delete ()
540  "Delete the link at point, and the corresponding backlink.
541 If no backlink exists, just delete link at point. This works from
542 either side, and deletes both sides of a link."
543  (interactive)
544  (save-window-excursion
545  (with-current-buffer (current-buffer)
546  (save-excursion
547  (let ((id (org-id-get (point))))
548  (org-open-at-point)
549  (let ((link-elements (org-graph-find-edges id)))
550  (if link-elements
551  (if (> (length link-elements) 1)
552  (error "Multiple links found.")
553  (org-graph-edge--delete-link (car link-elements)))
554  (message "No edge found. Deleting active only.")))))))
555  (org-graph-edge--delete-link (org-element-context)))
556 
557 ;;;###autoload
558 (defun org-graph-edge-insert ()
559  "Insert an edge from `org-stored-links'."
560  (interactive)
561  (if org-stored-links
562  (progn
563  (org-link-open (pop org-stored-links))
564  (org-graph-edge-insert-link-marker (set-marker (make-marker) (point))))
565  (org-graph-edge-link)))
566 
567 ;;;###autoload
568 (defun org-graph-edge-link (&optional no-backlink)
569  "Insert a link edge and add a backlink edge to the target heading. With
570 'C-u' don't create a backlink to the target."
571  (interactive)
572  (let ((target (org-graph-edge-search-function)))
573  (org-graph-edge-insert-link-marker (set-marker (make-marker) (car (cdddr target))
574  (get-file-buffer (car (cdr target))))
575  nil no-backlink)))
576 
577 (defun org-graph-edge-backlink ()
578  "Insert a backlink edge to the target heading from the current one."
579  (interactive)
580  (let ((target (org-graph-edge-search-function)))
581  (org-graph-edge-insert-link-marker (set-marker (make-marker) (car (cdddr target))
582  (get-file-buffer (car (cdr target))))
583  t)))
584 
585 (defun org-dblock-write:links ()
586  "Generate a 'links' block for the designated node.")
587 
588 (defun org-dblock-write:graph ()
589  "Generate a 'graph' block for the designated set of nodes.")
590 
591 (provide 'graph)
592 ;; graph.el ends here