Commit | Line | Data |
---|---|---|
c8d0cf5c | 1 | ;;; org-inlinetask.el --- Tasks independent of outline hierarchy |
26bd9e87 | 2 | |
cbd20947 | 3 | ;; Copyright (C) 2009-2011 Free Software Foundation, Inc. |
c8d0cf5c CD |
4 | ;; |
5 | ;; Author: Carsten Dominik <carsten at orgmode dot org> | |
6 | ;; Keywords: outlines, hypermedia, calendar, wp | |
7 | ;; Homepage: http://orgmode.org | |
3ab2c837 | 8 | ;; Version: 7.7 |
26bd9e87 GM |
9 | |
10 | ;; This file is part of GNU Emacs. | |
11 | ||
12 | ;; GNU Emacs is free software: you can redistribute it and/or modify | |
54a0dee5 | 13 | |
c8d0cf5c | 14 | ;; it under the terms of the GNU General Public License as published by |
26bd9e87 GM |
15 | ;; the Free Software Foundation, either version 3 of the License, or |
16 | ;; (at your option) any later version. | |
c8d0cf5c CD |
17 | |
18 | ;; GNU Emacs is distributed in the hope that it will be useful, | |
19 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
20 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
21 | ;; GNU General Public License for more details. | |
22 | ||
23 | ;; You should have received a copy of the GNU General Public License | |
26bd9e87 GM |
24 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. |
25 | ||
c8d0cf5c CD |
26 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
27 | ;; | |
28 | ;;; Commentary: | |
29 | ;; | |
30 | ;; This module implements inline tasks in Org-mode. Inline tasks are | |
31 | ;; tasks that have all the properties of normal outline nodes, including | |
32 | ;; the ability to store meta data like scheduling dates, TODO state, tags | |
33 | ;; and properties. However, these nodes are treated specially by the | |
34 | ;; visibility cycling and export commands. | |
35 | ;; | |
86fbb8ca | 36 | ;; Visibility cycling exempts these nodes from cycling. So whenever their |
c8d0cf5c | 37 | ;; parent is opened, so are these tasks. This will only work with |
8bfe682a | 38 | ;; `org-cycle', so if you are also using other commands to show/hide |
c8d0cf5c CD |
39 | ;; entries, you will occasionally find these tasks to behave like |
40 | ;; all other outline nodes, seemingly splitting the text of the parent | |
41 | ;; into children. | |
42 | ;; | |
43 | ;; Export commands do not treat these nodes as part of the sectioning | |
44 | ;; structure, but as a special inline text that is either removed, or | |
3ab2c837 BG |
45 | ;; formatted in some special way. This in handled by |
46 | ;; `org-inlinetask-export' and `org-inlinetask-export-templates' | |
47 | ;; variables. | |
c8d0cf5c CD |
48 | ;; |
49 | ;; Special fontification of inline tasks, so that they can be immediately | |
50 | ;; recognized. From the stars of the headline, only the first and the | |
51 | ;; last two will be visible, the others will be hidden using the | |
52 | ;; `org-hide' face. | |
53 | ;; | |
54 | ;; An inline task is identified solely by a minimum outline level, given | |
55 | ;; by the variable `org-inlinetask-min-level', default 15. | |
56 | ;; | |
3ab2c837 BG |
57 | ;; If you need to have a time planning line (DEADLINE etc), drawers, |
58 | ;; for example LOGBOOK of PROPERTIES, or even normal text as part of | |
59 | ;; the inline task, you must add an "END" headline with the same | |
60 | ;; number of stars. | |
c8d0cf5c | 61 | ;; |
3ab2c837 BG |
62 | ;; As an example, here are two valid inline tasks: |
63 | ;; | |
64 | ;; **************** TODO a small task | |
65 | ;; | |
66 | ;; and | |
67 | ;; | |
68 | ;; **************** TODO another small task | |
c8d0cf5c CD |
69 | ;; DEADLINE: <2009-03-30 Mon> |
70 | ;; :PROPERTIES: | |
71 | ;; :SOMETHING: or other | |
72 | ;; :END: | |
73 | ;; And here is some extra text | |
74 | ;; **************** END | |
75 | ;; | |
76 | ;; Also, if you want to use refiling and archiving for inline tasks, | |
77 | ;; The END line must be present to make things work properly. | |
78 | ;; | |
8bfe682a | 79 | ;; This package installs one new command: |
c8d0cf5c CD |
80 | ;; |
81 | ;; C-c C-x t Insert a new inline task with END line | |
82 | ||
86fbb8ca | 83 | ;;; Code: |
c8d0cf5c CD |
84 | |
85 | (require 'org) | |
86 | ||
87 | (defgroup org-inlinetask nil | |
88 | "Options concerning inline tasks in Org mode." | |
89 | :tag "Org Inline Tasks" | |
90 | :group 'org-structure) | |
91 | ||
92 | (defcustom org-inlinetask-min-level 15 | |
93 | "Minimum level a headline must have before it is treated as an inline task. | |
94 | It is strongly recommended that you set `org-cycle-max-level' not at all, | |
95 | or to a number smaller than this one. In fact, when `org-cycle-max-level' is | |
96 | not set, it will be assumed to be one less than the value of smaller than | |
97 | the value of this variable." | |
98 | :group 'org-inlinetask | |
afe98dfa CD |
99 | :type '(choice |
100 | (const :tag "Off" nil) | |
101 | (integer))) | |
c8d0cf5c | 102 | |
8bfe682a | 103 | (defcustom org-inlinetask-export t |
ed21c5c8 | 104 | "Non-nil means export inline tasks. |
8bfe682a CD |
105 | When nil, they will not be exported." |
106 | :group 'org-inlinetask | |
107 | :type 'boolean) | |
108 | ||
acedf35c | 109 | (defvar org-inlinetask-export-templates |
3ab2c837 | 110 | '((html "<pre class=\"inlinetask\"><b>%s%s</b><br />%s</pre>" |
acedf35c CD |
111 | '((unless (eq todo "") |
112 | (format "<span class=\"%s %s\">%s%s</span> " | |
113 | class todo todo priority)) | |
114 | heading content)) | |
3ab2c837 | 115 | (latex "\\begin\{description\}\n\\item[%s%s]~%s\\end\{description\}" |
acedf35c CD |
116 | '((unless (eq todo "") (format "\\textsc\{%s%s\} " todo priority)) |
117 | heading content)) | |
118 | (ascii " -- %s%s%s" | |
119 | '((unless (eq todo "") (format "%s%s " todo priority)) | |
120 | heading | |
121 | (unless (eq content "") | |
122 | (format "\n ¦ %s" | |
123 | (mapconcat 'identity (org-split-string content "\n") | |
124 | "\n ¦ "))))) | |
125 | (docbook "<variablelist> | |
126 | <varlistentry> | |
127 | <term>%s%s</term> | |
128 | <listitem><para>%s</para></listitem> | |
129 | </varlistentry> | |
130 | </variablelist>" | |
131 | '((unless (eq todo "") (format "%s%s " todo priority)) | |
132 | heading content))) | |
133 | "Templates for inline tasks in various exporters. | |
134 | ||
135 | This variable is an alist in the shape of (BACKEND STRING OBJECTS). | |
136 | ||
137 | BACKEND is the name of the backend for the template (ascii, html...). | |
138 | ||
139 | STRING is a format control string. | |
140 | ||
141 | OBJECTS is a list of elements to be substituted into the format | |
142 | string. They can be of any type, from a string to a form | |
143 | returning a value (thus allowing conditional insertion). A nil | |
144 | object will be substituted as the empty string. Obviously, there | |
145 | must be at least as many objects as %-sequences in the format | |
146 | string. | |
147 | ||
148 | Moreover, the following special keywords are provided: `todo', | |
149 | `priority', `heading', `content', `tags'. If some of them are not | |
150 | defined in an inline task, their value is the empty string. | |
151 | ||
152 | As an example, valid associations are: | |
153 | ||
154 | (html \"<ul><li>%s <p>%s</p></li></ul>\" (heading content)) | |
155 | ||
156 | or, with the additional package \"todonotes\" for LaTeX, | |
157 | ||
158 | (latex \"\\todo[inline]{\\textbf{\\textsf{%s %s}}\\linebreak{} %s}\" | |
159 | '((unless (eq todo \"\") | |
160 | (format \"\\textsc{%s%s}\" todo priority)) | |
161 | heading content)))") | |
162 | ||
c8d0cf5c CD |
163 | (defvar org-odd-levels-only) |
164 | (defvar org-keyword-time-regexp) | |
165 | (defvar org-drawer-regexp) | |
166 | (defvar org-complex-heading-regexp) | |
167 | (defvar org-property-end-re) | |
168 | ||
afe98dfa | 169 | (defcustom org-inlinetask-default-state nil |
86fbb8ca CD |
170 | "Non-nil means make inline tasks have a TODO keyword initially. |
171 | This should be the state `org-inlinetask-insert-task' should use by | |
172 | default, or nil of no state should be assigned." | |
173 | :group 'org-inlinetask | |
174 | :type '(choice | |
175 | (const :tag "No state" nil) | |
176 | (string :tag "Specific state"))) | |
177 | ||
178 | (defun org-inlinetask-insert-task (&optional no-state) | |
179 | "Insert an inline task. | |
afe98dfa | 180 | If prefix arg NO-STATE is set, ignore `org-inlinetask-default-state'." |
86fbb8ca | 181 | (interactive "P") |
c8d0cf5c | 182 | (or (bolp) (newline)) |
86fbb8ca CD |
183 | (let ((indent org-inlinetask-min-level)) |
184 | (if org-odd-levels-only | |
185 | (setq indent (- (* 2 indent) 1))) | |
186 | (insert (make-string indent ?*) | |
afe98dfa | 187 | (if (or no-state (not org-inlinetask-default-state)) |
86fbb8ca | 188 | " \n" |
afe98dfa | 189 | (concat " " org-inlinetask-default-state " \n")) |
86fbb8ca | 190 | (make-string indent ?*) " END\n")) |
c8d0cf5c CD |
191 | (end-of-line -1)) |
192 | (define-key org-mode-map "\C-c\C-xt" 'org-inlinetask-insert-task) | |
193 | ||
acedf35c CD |
194 | (defun org-inlinetask-outline-regexp () |
195 | "Return string matching an inline task heading. | |
196 | The number of levels is controlled by `org-inlinetask-min-level'." | |
197 | (let ((nstars (if org-odd-levels-only | |
198 | (1- (* org-inlinetask-min-level 2)) | |
199 | org-inlinetask-min-level))) | |
200 | (format "^\\(\\*\\{%d,\\}\\)[ \t]+" nstars))) | |
201 | ||
3ab2c837 BG |
202 | (defun org-inlinetask-at-task-p () |
203 | "Return true if point is at beginning of an inline task." | |
204 | (save-excursion | |
205 | (beginning-of-line) | |
206 | (and (looking-at (concat (org-inlinetask-outline-regexp) "\\(.*\\)")) | |
207 | (not (string-match "^end[ \t]*$" (downcase (match-string 2))))))) | |
208 | ||
afe98dfa CD |
209 | (defun org-inlinetask-in-task-p () |
210 | "Return true if point is inside an inline task." | |
211 | (save-excursion | |
3ab2c837 BG |
212 | (beginning-of-line) |
213 | (let* ((case-fold-search t) | |
214 | (stars-re (org-inlinetask-outline-regexp)) | |
afe98dfa | 215 | (task-beg-re (concat stars-re "\\(?:.*\\)")) |
3ab2c837 BG |
216 | (task-end-re (concat stars-re "END[ \t]*$"))) |
217 | (or (org-looking-at-p task-beg-re) | |
afe98dfa | 218 | (and (re-search-forward "^\\*+[ \t]+" nil t) |
3ab2c837 | 219 | (progn (beginning-of-line) (org-looking-at-p task-end-re))))))) |
afe98dfa | 220 | |
acedf35c CD |
221 | (defun org-inlinetask-goto-beginning () |
222 | "Go to the beginning of the inline task at point." | |
223 | (end-of-line) | |
3ab2c837 BG |
224 | (let ((case-fold-search t) |
225 | (inlinetask-re (org-inlinetask-outline-regexp))) | |
226 | (re-search-backward inlinetask-re nil t) | |
227 | (when (org-looking-at-p (concat inlinetask-re "END[ \t]*$")) | |
228 | (re-search-backward inlinetask-re nil t)))) | |
acedf35c CD |
229 | |
230 | (defun org-inlinetask-goto-end () | |
231 | "Go to the end of the inline task at point." | |
232 | (beginning-of-line) | |
3ab2c837 BG |
233 | (let ((case-fold-search t) |
234 | (inlinetask-re (org-inlinetask-outline-regexp))) | |
235 | (cond | |
236 | ((org-looking-at-p (concat inlinetask-re "END[ \t]*$")) | |
237 | (forward-line 1)) | |
238 | ((org-looking-at-p inlinetask-re) | |
239 | (forward-line 1) | |
240 | (when (org-inlinetask-in-task-p) | |
241 | (re-search-forward inlinetask-re nil t) | |
242 | (forward-line 1))) | |
243 | (t | |
244 | (re-search-forward inlinetask-re nil t) | |
245 | (forward-line 1))))) | |
acedf35c CD |
246 | |
247 | (defun org-inlinetask-get-task-level () | |
248 | "Get the level of the inline task around. | |
249 | This assumes the point is inside an inline task." | |
250 | (save-excursion | |
251 | (end-of-line) | |
252 | (re-search-backward (org-inlinetask-outline-regexp) nil t) | |
253 | (- (match-end 1) (match-beginning 1)))) | |
254 | ||
3ab2c837 BG |
255 | (defun org-inlinetask-promote () |
256 | "Promote the inline task at point. | |
257 | If the task has an end part, promote it. Also, prevents level from | |
258 | going below `org-inlinetask-min-level'." | |
259 | (interactive) | |
260 | (if (not (org-inlinetask-in-task-p)) | |
261 | (error "Not in an inline task") | |
262 | (save-excursion | |
263 | (let* ((lvl (org-inlinetask-get-task-level)) | |
264 | (next-lvl (org-get-valid-level lvl -1)) | |
265 | (diff (- next-lvl lvl)) | |
266 | (down-task (concat (make-string next-lvl ?*))) | |
267 | beg) | |
268 | (if (< next-lvl org-inlinetask-min-level) | |
269 | (error "Cannot promote an inline task at minimum level") | |
270 | (org-inlinetask-goto-beginning) | |
271 | (setq beg (point)) | |
272 | (replace-match down-task nil t nil 1) | |
273 | (org-inlinetask-goto-end) | |
274 | (if (eobp) (beginning-of-line) (forward-line -1)) | |
275 | (unless (= (point) beg) | |
276 | (replace-match down-task nil t nil 1) | |
277 | (when org-adapt-indentation | |
278 | (goto-char beg) | |
279 | (org-fixup-indentation diff)))))))) | |
280 | ||
281 | (defun org-inlinetask-demote () | |
282 | "Demote the inline task at point. | |
283 | If the task has an end part, also demote it." | |
284 | (interactive) | |
285 | (if (not (org-inlinetask-in-task-p)) | |
286 | (error "Not in an inline task") | |
287 | (save-excursion | |
288 | (let* ((lvl (org-inlinetask-get-task-level)) | |
289 | (next-lvl (org-get-valid-level lvl 1)) | |
290 | (diff (- next-lvl lvl)) | |
291 | (down-task (concat (make-string next-lvl ?*))) | |
292 | beg) | |
293 | (org-inlinetask-goto-beginning) | |
294 | (setq beg (point)) | |
295 | (replace-match down-task nil t nil 1) | |
296 | (org-inlinetask-goto-end) | |
297 | (if (eobp) (beginning-of-line) (forward-line -1)) | |
298 | (unless (= (point) beg) | |
299 | (replace-match down-task nil t nil 1) | |
300 | (when org-adapt-indentation | |
301 | (goto-char beg) | |
302 | (org-fixup-indentation diff))))))) | |
303 | ||
304 | (defvar org-export-current-backend) ; dynamically bound in org-exp.el | |
c8d0cf5c CD |
305 | (defun org-inlinetask-export-handler () |
306 | "Handle headlines with level larger or equal to `org-inlinetask-min-level'. | |
307 | Either remove headline and meta data, or do special formatting." | |
308 | (goto-char (point-min)) | |
3ab2c837 BG |
309 | (let* ((keywords-re (concat "^[ \t]*" org-keyword-time-regexp)) |
310 | (inline-re (concat (org-inlinetask-outline-regexp) ".*"))) | |
311 | (while (re-search-forward inline-re nil t) | |
312 | (let ((headline (match-string 0)) | |
313 | (beg (point-at-bol)) | |
314 | (end (copy-marker (save-excursion | |
315 | (org-inlinetask-goto-end) (point)))) | |
316 | content) | |
317 | ;; Delete SCHEDULED, DEADLINE... | |
318 | (while (re-search-forward keywords-re end t) | |
319 | (delete-region (point-at-bol) (1+ (point-at-eol)))) | |
c8d0cf5c | 320 | (goto-char beg) |
3ab2c837 BG |
321 | ;; Delete drawers |
322 | (while (re-search-forward org-drawer-regexp end t) | |
323 | (when (save-excursion (re-search-forward org-property-end-re nil t)) | |
324 | (delete-region beg (1+ (match-end 0))))) | |
325 | ;; Get CONTENT, if any. | |
326 | (goto-char beg) | |
327 | (forward-line 1) | |
328 | (unless (= (point) end) | |
329 | (setq content (buffer-substring (point) | |
330 | (save-excursion (goto-char end) | |
331 | (forward-line -1) | |
332 | (point))))) | |
333 | ;; Remove the task. | |
334 | (goto-char beg) | |
335 | (delete-region beg end) | |
8bfe682a | 336 | (when org-inlinetask-export |
3ab2c837 BG |
337 | ;; Format CONTENT, if appropriate. |
338 | (setq content | |
339 | (if (not (and content (string-match "\\S-" content))) | |
340 | "" | |
341 | ;; Ensure CONTENT has minimal indentation, a single | |
342 | ;; newline character at its boundaries, and isn't | |
343 | ;; protected. | |
344 | (when (string-match "`\\([ \t]*\n\\)+" content) | |
345 | (setq content (substring content (match-end 0)))) | |
346 | (when (string-match "[ \t\n]+\\'" content) | |
8bfe682a | 347 | (setq content (substring content 0 (match-beginning 0)))) |
3ab2c837 BG |
348 | (org-add-props (concat "\n" (org-remove-indentation content) "\n") |
349 | '(org-protected nil)))) | |
acedf35c | 350 | (when (string-match org-complex-heading-regexp headline) |
3ab2c837 BG |
351 | (let* ((nil-to-str |
352 | (function | |
353 | ;; Change nil arguments into empty strings. | |
354 | (lambda (el) (or (eval el) "")))) | |
355 | ;; Set up keywords provided to templates. | |
356 | (todo (or (match-string 2 headline) "")) | |
acedf35c CD |
357 | (class (or (and (eq "" todo) "") |
358 | (if (member todo org-done-keywords) "done" "todo"))) | |
359 | (priority (or (match-string 3 headline) "")) | |
360 | (heading (or (match-string 4 headline) "")) | |
361 | (tags (or (match-string 5 headline) "")) | |
3ab2c837 BG |
362 | ;; Read `org-inlinetask-export-templates'. |
363 | (backend-spec (assq org-export-current-backend | |
364 | org-inlinetask-export-templates)) | |
365 | (format-str (org-add-props (nth 1 backend-spec) | |
366 | '(org-protected t))) | |
acedf35c | 367 | (tokens (cadr (nth 2 backend-spec))) |
3ab2c837 BG |
368 | ;; Build export string. Ensure it won't break |
369 | ;; surrounding lists by giving it arbitrary high | |
370 | ;; indentation. | |
acedf35c CD |
371 | (export-str (org-add-props |
372 | (eval (append '(format format-str) | |
373 | (mapcar nil-to-str tokens))) | |
3ab2c837 BG |
374 | '(original-indentation 1000)))) |
375 | (insert export-str) | |
376 | (unless (bolp) (insert "\n"))))))))) | |
8bfe682a CD |
377 | |
378 | (defun org-inlinetask-get-current-indentation () | |
379 | "Get the indentation of the last non-while line above this one." | |
380 | (save-excursion | |
381 | (beginning-of-line 1) | |
382 | (skip-chars-backward " \t\n") | |
383 | (beginning-of-line 1) | |
384 | (or (org-at-item-p) | |
385 | (looking-at "[ \t]*")) | |
386 | (goto-char (match-end 0)) | |
387 | (current-column))) | |
c8d0cf5c CD |
388 | |
389 | (defun org-inlinetask-fontify (limit) | |
390 | "Fontify the inline tasks." | |
391 | (let* ((nstars (if org-odd-levels-only | |
392 | (1- (* 2 (or org-inlinetask-min-level 200))) | |
393 | (or org-inlinetask-min-level 200))) | |
394 | (re (concat "^\\(\\*\\)\\(\\*\\{" | |
395 | (format "%d" (- nstars 3)) | |
396 | ",\\}\\)\\(\\*\\* .*\\)"))) | |
397 | (while (re-search-forward re limit t) | |
398 | (add-text-properties (match-beginning 1) (match-end 1) | |
399 | '(face org-warning font-lock-fontified t)) | |
400 | (add-text-properties (match-beginning 2) (match-end 2) | |
401 | '(face org-hide font-lock-fontified t)) | |
402 | (add-text-properties (match-beginning 3) (match-end 3) | |
403 | '(face shadow font-lock-fontified t))))) | |
404 | ||
3ab2c837 BG |
405 | (defun org-inlinetask-toggle-visibility () |
406 | "Toggle visibility of inline task at point." | |
407 | (let ((end (save-excursion | |
408 | (org-inlinetask-goto-end) | |
409 | (if (bolp) (1- (point)) (point)))) | |
410 | (start (save-excursion | |
411 | (org-inlinetask-goto-beginning) | |
412 | (point-at-eol)))) | |
413 | (cond | |
414 | ;; Nothing to show/hide. | |
415 | ((= end start)) | |
416 | ;; Inlinetask was folded: expand it. | |
417 | ((get-char-property (1+ start) 'invisible) | |
418 | (outline-flag-region start end nil)) | |
419 | (t (outline-flag-region start end t))))) | |
420 | ||
c8d0cf5c CD |
421 | (defun org-inlinetask-remove-END-maybe () |
422 | "Remove an END line when present." | |
423 | (when (looking-at (format "\\([ \t]*\n\\)*\\*\\{%d,\\}[ \t]+END[ \t]*$" | |
424 | org-inlinetask-min-level)) | |
425 | (replace-match ""))) | |
426 | ||
427 | (eval-after-load "org-exp" | |
3ab2c837 | 428 | '(add-hook 'org-export-preprocess-before-backend-specifics-hook |
c8d0cf5c CD |
429 | 'org-inlinetask-export-handler)) |
430 | (eval-after-load "org" | |
431 | '(add-hook 'org-font-lock-hook 'org-inlinetask-fontify)) | |
432 | ||
433 | (provide 'org-inlinetask) | |
434 | ||
435 | ;;; org-inlinetask.el ends here |