diff options
-rw-r--r-- | CHANGELOG.org | 88 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | README.org | 59 | ||||
-rw-r--r-- | lisp/seam-export.el (renamed from seam-export.el) | 168 | ||||
-rw-r--r-- | lisp/seam-html.el (renamed from seam-html.el) | 49 | ||||
-rw-r--r-- | lisp/seam-test.el (renamed from seam-test.el) | 276 | ||||
-rw-r--r-- | lisp/seam.el (renamed from seam.el) | 341 |
7 files changed, 734 insertions, 260 deletions
diff --git a/CHANGELOG.org b/CHANGELOG.org new file mode 100644 index 0000000..ff8a9a6 --- /dev/null +++ b/CHANGELOG.org @@ -0,0 +1,88 @@ +** Changes since 0.1.0 + +*** Breaking changes + +- Seam's code has been moved to the =lisp/= subdirectory, where it + should have been all along. Make sure to update your =init.el= + accordingly. + +- Seam now uses [[https://github.com/Wilfred/mustache.el][mustache.el]] for templating. In Mustache, + double-bracketed variables are escaped, so you must use + triple-brackets for variables that include raw HTML. Please see the + updated =seam-export-default-template-string= for reference. + +- Your =seam-title-formatter= function should now take three arguments + instead of two: the third arg (=draft-p=) will be non-nil if the + note is a draft. + +**** Renamed functions + +- =seam-make-note= is now =seam-create-note=. + +- =seam-replace-string-in-notes= is now + =seam-replace-string-in-all-notes=. + +- =seam-visited-files= is now =seam-visited-notes=. + +*** New features + +- Notes can now be set as drafts for finer-grained control over + exporting. If =seam-create-as-draft= is non-nil, new notes will be + created as drafts. See =seam-note-types= for info on overriding + this per type. A note's draft status can be toggled with the new + command =seam-toggle-draft=. A new =seam-export-alist= option, + =:include-drafts=, controls whether drafts are included in a given + export profile. + +- Custom template variables can now be defined, and built-in ones + overridden. This is done globally with + =seam-export-template-values=, or per profile with + =:template-values=. + +- An option has been added to export internal links with a custom CSS + class. The default is set by =seam-export-internal-link-class=, and + can overridden per profile using =:internal-link-class=. + +- Custom slugs can now be set by adding the =SEAM_SLUG= property to a + note's title headline. + +*** Improvements + +- Notes are no longer re-exported unnecessarily whenever a linked note + is changed. + +- In HTML templates, ={{title}}= now strips out formatting, so that it + is more suitable for use in =<title>= tags. To get a raw + HTML-formatted title for =<h1>= tags and the like, you should use + the new ={{{raw-title}}}=. As mentioned above, triple brackets are + the Mustache syntax for interpolating raw HTML. + +- When invoking =seam-delete-note=, the note's title is now mentioned. + This is to reduce the risk of deleting the wrong note by mistake. + +- Completion support is somewhat improved. =ido-completing-read= now + works properly, and Seam no longer binds =completion-ignore-case=. + +- Changes to =seam-export-alist= are now respected when forcibly + re-exporting (e.g. with =seam-export-all-notes=). This is done by + always deleting old HTML files before exporting, thus avoiding the + situation where notes of no-longer-exported types still have files + hanging around. + +*** Bugfixes + +- Notes with single quotes in the name (') are no longer broken. + +- =seam-visited-notes= no longer returns buffers that visit non-note + files within =seam-note-directory=. This could have resulted in + Seam inappropriately modifying those files (e.g. updating links). + +- Buffer titles are now set correctly from narrowed buffers. + +- An issue with regexp escape sequences being interpreted in template + variable replacements has been fixed. + +- Seam now validates note types entered with =C-u seam-set-note-type=, + averting any mishaps if an invalid type is entered. + +- It is no longer possible to create a note with an empty slug. diff --git a/README.md b/README.md deleted file mode 100644 index a3f75e4..0000000 --- a/README.md +++ /dev/null @@ -1,13 +0,0 @@ -[Seam](https://wiki.plexwave.org/seam) is a personal wiki system based -on [Org mode](https://orgmode.org/). - -# Installation -Clone this repo and add it to your load path. As long as your system -has `find` and `grep` installed, no further setup should be required. - -# Documentation -For now, the best way to learn about Seam is the [project -page](https://wiki.plexwave.org/seam) and the -[tutorial](https://wiki.plexwave.org/seam-tutorial). I have -endeavored to make Seam fairly self-documenting, so also see the -docstrings and the Seam customization group. @@ -1,10 +1,53 @@ -[[https://wiki.plexwave.org/seam][Seam]] is a personal wiki system based on [[https://orgmode.org/][Org mode]]. +** A personal wiki toolkit for Emacs -* Installation -Clone this repo and add it to your load path. As long as your system -has =find= and =grep= installed, no further setup should be required. +[[https://wiki.plexwave.org/seam][Seam]] leverages the power of [[https://orgmode.org/][Org mode]] to make creating, linking, and +publishing your notes easier. It is geared particularly towards +creating a personal wiki — a place where you can share some portion of +your notes with the world. It takes inspiration from the likes of +[[https://obsidian.md/][Obsidian]] and [[https://www.mediawiki.org/wiki/MediaWiki][MediaWiki]]. -* Documentation -For now, the best way to learn about Seam is the [[https://wiki.plexwave.org/seam][project page]] and the -[[https://wiki.plexwave.org/seam-tutorial][tutorial]]. I have endeavored to make Seam fairly self-documenting, so -also see the docstrings and the Seam customization group. +Three of Seam's key design tenets are: + +- Org files and their resultant HTML files should always be kept in + sync. + +- It should be easy to create multiple sites using different subsets + of the same note collection. + +- Notes should not be unnecessarily clouded with metadata. + +Be aware that Seam is a fully self-contained package, and is not +likely to be compatible with things like [[https://www.orgroam.com/][Org-roam]] due to its vastly +different approach. + +*Note:* Requires Emacs 29+, Org 9.6+, and [[https://github.com/Wilfred/mustache.el][mustache.el]]. + +*** Getting started + +The easiest way to begin is to follow the brief [[https://wiki.plexwave.org/seam-tutorial][tutorial]]. + +*** Documentation + +Seam's manual is still being written. In the meantime, the [[https://wiki.plexwave.org/seam][project +page]] contains some more tidbits you might find useful. + +I have endeavored to make Seam fairly self-documenting, so check the +docstrings and the Seam customization group when in any doubt. + +*** Known issues + +- =find-file= does not create notes properly. You should use + =seam-find-note= instead. + +- Commented-out links are not ignored, e.g. for determining backlinks. + +- Tags in note title headlines are not ignored; they are treated as + part of the title. + +- =seam:= links /must/ have a description. Bare links are not + supported. + +*** Upgrading + +As a new project, Seam is very much in flux. Whenever you upgrade it, +please see the [[file:CHANGELOG.org][changelog]] for breaking changes, new features, etc. diff --git a/seam-export.el b/lisp/seam-export.el index c36f97c..c267d69 100644 --- a/seam-export.el +++ b/lisp/seam-export.el @@ -28,11 +28,14 @@ ;;; Code: (require 'cl-lib) +(require 'mustache) (require 'seam-html) (defvar seam-export--types nil) (defvar seam-export--template nil) +(defvar seam-export--template-values nil) (defvar seam-export--root-path nil) +(defvar seam-export--include-drafts nil) (defvar seam-export--no-extension nil) (defvar seam-export--internal-link-class nil) (defvar seam-export--options nil) @@ -55,21 +58,34 @@ properties: `:template-file' - The HTML template file to be used by the exporter. If this is - missing, falls back to :template-string, `seam-export-template-file', - or `seam-export-template-string' in that order. + The HTML template file to be used by the exporter. If this + is missing, falls back to :template-string, + `seam-export-template-file', or `seam-export-template-string' + in that order. `:template-string' - The HTML template string to be used by the exporter. If this is - missing, falls back to :template-file, `seam-export-template-file', - or `seam-export-template-string' in that order. + The HTML template string to be used by the exporter. If this + is missing, falls back to :template-file, + `seam-export-template-file', or `seam-export-template-string' + in that order. + + `:template-values' + + An alist of (VAR . VALUE) pairs, where VAR is a string naming + a template variable, and VALUE is the value to be used when + interpolating that variable. See the mustache.el docs for + more information. Defaults to nil. `:root-path' The root path used for rendering internal links. Defaults to \"\", which means all paths are relative. + `:include-drafts' + + Whether to export draft notes as well. Defaults to nil. + `:no-extension' Whether to drop the \".html\" file extension in links. Defaults to @@ -107,21 +123,23 @@ See `seam-export-alist' for more information about specifying templates." <body> <main> <header> -<h1>{{title}}</h1> +<h1>{{{raw-title}}}</h1> <p class=\"modified\">Last modified: <time datetime=\"{{modified-dt}}\">{{modified}}</time></p> </header> -{{contents}} +{{{contents}}} <section class=\"backlinks\"> <h1>Backlinks</h1> -{{backlinks}} +{{{backlinks}}} </section> </main> </body> </html>" "The default HTML template string if no other template is specified. -It should be plain HTML5. Several variables are defined which can be -interpolated using the {{variable}} syntax: +It should be plain HTML5. Several variables are defined which +can be interpolated using Mustache bracket syntax. {{variable}} +will HTML-escape the interpolated text, while {{{variable}}} will +interpolate it as-is. `contents' @@ -129,7 +147,12 @@ interpolated using the {{variable}} syntax: `title' - The note's title (HTML-escaped). + The note's title, in a plain text format suitable for a + <title> tag. + + `raw-title' + + The note's title, in an HTML format suitable for an <h1> tag. `backlinks' @@ -153,6 +176,14 @@ See `seam-export-alist' for more information about specifying templates." :group 'seam-export :type '(choice string (const nil))) +(defcustom seam-export-template-values nil + "An alist of (VAR . VALUE) pairs, where VAR is a string naming a +template variable, and VALUE is the value to be used when +interpolating that variable. See the mustache.el docs for more +information." + :group 'seam-export + :type '(alist :key-type string :value-type sexp)) + (defcustom seam-export-time-format "%e %B %Y" "Human-readable format for template time strings. Passed to `format-time-string'." @@ -196,7 +227,7 @@ notes)." :with-smart-quotes t :with-toc nil)) -(defmacro seam-export--to-string (&rest body) +(defmacro seam-export--export-to-html-string (&rest body) (declare (indent 0)) (let ((buf (gensym))) `(let ((,buf (generate-new-buffer " *seam-export*"))) @@ -210,28 +241,44 @@ notes)." (buffer-string))) (kill-buffer ,buf))))) -;;; Some HACK-ery to get fully escaped and smartquote-ized string. -(defun seam-export--escape-string (s) +(defmacro seam-export--export-to-text-string (&rest body) + (declare (indent 0)) + (let ((buf (gensym))) + `(let ((,buf (generate-new-buffer " *seam-export*"))) + (unwind-protect + (progn (with-temp-buffer + ,@body + ;; This let prevents Org from popping up a window. + (let ((org-export-show-temporary-export-buffer nil) + (org-ascii-charset 'utf-8)) + (org-export-to-buffer 'ascii ,buf nil nil nil t seam-export--options nil))) + (with-current-buffer ,buf + (buffer-string))) + (kill-buffer ,buf))))) + +(defun seam-export--org-to-html (s) + "Convert single-line Org string to HTML via Org exporter." (string-remove-prefix "<p>\n" (string-remove-suffix "</p>\n" - (seam-export--to-string + (seam-export--export-to-html-string (insert s))))) -(defun seam-export--replace-variable (var replacement) - (goto-char 1) - (while (re-search-forward (format "{{%s}}" var) nil t) - (replace-match replacement))) +(defun seam-export--org-to-text (s) + "Convert single-line Org string to plain text via Org exporter." + (string-chop-newline + (seam-export--export-to-text-string + (insert s)))) (defun seam-export--generate-backlinks (file) - (seam-export--to-string - (let ((files (sort + (seam-export--export-to-html-string + (let ((files (cl-sort (let ((seam--subset seam-export--types)) (cl-loop for x in (seam-get-links-to-file file) collect (cons (seam-get-title-from-file x) x))) - :key #'car - :lessp #'string<))) + #'string< + :key #'car))) (when files (cl-loop for (title . file) in files do (insert (format "- [[seam:%s][%s]]\n" (file-name-base file) title))))))) @@ -239,37 +286,41 @@ notes)." (defun seam-export--note-to-html (note-file html-directory) (seam-ensure-directory-exists html-directory) (let ((html-file (file-name-concat html-directory - (concat (file-name-base note-file) ".html"))) + (concat (seam-get-slug-from-file-name note-file) ".html"))) (modified (file-attribute-modification-time (file-attributes note-file)))) (with-temp-buffer - (insert seam-export--template) - (seam-export--replace-variable - "title" - (seam-export--escape-string - (seam-get-title-from-file note-file))) - (seam-export--replace-variable - "modified" - (format-time-string - seam-export-time-format - modified - seam-export-time-zone)) - (seam-export--replace-variable - "modified-dt" - (format-time-string - seam-export-time-format-datetime - modified - seam-export-time-zone)) - (seam-export--replace-variable - "contents" - (seam-export--to-string - (insert-file-contents note-file) - (re-search-forward (org-headline-re 1)) - (org-mode) ;Needed for `org-set-property'. - (org-set-property "seam-title-p" "t"))) - (seam-export--replace-variable - "backlinks" - (seam-export--generate-backlinks note-file)) + (insert + (mustache-render + seam-export--template + (append + seam-export--template-values + seam-export-template-values + `(("title" . + ,(seam-export--org-to-text + (seam-get-title-from-file note-file))) + ("raw-title" . + ,(seam-export--org-to-html + (seam-get-title-from-file note-file))) + ("modified" . + ,(format-time-string + seam-export-time-format + modified + seam-export-time-zone)) + ("modified-dt" . + ,(format-time-string + seam-export-time-format-datetime + modified + seam-export-time-zone)) + ("contents" . + ,(seam-export--export-to-html-string + (insert-file-contents note-file) + (re-search-forward "^\\* ") + (org-mode) ;Needed for `org-set-property'. + (org-set-property "seam-title-p" "t"))) + ("backlinks" . + ,(seam-export--generate-backlinks note-file))) + nil))) (write-file html-file)))) (defun seam-export--file-string (file) @@ -278,12 +329,14 @@ notes)." (buffer-string))) (defun seam-export-note (file) - (let ((type (seam-get-note-type file))) + (let ((type (seam-get-note-type file)) + (draft-p (seam-draft-p file))) (cl-loop for (dir . plist) in seam-export-alist do (let ((types (plist-get plist :types)) (template-file (plist-get plist :template-file)) - (template-string (plist-get plist :template-string))) + (template-string (plist-get plist :template-string)) + (template-values (plist-get plist :template-values))) (unless types (error "You must specify :types for export")) (let ((template @@ -294,11 +347,15 @@ notes)." seam-export-template-file)) (seam-export-template-string seam-export-template-string) (t (error "You must specify a template for export (see `seam-export-alist')"))))) - (when (member type types) + (when (and (member type types) + (or (not (seam-draft-p file)) + (plist-get plist :include-drafts))) (let ((seam-export--types types) (seam-export--root-path (or (plist-get plist :root-path) "")) + (seam-export--include-drafts (plist-get plist :include-drafts)) (seam-export--no-extension (plist-get plist :no-extension)) (seam-export--template template) + (seam-export--template-values template-values) (seam-export--internal-link-class (or (plist-get plist :internal-link-class) seam-export-internal-link-class)) @@ -314,6 +371,7 @@ notes)." (error "Nothing to export. Please configure `seam-export-alist'.")) (dolist (dir (seam-note-subdirectories)) (dolist (file (directory-files dir t seam-note-file-regexp)) + (seam-delete-html-files-for-note file) (seam-export-note file)))) (provide 'seam-export) diff --git a/seam-html.el b/lisp/seam-html.el index 018e56f..57cd9b9 100644 --- a/seam-html.el +++ b/lisp/seam-html.el @@ -1,6 +1,7 @@ ;;; seam-html.el --- Seam HTML exporter -*- lexical-binding: t -*- ;; Copyright (C) 2025 Spencer Williams +;; Copyright (C) 2011-2025 Free Software Foundation, Inc. ;; Author: Spencer Williams <spnw@plexwave.org> @@ -28,15 +29,34 @@ ;; This was blithely hacked together using large chunks of code lifted ;; straight from ox-html.el, and could do with much improvement. ;; -;; Original ox-html code is licensed under GPL v3+. Copyright (c) -;; 2011-2025 Free Software Foundation, Inc. Original authors: Carsten -;; Dominik <carsten.dominik@gmail.com> and Jambunathan K <kjambunathan -;; at gmail dot com>. +;; The original authors of ox-html are: +;; Carsten Dominik <carsten.dominik@gmail.com> +;; Jambunathan K <kjambunathan at gmail dot com> ;;; Code: (require 'ox-html) +;;; Org <9.7 compatibility. + +(fset 'seam-html--element-parent-element + (if (fboundp 'org-element-parent-element) + 'org-element-parent-element + 'org-export-get-parent-element)) + +(fset 'seam-html--element-parent + (if (fboundp 'org-element-parent) + 'org-element-parent + (lambda (node) + (org-element-property :parent node)))) + +(fset 'seam-html--element-type-p + (if (fboundp 'org-element-type-p) + 'org-element-type-p + (lambda (node types) + (memq (org-element-type node) + (ensure-list types))))) + ;;; NOTE: This function does not respect `:headline-levels' or ;;; `:html-self-link-headlines'. (defun seam-html-headline (headline contents info) @@ -102,8 +122,8 @@ images, set it to: (lambda (paragraph) (org-element-property :caption paragraph))" (let ((paragraph (pcase (org-element-type element) (`paragraph element) - (`link (org-element-parent element))))) - (and (org-element-type-p paragraph 'paragraph) + (`link (seam-html--element-parent element))))) + (and (seam-html--element-type-p paragraph 'paragraph) (or (not (and (boundp 'seam-html-standalone-image-predicate) (fboundp seam-html-standalone-image-predicate))) (funcall seam-html-standalone-image-predicate paragraph)) @@ -146,11 +166,13 @@ INFO is a plist holding contextual information. See (path (cond ((string= "seam" link-type) - (let ((slug raw-path)) + (let ((slug (string-remove-prefix "-" raw-path))) (when-let ((file (seam-lookup-slug slug))) (let ((type (seam-get-note-type file))) (when (and (member type seam-export--types) - (file-exists-p (seam-make-file-name slug type))) + (or seam-export--include-drafts + (not (seam-draft-p file))) + (file-exists-p (seam-make-file-name raw-path type))) (concat seam-export--root-path slug (if seam-export--no-extension "" ".html"))))))) @@ -187,9 +209,9 @@ INFO is a plist holding contextual information. See ;; do this for the first link in parent (inner image link ;; for inline images). This is needed as long as ;; attributes cannot be set on a per link basis. - (let* ((parent (org-element-parent-element link)) - (link (let ((container (org-element-parent link))) - (if (and (org-element-type-p container 'link) + (let* ((parent (seam-html--element-parent-element link)) + (link (let ((container (seam-html--element-parent link))) + (if (and (seam-html--element-type-p container 'link) (org-html-inline-image-p link info)) container link)))) @@ -268,7 +290,7 @@ INFO is a plist holding contextual information. See (_ (if (and destination (memq (plist-get info :with-latex) '(mathjax t)) - (org-element-type-p destination 'latex-environment) + (seam-html--element-type-p destination 'latex-environment) (eq 'math (org-latex--environment-type destination))) ;; Caption and labels are introduced within LaTeX ;; environment. Use "ref" or "eqref" macro, depending on user @@ -279,7 +301,7 @@ INFO is a plist holding contextual information. See (seam-html-standalone-image-predicate #'org-html--has-caption-p) (counter-predicate - (if (org-element-type-p destination 'latex-environment) + (if (seam-html--element-type-p destination 'latex-environment) #'org-html--math-environment-p #'org-html--has-caption-p)) (number @@ -322,7 +344,6 @@ INFO is a plist holding contextual information. See ;; No path, only description. (t desc)))) - (defun seam-html-src-block (src-block _contents info) "Transcode a SRC-BLOCK element from Org to HTML. CONTENTS holds the contents of the item. INFO is a plist holding diff --git a/seam-test.el b/lisp/seam-test.el index 7d10f94..5bae015 100644 --- a/seam-test.el +++ b/lisp/seam-test.el @@ -39,14 +39,16 @@ (defmacro seam-test-environment (&rest body) (declare (indent 0)) - `(let* ((seam-test-directory (make-temp-file "seam-test" t)) + `(let* ((seam-test-directory (file-name-as-directory (make-temp-file "seam-test" t))) (seam-note-directory seam-test-directory) (default-directory seam-test-directory) + (seam-create-as-draft nil) (seam-note-types '("private" "public")) (seam-default-note-type "private") - (seam-title-formatter (lambda (title _type) title)) + (seam-title-formatter (lambda (title _type _draft-p) title)) (seam-export-template-file nil) (seam-export-template-string seam-export-default-template-string) + (seam-export-template-values nil) (seam-export-internal-link-class nil) (seam-export-alist `((,(file-name-concat seam-test-directory "html") @@ -65,16 +67,18 @@ `(seam-test-environment (let ,options (let ,(cl-loop for (name . args) in varlist - collect `(,name (seam-make-note ,@args))) + collect `(,name (seam-create-note ,@args))) + ;; FIXME: It's quite possible for tests to fail in such a way + ;; that this does not kill the buffers. (unwind-protect (progn ,@body) (mapcar #'kill-buffer (list ,@(mapcar #'car varlist)))))))) -(defun seam-test-remove-testdir (filename) - (string-remove-prefix (concat seam-test-directory "/") filename)) +(defun seam-test-strip-testdir (filename) + (string-remove-prefix seam-test-directory filename)) (defun seam-test-list-files () (mapcar - #'seam-test-remove-testdir + #'seam-test-strip-testdir (directory-files-recursively seam-test-directory ""))) (defun seam-test-add-contents (buffer contents) @@ -100,7 +104,7 @@ (cl-loop for ret = (re-search-forward "<a href=\"/\\(.*\\)?\">" nil t) while ret collect (match-string 1))))) -(ert-deftest seam-test-make-note-private () +(ert-deftest seam-test-create-note-private () (should (equal '("private/note.org") @@ -108,7 +112,7 @@ ((note "Note")) (seam-test-list-files))))) -(ert-deftest seam-test-make-note-public () +(ert-deftest seam-test-create-note-public () (should (equal '("html/note.html" "public/note.org") @@ -116,12 +120,12 @@ ((note "Note" "public")) (seam-test-list-files))))) -(ert-deftest seam-test-make-note-weird-filename () +(ert-deftest seam-test-create-note-weird-filename () (should (equal - '("./Weird file name!" ("private/weird-file-name.org")) + '("./Weir'd file name!" ("private/weird-file-name.org")) (seam-test-with-notes () - ((weird "./Weird file name! ")) + ((weird "./Weir'd file name! ")) (list (buffer-name weird) (seam-test-list-files)))))) @@ -151,22 +155,22 @@ (buffer-name note) (seam-get-title-from-file (buffer-file-name note)))))) -(ert-deftest seam-test-make-note-invalid-type () +(ert-deftest seam-test-create-note-invalid-type () (should-error (seam-test-environment - (kill-buffer (seam-make-note "Note" "invalid-type"))))) + (kill-buffer (seam-create-note "Note" "invalid-type"))))) -(ert-deftest seam-test-make-note-name-conflict () +(ert-deftest seam-test-create-note-name-conflict () (should-error (seam-test-environment - (kill-buffer (seam-make-note " Note 1 ")) - (kill-buffer (seam-make-note "Note_1"))))) + (kill-buffer (seam-create-note " Note 1 ")) + (kill-buffer (seam-create-note "Note_1"))))) -(ert-deftest seam-test-make-note-name-conflict-different-types () +(ert-deftest seam-test-create-note-name-conflict-different-types () (should-error (seam-test-environment - (kill-buffer (seam-make-note "Note")) - (kill-buffer (seam-make-note "Note" "public"))))) + (kill-buffer (seam-create-note "Note")) + (kill-buffer (seam-create-note "Note" "public"))))) (ert-deftest seam-test-rename-note () (should @@ -202,14 +206,16 @@ (ert-deftest seam-test-buffer-name-format-custom () (should (equal - "[private] Note" + "[private draft] Note" (seam-test-with-notes ((seam-title-formatter - (lambda (title type) (format "[%s] %s" type title)))) - ((note "Note")) + (lambda (title type draft-p) + (format "[%s%s] %s" type (if draft-p " draft" "") title)))) + ((note "Note" nil nil t)) (buffer-name note))))) (ert-deftest seam-test-link-update () - "Test that renaming a note updates its HTML and that of notes which link to it." + "Test that renaming a note updates its HTML and that of notes +which link to it." (should (equal '(("qux.html") ("public/qux.org") @@ -222,9 +228,23 @@ (seam-test-replace-contents bar "* qux") (list (seam-test-links-from-html "html/foo.html") - (mapcar #'seam-test-remove-testdir (seam-get-links-from-file (buffer-file-name foo))) + (mapcar #'seam-test-strip-testdir (seam-get-links-from-file (buffer-file-name foo))) (seam-test-list-files))))))) +(ert-deftest seam-test-link-update-no-unnecessary-export () + "Test that updating the contents of a note does not unnecessarily +re-export note to which it links." + (should-not + (member + "html/bar.html" + (seam-test-with-notes () + ((foo "foo" "public") + (bar "bar" "public")) + (seam-test-add-contents foo (seam-test-link-to-buffer bar)) + (delete-file "html/bar.html") + (seam-test-add-contents foo "hello") + (seam-test-list-files))))) + (ert-deftest seam-test-link-to-private () "Test that a private link does not get exported in HTML." (should-error @@ -238,8 +258,8 @@ (re-search-forward "<a href=\"/bar.html\">"))))) (ert-deftest seam-test-link-no-extension () - "Test that the :no-extension option causes links to render without .html -extension." + "Test that the :no-extension option causes links to render without +.html extension." (should (identity (seam-test-with-notes ((seam-export-alist @@ -256,8 +276,8 @@ extension." (re-search-forward "<a href=\"/bar\">")))))) (ert-deftest seam-test-link-internal-class () - "Test that setting `seam-export-internal-link-class' correctly renders -the class." + "Test that setting `seam-export-internal-link-class' correctly +renders the class." (should (identity (seam-test-with-notes ((seam-export-internal-link-class "internal")) @@ -283,13 +303,13 @@ the class." (seam-test-add-contents foo (seam-test-link-to-buffer qux)) (seam-test-add-contents bar (seam-test-link-to-buffer qux)) (list - (mapcar #'seam-test-remove-testdir (seam-get-links-from-file (buffer-file-name foo))) - (mapcar #'seam-test-remove-testdir (seam-get-links-to-file (buffer-file-name bar))) - (mapcar #'seam-test-remove-testdir (seam-get-links-to-file (buffer-file-name qux)))))))) + (mapcar #'seam-test-strip-testdir (seam-get-links-from-file (buffer-file-name foo))) + (mapcar #'seam-test-strip-testdir (seam-get-links-to-file (buffer-file-name bar))) + (mapcar #'seam-test-strip-testdir (seam-get-links-to-file (buffer-file-name qux)))))))) (ert-deftest seam-test-delete-note () - "Test that deleting a note also deletes its HTML and re-exports linking -notes such that they no longer link to it." + "Test that deleting a note also deletes its HTML and re-exports +linking notes such that they no longer link to it." (should (equal '(nil ("html/foo.html" "public/foo.org")) @@ -305,10 +325,11 @@ notes such that they no longer link to it." (seam-test-list-files)))))) (ert-deftest seam-test-backlinks-public () - "Test that linking to a note from a public note creates a backlink." + "Test that linking to a note from a public note creates a +backlink." (should (identity - (seam-test-with-notes ((seam-export-template-string "{{backlinks}}")) + (seam-test-with-notes ((seam-export-template-string "{{{backlinks}}}")) ((foo "foo" "public") (bar "bar" "public")) (with-current-buffer foo @@ -320,33 +341,53 @@ notes such that they no longer link to it." (ert-deftest seam-test-backlinks-private () "Test that linking to a note from a private note does not create a backlink." - (should-error - (seam-test-with-notes ((seam-export-template-string "{{backlinks}}")) - ((foo "foo") - (bar "bar" "public")) - (with-current-buffer foo - (seam-test-add-contents foo (seam-test-link-to-buffer bar))) - (with-temp-buffer - (insert-file-contents "html/bar.html") - (re-search-forward "<a href=\"/foo.html\">"))))) + (should + (equal + "" + (seam-test-with-notes ((seam-export-template-string "{{{backlinks}}}")) + ((foo "foo") + (bar "bar" "public")) + (with-current-buffer foo + (seam-test-add-contents foo (seam-test-link-to-buffer bar))) + (with-temp-buffer + (insert-file-contents "html/bar.html") + (buffer-string)))))) (ert-deftest seam-test-backlinks-delete () "Test that deleting a note removes backlink." - (should-error - (seam-test-with-notes ((seam-export-template-string "{{backlinks}}")) - ((foo "foo" "public") - (bar "bar" "public")) - (with-current-buffer foo - (seam-test-add-contents foo (seam-test-link-to-buffer bar))) - (let ((delete-by-moving-to-trash nil)) - (seam-delete-note (buffer-file-name foo))) - (with-temp-buffer - (insert-file-contents "html/bar.html") - (re-search-forward "<a href=\"/foo.html\">"))))) + (should + (equal + "" + (seam-test-with-notes ((seam-export-template-string "{{{backlinks}}}")) + ((foo "foo" "public") + (bar "bar" "public")) + (with-current-buffer foo + (seam-test-add-contents foo (seam-test-link-to-buffer bar))) + (let ((delete-by-moving-to-trash nil)) + (seam-delete-note (buffer-file-name foo))) + (with-temp-buffer + (insert-file-contents "html/bar.html") + (buffer-string)))))) + +(ert-deftest seam-test-backlinks-draft () + "Test that linking to a note from a draft note does not create a +backlink." + (should + (equal + "" + (seam-test-with-notes ((seam-export-template-string "{{{backlinks}}}")) + ((foo "foo" "public" nil t) + (bar "bar" "public")) + (with-current-buffer foo + (seam-test-add-contents foo (seam-test-link-to-buffer bar))) + (with-temp-buffer + (insert-file-contents "html/bar.html") + (buffer-string)))))) (ert-deftest seam-test-set-type-private () - "Test that setting a public note to private will delete its HTML file and -update linking HTML files such that they no longer link to it." + "Test that setting a public note to private will delete its HTML +file and update linking HTML files such that they no longer link +to it." (should (equal '(nil ("html/foo.html" "private/bar.org" "public/foo.org")) @@ -384,6 +425,59 @@ update linking HTML files such that they link to it." ((foo "foo")) (seam-set-note-type (buffer-file-name foo) "invalid-type")))) +(ert-deftest seam-test-create-draft () + (should + (equal + '("public/-note.org") + (seam-test-with-notes ((seam-create-as-draft t)) + ((note "Note" "public")) + (seam-test-list-files))))) + +(ert-deftest seam-test-create-draft-override () + (should + (equal + '("public/-note.org") + (seam-test-with-notes ((seam-note-types + '(("public" :create-as-draft t)))) + ((note "Note" "public")) + (seam-test-list-files))))) + +(ert-deftest seam-test-set-draft () + "Test that toggling a note from non-draft to draft will delete its +HTML file and update linking HTML files such that they no longer +link to it." + (should + (equal + '(nil ("html/foo.html" "public/-bar.org" "public/foo.org")) + (seam-test-with-notes () + ((foo "foo" "public") + (bar "bar" "public")) + (with-current-buffer foo + (seam-test-add-contents foo (seam-test-link-to-buffer bar))) + (with-current-buffer bar + (call-interactively 'seam-toggle-draft)) + (list + (seam-test-links-from-html "html/foo.html") + (seam-test-list-files)))))) + +(ert-deftest seam-test-unset-draft () + "Test that toggling a note from draft to non-draft will export its +HTML file and update linking HTML files such that they link to +it." + (should + (equal + '(("bar.html") ("html/bar.html" "html/foo.html" "public/bar.org" "public/foo.org")) + (seam-test-with-notes () + ((foo "foo" "public") + (bar "bar" "public" nil t)) + (with-current-buffer foo + (seam-test-add-contents foo (seam-test-link-to-buffer bar))) + (with-current-buffer bar + (call-interactively 'seam-toggle-draft)) + (list + (seam-test-links-from-html "html/foo.html") + (seam-test-list-files)))))) + (ert-deftest seam-test-follow-link-existing () "Test that following a link to an existing note opens that note." (should @@ -399,7 +493,8 @@ update linking HTML files such that they link to it." (buffer-name)))))) (ert-deftest seam-test-follow-link-new () - "Test that following a link to an nonexistent note creates and opens that note." + "Test that following a link to an nonexistent note creates and +opens that note." (should (equal '("bar" ("private/bar.org" "private/foo.org")) @@ -416,14 +511,75 @@ update linking HTML files such that they link to it." (seam-test-list-files)) (kill-buffer))))))) +(ert-deftest seam-test-follow-link-new-draft () + "Test that following a link to an nonexistent draft note creates +and opens that note." + (should + (equal + '("-bar" ("private/-bar.org" "private/foo.org")) + (seam-test-with-notes () + ((foo "foo")) + (with-current-buffer foo + (seam-test-add-contents foo "[[seam:-bar]]") + (goto-char 1) + (org-next-link) + (org-open-at-point) + (unwind-protect + (list + (buffer-name) + (seam-test-list-files)) + (kill-buffer))))))) + (ert-deftest seam-test-escape-title () (should (equal - "“quotes” & <symbols>\n" - (seam-test-with-notes ((seam-export-template-string "{{title}}")) + "“quotes” & <symbols>\n“quotes” & <symbols>\n" + (seam-test-with-notes ((seam-export-template-string "{{title}}\n{{{raw-title}}}")) ((note "\"quotes\" & <symbols>" "public")) (seam-export--file-string "html/quotes-symbols.html"))))) +(ert-deftest seam-test-custom-slug () + "Test that setting the SEAM_SLUG property saves and exports +accordingly." + (should + (equal + '("html/c-vs-cpp.html" "public/c-vs-cpp.org") + (seam-test-with-notes () + ((note "C vs C++" "public")) + (seam-test-add-contents note ":PROPERTIES:\n:SEAM_SLUG: c-vs-cpp\n:END:") + (seam-test-list-files))))) + +(ert-deftest seam-test-removing-type-from-export-alist () + (should + (equal + '("public/note.org") + (seam-test-with-notes () + ((note "Note" "public")) + (setq seam-export-alist + `((,(file-name-concat seam-test-directory "html") + :types ("foo") + :root-path "/"))) + (seam-export-all-notes) + (seam-test-list-files))))) + +(ert-deftest seam-test-template-values () + "Test that custom variables can be used in templates, and that +existing ones can be overridden." + (should + (equal + "Qux\nhello, world\n" + (seam-test-with-notes + ((seam-export-template-values '(("title" . "Bar") + ("greeting" . "hello, world"))) + (seam-export-template-string "{{title}}\n{{greeting}}") + (seam-export-alist + `((,(file-name-concat seam-test-directory "html") + :types ("public") + :root-path "/" + :template-values (("title" . "Qux")))))) + ((foo "Foo" "public")) + (seam-export--file-string "html/foo.html"))))) + (provide 'seam-test) ;;; seam-test.el ends here @@ -33,6 +33,7 @@ ;;; Code: (require 'seam-export) +(require 'org) (require 'cl-lib) (require 'grep) @@ -52,13 +53,33 @@ :type 'string) (defcustom seam-note-types '("private" "public") - "Seam note types." + "List of valid Seam note types. Each element can either be a +string (the name of the type), or an alist. If using an alist, +the car should be the type name, and the cdr should be a plist +containing any number of these properties: + + `:create-as-draft' + + When this is non-nil, new Seam notes of this type will be + created as drafts. If this is missing, falls back to + `seam-create-as-draft'." :group 'seam - :type '(repeat string)) + :type '(repeat + (choice string + (alist :key-type string :value-type plist)))) -(defun seam-format-title-default (title type) - "Default Seam title formatter. Formats like this: \"TITLE (TYPE)\"." - (format "%s %s" title (propertize (format "(%s)" type) 'face 'font-lock-comment-face))) +(defcustom seam-create-as-draft nil + "When non-nil, new Seam notes will be created as drafts." + :group 'seam + :type 'boolean) + +(defun seam-format-title-default (title type draft-p) + "Default Seam title formatter. Formats like this: \"TITLE (TYPE[ draft])\"." + (format "%s %s" + title + (propertize + (format "(%s%s)" type (if draft-p " draft" "")) + 'face 'font-lock-comment-face))) (defcustom seam-title-formatter #'seam-format-title-default @@ -70,21 +91,31 @@ naming. Must be a function taking two arguments: TITLE and TYPE." (defun seam-html-directories () (mapcar #'car seam-export-alist)) -(defun seam-lookup-slug (slug) - (cl-dolist (type seam-note-types) - (let ((file (file-name-concat seam-note-directory type (concat slug ".org")))) - (when (file-exists-p file) - (cl-return (expand-file-name file)))))) +(defun seam-slugify (title) + (setq title (string-replace "'" "" title)) + (setq title (string-split title "\\W+" t)) + (setq title (string-join title "-")) + (downcase title)) -(defun seam--check-conflict (title) - (when (seam-lookup-slug (seam-slugify title)) - (error "`%s' would conflict with an existing note" title))) +(defun seam-lookup-slug (slug) + (cl-dolist (type (seam-get-all-note-type-names)) + (let ((file (file-name-concat seam-note-directory type (concat slug ".org"))) + (draft-file (file-name-concat seam-note-directory type (concat "-" slug ".org")))) + (cond + ((file-exists-p file) + (cl-return (expand-file-name file))) + ((file-exists-p draft-file) + (cl-return (expand-file-name draft-file))))))) + +(defun seam--check-conflict (slug) + (when (seam-lookup-slug slug) + (error "A note called `%s.org' already exists" slug))) (defun seam-link-open (path _prefix) (org-mark-ring-push) (if-let ((file (seam-lookup-slug path))) (find-file file) - (seam-make-note path nil t)) + (seam-create-note path nil t (seam-draft-p path))) (seam-set-buffer-name)) (defvar seam-note-file-regexp "\\`[^.].+\\.org\\'") @@ -102,7 +133,7 @@ naming. Must be a function taking two arguments: TITLE and TYPE." (defun seam-ensure-note-subdirectories-exist () (unless seam-note-directory (error "Please set `seam-note-directory'")) - (cl-dolist (type seam-note-types) + (cl-dolist (type (seam-get-all-note-type-names)) (let ((dir (file-name-concat seam-note-directory type))) (seam-ensure-directory-exists dir)))) @@ -112,6 +143,9 @@ naming. Must be a function taking two arguments: TITLE and TYPE." :type '(choice (const :tag "Sort by title" title) (const :tag "Sort by modification date" modified))) +(defun seam-get-all-note-type-names () + (mapcar (lambda (x) (car (ensure-list x))) seam-note-types)) + (cl-defgeneric seam-get-all-notes (sort-by)) (cl-defmethod seam-get-all-notes ((sort-by (eql 't))) @@ -153,8 +187,7 @@ naming. Must be a function taking two arguments: TITLE and TYPE." (save-restriction (widen) (goto-char 1) - (ignore-errors - (re-search-forward (org-headline-re 1)) + (when (re-search-forward "^\\* " nil t) (let ((start (point))) (end-of-line) (let ((title (string-trim (buffer-substring-no-properties start (point))))) @@ -166,26 +199,44 @@ naming. Must be a function taking two arguments: TITLE and TYPE." (insert-file-contents file) (seam-get-title-from-buffer))) -(defun seam-format-title (title type) - (funcall seam-title-formatter title type)) +(defun seam-get-slug-from-file-name (file) + (string-remove-prefix "-" (file-name-base file))) -(defun seam--completing-read (&rest args) - (let ((completion-ignore-case t)) - (apply seam-completing-read-function args))) +(cl-defun seam-get-slug-from-buffer (&optional (buffer (current-buffer))) + (or (with-current-buffer buffer + (save-mark-and-excursion + (save-restriction + (widen) + (goto-char 1) + (when (re-search-forward "^\\* " nil t) + (org-element-property :SEAM_SLUG (org-element-at-point)))))) + (seam-slugify (seam-get-title-from-buffer buffer)))) -(defun seam-slugify (title) - (downcase (string-join (string-split title "\\W+" t) "-"))) +(defun seam-format-title (title type draft-p) + (funcall seam-title-formatter title type draft-p)) + +(defun seam-validate-note-type (type) + (unless (member type (seam-get-all-note-type-names)) + (error "`%s' is not a valid Seam note type" type))) -(defun seam-make-note (title &optional type select) +(cl-defun seam-create-note (title &optional type select (draft-p nil draft-supplied-p)) (unless type (setq type seam-default-note-type)) - (unless (member type seam-note-types) - (error "`%s' is not a valid Seam note type" type)) + (seam-validate-note-type type) (seam-ensure-note-subdirectories-exist) - (let ((file (file-name-concat seam-note-directory - type - (concat (seam-slugify title) ".org")))) - (seam--check-conflict title) + (let* ((slug (seam-slugify title)) + (draft-p + (if draft-supplied-p + draft-p + (cl-getf (cdr (assoc type (mapcar #'ensure-list seam-note-types))) + :create-as-draft + seam-create-as-draft))) + (file (file-name-concat seam-note-directory + type + (concat (when draft-p "-") slug ".org")))) + (when (string= "" slug) + (error "Cannot create a note with an empty slug")) + (seam--check-conflict slug) (let ((buffer (funcall (if select #'find-file #'find-file-noselect) file))) (with-current-buffer buffer (insert (format "* %s\n" title)) @@ -202,16 +253,27 @@ naming. Must be a function taking two arguments: TITLE and TYPE." :test #'equal) (and self (list self))))) (let ((files (cl-loop for (title . file) in notes - collect (cons (seam-format-title title (seam-get-note-type file)) file)))) - (let ((completion (string-trim (seam--completing-read prompt files)))) + collect (cons (seam-format-title + title + (seam-get-note-type file) + (seam-draft-p file)) + file)))) + (let ((completion (string-trim (funcall seam-completing-read-function prompt (mapcar #'car files))))) (or (assoc completion files) (cons completion nil))))))) (defun seam--read-type (prompt arg &optional choices) (when arg (if (listp arg) - (seam--completing-read prompt (or choices seam-note-types) nil t) - (nth (1- arg) seam-note-types)))) + (let ((type (funcall seam-completing-read-function + prompt + (or choices (seam-get-all-note-type-names)) + nil + t))) + (seam-validate-note-type type) + type) + (nth (1- arg) + (seam-get-all-note-type-names))))) ;;;###autoload (defun seam-find-note (arg) @@ -225,7 +287,7 @@ completion prompt is given to choose the type." (interactive "P") (let* ((type (seam--read-type "Type: " arg)) (seam--subset - (if type (list type) seam-note-types))) + (if type (list type) (seam-get-all-note-type-names)))) (cl-destructuring-bind (completion . file) (seam-read-title "Open note: ") (if file @@ -234,101 +296,145 @@ completion prompt is given to choose the type." ;; formatter function) (NOTE: Redundant if buffer wasn't ;; already open, as `seam-setup-buffer' does this too.) (seam-set-buffer-name)) - (seam-make-note (string-trim completion) (or type seam-default-note-type) t))))) + (seam-create-note (string-trim completion) (or type seam-default-note-type) t))))) (cl-defun seam-get-note-type (file &optional no-error) (when (and file (equal "org" (file-name-extension file))) (let ((type (cadr (nreverse (file-name-split file))))) - (when (member type seam-note-types) + (when (member type (seam-get-all-note-type-names)) (cl-return-from seam-get-note-type type)))) (unless no-error (error "%s is not a Seam note" file))) -(defun seam-make-file-name (slug type) +(defun seam-make-file-name (slug type &optional draft) (expand-file-name (file-name-concat seam-note-directory type - (concat slug ".org")))) + (concat (when draft "-") slug ".org")))) -(defun seam-get-links-to-file (file) +(defun seam-get-links-to-file (file &optional include-drafts) "Return filename of each note which links to FILE." - (remove (expand-file-name file) - (seam-note-files-containing-string (format "[[seam:%s]" (file-name-base file))))) + (cl-loop for file in (remove (expand-file-name file) + (seam-note-files-containing-string + (format "[[seam:%s]" (file-name-base file)))) + when (or include-drafts + seam-export--include-drafts + (not (seam-draft-p file))) + collect file)) + +(cl-defun seam-get-links-from-buffer (&optional (buffer (current-buffer))) + "Return filename of each existing note which is linked to from BUFFER." + (let ((links (with-current-buffer buffer + (save-mark-and-excursion + (save-restriction + (widen) + (goto-char 1) + (delete-dups + (cl-loop for ret = (re-search-forward "\\[\\[seam:\\(.*?\\)\\]" nil t) + while ret collect (match-string 1)))))))) + (let ((file (buffer-file-name buffer))) + (remove (and file (expand-file-name file)) + (cl-loop for link in links + as f = (seam-lookup-slug link) + when f collect f))))) (defun seam-get-links-from-file (file) "Return filename of each existing note which is linked to from FILE." - (let ((links - (with-temp-buffer - (insert-file-contents file) - (delete-dups - (cl-loop for ret = (re-search-forward "\\[\\[seam:\\(.*?\\)\\]" nil t) - while ret collect (match-string 1)))))) - (remove (expand-file-name file) - (cl-loop for link in links - as f = (seam-lookup-slug link) - when f collect f)))) + (with-temp-buffer + (insert-file-contents file) + (seam-get-links-from-buffer))) (defun seam-delete-html-files-for-note (note-file) - (dolist (dir (seam-html-directories)) - (let ((html (file-name-concat dir (concat (file-name-base note-file) ".html")))) - (when (file-exists-p html) - (delete-file html) - (message "Deleted %s" html))))) - -(defun seam-post-save-or-rename (old new &optional previous-links-from-file) - (unless (string= old new) - (seam-update-links old new) - (seam-delete-html-files-for-note old) + (let ((html-nd (concat (seam-get-slug-from-file-name note-file) ".html"))) (dolist (dir (seam-html-directories)) - (delete-file (file-name-concat dir (concat (file-name-base old) ".html"))))) + (let ((html (file-name-concat dir html-nd))) + (when (file-exists-p html) + (delete-file html) + (message "Deleted %s" html)))))) + +(defun seam--rename-file (old new interactive) + (rename-file old new) + (when interactive + (set-visited-file-name new nil t) + (seam-set-buffer-name)) + (seam-post-save-or-rename old new)) + +(defun seam-post-save-or-rename (old new &optional previous-links-from-file slug-or-title-changed) + (unless (string= old new) + (seam-update-links old new)) + (seam-delete-html-files-for-note old) (seam-export-note new) - (let ((removed-links (cl-set-difference previous-links-from-file - (seam-get-links-from-file new) - :test #'string=))) - (mapc #'seam-export-note - (delete-dups - (append removed-links - (seam-get-links-from-file new) - ;; If our type changes, we cannot rely on - ;; `seam-update-links' to trigger a re-render of - ;; the pages that link to us, as types are not - ;; encoded in the link. - (unless (string= (seam-get-note-type old) - (seam-get-note-type new)) - (seam-get-links-to-file new))))))) + (let* ((current-links (seam-get-links-from-file new)) + (added-links (cl-set-difference current-links + previous-links-from-file + :test #'string=)) + (removed-links (cl-set-difference previous-links-from-file + current-links + :test #'string=))) + (let ((type-changed + (not (string= (seam-get-note-type old) + (seam-get-note-type new))))) + (mapc #'seam-export-note + (delete-dups + (append + removed-links + + ;; Backlinks sections must be updated when either + ;; slug or title changes. + (if slug-or-title-changed + current-links + added-links) + + ;; `seam-update-links' inherently triggers + ;; re-exporting of notes when links change. + ;; However, note type is not encoded in the link, + ;; so we must handle that case manually. + (when type-changed + (seam-get-links-to-file new)))))))) + +(defun seam-draft-p (file) + (string-prefix-p "-" (file-name-base file))) (defun seam-save-buffer () (let* ((old (buffer-file-name)) - (type (seam-get-note-type old t))) + (type (seam-get-note-type old t)) + (draft-p (seam-draft-p old))) (when type - (let* ((title (or (seam-get-title-from-buffer) - (error "Note must have a title"))) - (slug (seam-slugify title)) - (new (seam-make-file-name slug type))) + (unless (seam-get-title-from-buffer) + (error "Note must have a title")) + (let* ((slug (seam-get-slug-from-buffer)) + (new (seam-make-file-name slug type draft-p)) + (newly-created-p (not (file-exists-p old))) + (slug-changed-p (not (string= slug (file-name-base old)))) + (title-changed-p (unless newly-created-p + (not (string= (seam-get-title-from-buffer) + (seam-get-title-from-file old)))))) (unless (string= old new) ;This is valid because ;`seam-save-buffer' cannot - ;change type. - (seam--check-conflict title) + ;change type or draft status. + (seam--check-conflict slug) (rename-file old new) (set-visited-file-name new nil t)) (let ((previous-links-from-file ;; If we've yet to create the file, don't check it. - (when (file-exists-p new) + (unless newly-created-p (seam-get-links-from-file new)))) (let ((write-contents-functions (remove 'seam-save-buffer write-contents-functions))) (save-buffer)) - (seam-post-save-or-rename old new previous-links-from-file) + (seam-post-save-or-rename old + new + previous-links-from-file + (or slug-changed-p title-changed-p)) (seam-set-buffer-name) t))))) -(defun seam--set-note-type (file new-type) +(defun seam--set-note-type (file new-type interactive) (let ((old-type (seam-get-note-type file)) (new-file (seam-make-file-name (file-name-base file) new-type))) (if (string= new-type old-type) file - (rename-file file new-file) - (seam-post-save-or-rename file new-file) + (seam--rename-file file new-file interactive) new-file))) ;;;###autoload @@ -345,28 +451,38 @@ from 1). Otherwise a completion prompt is given for the desired type." (or (seam--read-type "New type: " ;; HACK: Treat nil prefix as C-u. (or current-prefix-arg '(4)) - (remove old-type seam-note-types)) + (remove old-type (seam-get-all-note-type-names))) old-type) t))) - (let ((new-file (seam--set-note-type file new-type))) - (when interactive - (set-visited-file-name new-file nil t) - (seam-set-buffer-name)))) + (seam--set-note-type file new-type interactive)) + +;;;###autoload +(defun seam-toggle-draft (file &optional interactive) + "Toggle the draft status of Seam note FILE." + (interactive (list (buffer-file-name) t)) + (seam-get-note-type file) ;Error if file isn't a Seam note. + (let* ((base (file-name-nondirectory file)) + (new-file (file-name-concat + (file-name-directory file) + (if (string-prefix-p "-" base) + (string-remove-prefix "-" base) + (concat "-" base))))) + (seam--rename-file file new-file interactive))) (defun seam-update-links (old new) - (let ((old-slug (file-name-base old)) - (new-slug (file-name-base new))) - (unless (string= old-slug new-slug) + (let* ((old-link (file-name-base old)) + (new-link (file-name-base new))) + (unless (string= old-link new-link) (let ((count (seam-replace-string-in-all-notes - (format "[[seam:%s]" old-slug) - (format "[[seam:%s]" new-slug) + (format "[[seam:%s]" old-link) + (format "[[seam:%s]" new-link) t))) (unless (zerop count) (message "Updated links in %d file%s" count (if (= count 1) "" "s"))))))) (defun seam--active-subset () - (or seam--subset seam-note-types)) + (or seam--subset (seam-get-all-note-type-names))) (defun seam-note-subdirectories () (cl-loop for type in (seam--active-subset) @@ -406,7 +522,7 @@ Otherwise, it's nil." find-program (string-join (mapcar (lambda (type) (shell-quote-argument (concat type "/"))) - seam-note-types) + (seam-get-all-note-type-names)) " ") (shell-quote-argument "*.org") (shell-quote-argument ".*") @@ -460,10 +576,13 @@ Otherwise, it's nil." update-count))) (cl-defun seam-set-buffer-name (&optional (buffer (current-buffer))) - (with-current-buffer buffer - (rename-buffer - (seam-format-title (seam-get-title-from-buffer) - (seam-get-note-type (buffer-file-name buffer)))))) + (when-let ((title (seam-get-title-from-buffer))) + (let ((file (buffer-file-name buffer))) + (with-current-buffer buffer + (rename-buffer + (seam-format-title title + (seam-get-note-type file) + (seam-draft-p file))))))) (defun seam-setup-buffer () "Setup hooks when loading a Seam file." @@ -501,12 +620,13 @@ buffer is killed after deletion." (let ((file (buffer-file-name))) (seam-get-note-type file) ;Error if file isn't a Seam note. (list - (let ((incoming (length (seam-get-links-to-file file)))) + (let ((incoming (length (seam-get-links-to-file file t)))) (and (yes-or-no-p - (format "Really %s file and kill buffer%s?" + (format "Really %s `%s' and kill buffer%s?" (if delete-by-moving-to-trash "trash" "delete") + (seam-get-title-from-buffer) (if (> incoming 0) (format " (breaking links from %d note%s)" incoming @@ -528,7 +648,7 @@ link will replace it." (cl-destructuring-bind (completion . file) (seam-read-title "Insert note link: ") (let* ((new-buffer (unless file - (seam-make-note completion seam-default-note-type nil))) + (seam-create-note completion seam-default-note-type nil))) (selection (when (use-region-p) (buffer-substring (region-beginning) @@ -551,7 +671,8 @@ link will replace it." "k" #'seam-delete-note "l" #'seam-insert-link "s" #'seam-search - "t" #'seam-set-note-type) + "t" #'seam-set-note-type + "d" #'seam-toggle-draft) (org-link-set-parameters "seam" :follow #'seam-link-open) |