diff options
| -rw-r--r-- | CHANGELOG.org | 102 | ||||
| -rw-r--r-- | CHANGES.org | 37 | ||||
| -rw-r--r-- | README.org | 24 | ||||
| -rw-r--r-- | lisp/seam-export.el | 315 | ||||
| -rw-r--r-- | lisp/seam-html.el | 49 | ||||
| -rw-r--r-- | lisp/seam-test.el | 258 | ||||
| -rw-r--r-- | lisp/seam.el | 211 | 
7 files changed, 734 insertions, 262 deletions
| diff --git a/CHANGELOG.org b/CHANGELOG.org new file mode 100644 index 0000000..bdc6833 --- /dev/null +++ b/CHANGELOG.org @@ -0,0 +1,102 @@ +** 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 variables + +- =seam-export-time-format-datetime= is now +  =seam-export-time-format-dt=. + +**** 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. + +- Creation and modification date can now be set explicitly using the +  =SEAM_CREATED= and =SEAM_MODIFIED= properties.  These dates can be +  accessed within templates.  The ={{#modified?}}= block tests whether +  the two dates are unequal, and the behavior of this can be +  customized with =seam-export-ignore-same-day-modifications= and +  =:ignore-same-day-modifications=. + +*** 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. + +- Per-profile export options now override global options in all cases. diff --git a/CHANGES.org b/CHANGES.org deleted file mode 100644 index bee357b..0000000 --- a/CHANGES.org +++ /dev/null @@ -1,37 +0,0 @@ -** Changes since 0.1.0 - -*** Breaking changes - -- Code has been moved to =lisp/= subdirectory, where it should have -  been all along.  Make sure to add =seam/lisp= to your =load-path= -  instead of just =seam=. - -**** Renamed functions - -- =seam-replace-string-in-notes= is now =seam-replace-string-in-all-notes=. -- =seam-visited-files= is now =seam-visited-notes=. - -*** New features - -- Option to export internal links with a custom CSS class -  (=seam-export-internal-link-class= / =:internal-link-class=). -- =SEAM_SLUG= property can be added to title headline to set a custom -  slug. - -*** Improvements - -- Notes are no longer re-exported unnecessarily whenever a linked note -  is changed. -- Title is now mentioned when deleting a note, to make it less likely -  you delete the wrong one by mistake. - -*** Bugfixes - -- =seam-visited-notes= no longer returns buffers visiting non-note -  files within the Seam directory. -- 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 your completing-read function returns an -  invalid type. @@ -1,7 +1,7 @@  ** A personal wiki toolkit for Emacs  [[https://wiki.plexwave.org/seam][Seam]] leverages the power of [[https://orgmode.org/][Org mode]] to make creating, linking, and -exporting your notes easier.  It is geared particularly towards +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]]. @@ -10,14 +10,18 @@ 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]]. @@ -29,3 +33,21 @@ 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/lisp/seam-export.el b/lisp/seam-export.el index 7fa6c2d..549eeca 100644 --- a/lisp/seam-export.el +++ b/lisp/seam-export.el @@ -28,14 +28,24 @@  ;;; Code:  (require 'cl-lib) +(require 'mustache) +(require 'ox-ascii) +(require 'ox-org) +(require 'time-date)  (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--time-format nil) +(defvar seam-export--time-format-dt nil) +(defvar seam-export--time-zone nil) +(defvar seam-export--ignore-same-day-modifications nil)  (defvar seam-export--internal-link-class nil) -(defvar seam-export--options nil) +(defvar seam-export--backend-options nil)  (defgroup seam-export nil    "Options for Seam exporter." @@ -55,26 +65,59 @@ 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 template variables and their values.  Values +    specified here will take precedence over those in +    `seam-export-template-values'.  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      nil. +  `:time-format' + +    Human-readable format for template time strings.  Defaults to +    the value of `seam-export-time-format'. + +  `:time-format-dt' + +    Machine-readable format for template time strings.  Defaults +    to the value of `seam-export-time-format-dt'. + +  `:time-zone' + +    Time zone used for template time strings.  Defaults to the +    value of `seam-export-time-zone'. + +  `:ignore-same-day-modifications' + +    Whether the `modified?' template variable should be false if +    creation and modification date are on the same day.  Defaults +    to the value of `seam-export-ignore-same-day-modifications'. +    `:internal-link-class'      CSS class name for internal links.  Defaults to the value of @@ -107,21 +150,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,12 +174,31 @@ 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. + +  `slug' + +   The note's slug (that is, its filename without any extension).    `backlinks'     A list (<ul>) of notes that link to the given note. +  `created' + +   The human-readable date that the note was created.  See +   `seam-export-time-format'. + +  `created-dt' + +   The machine-readable date that the note was created.  See +   `seam-export-time-format-dt'. +    `modified'     The human-readable date that the note was last modified.  See @@ -142,8 +206,13 @@ interpolated using the {{variable}} syntax:    `modified-dt' -   The machine-readable date that the note was last modified. See -   `seam-export-time-format-datetime'.") +   The machine-readable date that the note was last modified. +   See `seam-export-time-format-dt'. + +  `modified?' + +   When used as a block, this will render only when the creation +   and modification dates are not the same.")  (defcustom seam-export-template-string seam-export-default-template-string    "The HTML template string to be used by the exporter.  The template @@ -153,13 +222,21 @@ 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'."    :group 'seam-export    :type 'string) -(defcustom seam-export-time-format-datetime "%Y-%m-%d" +(defcustom seam-export-time-format-dt "%Y-%m-%d"    "Machine-readable format for template time strings.  Meant to be used in  the datetime attribute of <time>.  Passed to `format-time-string'."    :group 'seam-export @@ -171,6 +248,10 @@ the datetime attribute of <time>.  Passed to `format-time-string'."    :group 'seam-export    :type 'sexp) +(defcustom seam-export-ignore-same-day-modifications t +  "When non-nil, the `modified?' template variable will evaluate to +false if creation and modification date are on the same day.") +  (defcustom seam-export-internal-link-class nil    "CSS class name to use for internal links (i.e., links to other Seam  notes)." @@ -196,8 +277,8 @@ notes)."     :with-smart-quotes t     :with-toc nil)) -(defmacro seam-export--to-string (&rest body) -  (declare (indent 0)) +(cl-defmacro seam-export--export-to-string ((&key backend) &rest body) +  (declare (indent 1))    (let ((buf (gensym)))      `(let ((,buf (generate-new-buffer " *seam-export*")))         (unwind-protect @@ -205,72 +286,135 @@ notes)."                      ,@body                      ;; This let prevents Org from popping up a window.                      (let ((org-export-show-temporary-export-buffer nil)) -                      (org-export-to-buffer 'seam ,buf nil nil nil t seam-export--options nil))) +                      (org-export-to-buffer ,backend ,buf nil nil nil t seam-export--backend-options nil)))                    (with-current-buffer ,buf                      (buffer-string)))           (kill-buffer ,buf))))) -;;; Some HACK-ery to get fully escaped and smartquote-ized string. -(defun seam-export--escape-string (s) +(defun seam-export--convert-string (backend s) +  "Export Org string using the given Org exporter backend." +  (seam-export--export-to-string (:backend backend) +    (insert s))) + +(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 -      (insert s))))) +    (seam-export--convert-string 'seam s)))) -(defun seam-export--replace-variable (var replacement) -  (goto-char 1) -  (while (re-search-forward (format "{{%s}}" var) nil t) -    (replace-match replacement t t))) +(defun seam-export--org-to-text (s) +  "Convert single-line Org string to plain text via Org exporter." +  (string-chop-newline +   (let ((org-ascii-charset 'utf-8)) +     (seam-export--convert-string 'ascii s)))) + +(defun seam-export--get-props (file props) +  (with-temp-buffer +    (insert-file-contents file) +    (when (re-search-forward "^\\* " nil t) +      (org-mode) +      (cl-loop for prop in props +               collect (org-element-property prop (org-element-at-point))))))  (defun seam-export--generate-backlinks (file) -  (seam-export--to-string -    (let ((files (sort +  (seam-export--export-to-string (:backend 'seam) +    (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))))))) +;;; This was copied from time-date.el, with the addition of a ZONE +;;; argument. +(defun seam-export--time-to-days (time &optional zone) +  "The absolute pseudo-Gregorian date for TIME, a time value. +The absolute date is the number of days elapsed since the imaginary +Gregorian date Sunday, December 31, 1 BC." +  (let* ((tim (decode-time time zone)) +	       (year (decoded-time-year tim))) +    (+ (time-date--day-in-year tim)     ;	Days this year +       (* 365 (1- year))                ;	+ Days in prior years +       (/ (1- year) 4)                  ;	+ Julian leap years +       (- (/ (1- year) 100))            ;	- century years +       (/ (1- year) 400)))) +  (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"))) -        (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)) -      (write-file html-file)))) +  (cl-destructuring-bind (created-prop modified-prop) +      (seam-export--get-props note-file '(:SEAM_CREATED :SEAM_MODIFIED)) +    (let* ((html-file (file-name-concat html-directory +                                        (concat (seam-get-slug-from-file-name note-file) ".html"))) +           (modified +            (or (ignore-errors (parse-iso8601-time-string modified-prop)) +                (file-attribute-modification-time +                 (file-attributes note-file)))) +           (created +            (or (ignore-errors (parse-iso8601-time-string created-prop)) +                modified))) +      (with-temp-buffer +        (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))) +             ("slug" . +              ,(seam-get-slug-from-file-name note-file)) +             ("created" . +              ,(format-time-string +                seam-export--time-format +                created +                seam-export--time-zone)) +             ("created-dt" . +              ,(format-time-string +                seam-export--time-format-dt +                created +                seam-export--time-zone)) +             ("modified" . +              ,(format-time-string +                seam-export--time-format +                modified +                seam-export--time-zone)) +             ("modified-dt" . +              ,(format-time-string +                seam-export--time-format-dt +                modified +                seam-export--time-zone)) +             ("modified?" . +              ,(lambda (template context) +                 (unless (cond +                          (seam-export--ignore-same-day-modifications +                           (= (seam-export--time-to-days +                               created +                               seam-export--time-zone) +                              (seam-export--time-to-days +                               modified +                               seam-export--time-zone))) +                          (t +                           (equal created modified))) +                   (mustache-render template context)))) +             ("contents" . +              ,(seam-export--export-to-string (:backend 'seam) +                 (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)    (with-temp-buffer @@ -278,12 +422,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,17 +440,39 @@ 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--root-path (cl-getf 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--time-format +                          (cl-getf plist +                                   :time-format +                                   seam-export-time-format)) +                         (seam-export--time-format-dt +                          (cl-getf plist +                                   :time-format-dt +                                   seam-export-time-format-dt)) +                         (seam-export--time-zone +                          (cl-getf plist +                                   :time-zone +                                   seam-export-time-zone)) +                         (seam-export--ignore-same-day-modifications +                          (cl-getf plist +                                   :ignore-same-day-modifications +                                   seam-export-ignore-same-day-modifications))                           (seam-export--internal-link-class -                          (or (plist-get plist :internal-link-class) -                              seam-export-internal-link-class)) -                         (seam-export--options (org-combine-plists -                                                seam-export-backend-options -                                                (plist-get plist :backend-options)))) +                          (cl-getf plist +                                   :internal-link-class +                                   seam-export-internal-link-class)) +                         (seam-export--backend-options +                          (org-combine-plists +                           seam-export-backend-options +                           (plist-get plist :backend-options))))                       (seam-export--note-to-html file dir))))))))  (defun seam-export-all-notes () @@ -314,6 +482,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/lisp/seam-html.el b/lisp/seam-html.el index 018e56f..57cd9b9 100644 --- a/lisp/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/lisp/seam-test.el b/lisp/seam-test.el index 5d4a562..5bae015 100644 --- a/lisp/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,7 +228,7 @@                (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 () @@ -252,8 +258,8 @@ re-export note to which it links."         (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 @@ -270,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")) @@ -297,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")) @@ -319,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 @@ -334,47 +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\">"))))) - -(ert-deftest seam-test-backlinks-comment () -  "Test that a commented-out link does not add a backlink." -  :expected-result :failed -  (should-error -   (identity -    (seam-test-with-notes ((seam-export-template-string "{{backlinks}}")) +  (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 (concat "# " (seam-test-link-to-buffer bar)))) +        (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\">")))))) +        (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")) @@ -412,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 @@ -427,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")) @@ -444,16 +511,36 @@ 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." +  "Test that setting the SEAM_SLUG property saves and exports +accordingly."    (should     (equal      '("html/c-vs-cpp.html" "public/c-vs-cpp.org") @@ -462,6 +549,37 @@ update linking HTML files such that they link to it."        (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 diff --git a/lisp/seam.el b/lisp/seam.el index 589e947..eb3037e 100644 --- a/lisp/seam.el +++ b/lisp/seam.el @@ -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 +          (choice string +                  (alist :key-type string :value-type plist)))) + +(defcustom seam-create-as-draft nil +  "When non-nil, new Seam notes will be created as drafts."    :group 'seam -  :type '(repeat string)) +  :type 'boolean) -(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))) +(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 @@ -71,13 +92,20 @@ naming.  Must be a function taking two arguments: TITLE and TYPE."    (mapcar #'car seam-export-alist))  (defun seam-slugify (title) -  (downcase (string-join (string-split title "\\W+" t) "-"))) +  (setq title (string-replace "'" "" title)) +  (setq title (string-split title "\\W+" t)) +  (setq title (string-join title "-")) +  (downcase title))  (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)))))) +  (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) @@ -87,7 +115,7 @@ naming.  Must be a function taking two arguments: TITLE and TYPE."    (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\\'") @@ -105,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)))) @@ -115,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))) @@ -156,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))))) @@ -169,33 +199,43 @@ naming.  Must be a function taking two arguments: TITLE and TYPE."      (insert-file-contents file)      (seam-get-title-from-buffer))) +(defun seam-get-slug-from-file-name (file) +  (string-remove-prefix "-" (file-name-base file))) +  (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) -            (ignore-errors -              (re-search-forward (org-headline-re 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-format-title (title type) -  (funcall seam-title-formatter title type)) +(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-note-types) +  (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))    (seam-validate-note-type type)    (seam-ensure-note-subdirectories-exist)    (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 slug ".org")))) +                                 (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 @@ -213,18 +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 (funcall seam-completing-read-function 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) -        (let ((type (funcall seam-completing-read-function prompt (or choices seam-note-types) nil t))) +        (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-note-types)))) +      (nth (1- arg) +           (seam-get-all-note-type-names)))))  ;;;###autoload  (defun seam-find-note (arg) @@ -238,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 @@ -247,26 +296,31 @@ 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." @@ -291,18 +345,24 @@ completion prompt is given to choose the type."      (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))))) +  (let ((html-nd (concat (seam-get-slug-from-file-name note-file) ".html"))) +    (dolist (dir (seam-html-directories)) +      (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) -    (dolist (dir (seam-html-directories)) -      (delete-file (file-name-concat dir (concat (file-name-base old) ".html"))))) +    (seam-update-links old new)) +  (seam-delete-html-files-for-note old)    (seam-export-note new)    (let* ((current-links (seam-get-links-from-file new))           (added-links (cl-set-difference current-links @@ -332,14 +392,18 @@ completion prompt is given to choose the type."                (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        (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)) +             (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 @@ -347,7 +411,7 @@ completion prompt is given to choose the type."                                                (seam-get-title-from-file old))))))          (unless (string= old new)       ;This is valid because                                          ;`seam-save-buffer' cannot -                                        ;change type. +                                        ;change type or draft status.            (seam--check-conflict slug)            (rename-file old new)            (set-visited-file-name new nil t)) @@ -365,13 +429,12 @@ completion prompt is given to choose the type."            (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 @@ -388,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) @@ -449,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 ".*") @@ -503,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." @@ -544,7 +620,7 @@ 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 `%s' and kill buffer%s?"                        (if delete-by-moving-to-trash @@ -572,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) @@ -595,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) | 
