First checked in version
[project-aon.git] / scripts / aon.el
1 ;;; aon.el --- utilities for implementing Project Aon errata
2
3 ;; Copyright (C) 2003, 2004, 2005 Thomas Wolmer & Project Aon
4
5 ;; Author: Thomas Wolmer <thomas@powerpuff.org>
6 ;; Created: 17 Aug 2003
7 ;; Version: 0.12
8 ;; Keywords: aon 
9
10 ;;; Commentary:
11
12 ;; This code provides support for implementing errata in the XML files
13 ;; Project Aon uses as "single sources" for the books it publishes. For
14 ;; more information on Project Aon, see http://www.projectaon.org/.
15
16 ;;; Change Log:
17
18 ;; 2003-08-17: 0.01: First nicely formatted version.
19 ;; 2003-08-24: 0.02: Fixed bugs in aon-find-errata-entry and aon-fix-markup,
20 ;;                   implemented aon-errata-jump and support functions.
21 ;; 2003-08-31: 0.03: Added default section id and title, and modified
22 ;;                   aon-get-sect-id and aon-get-sect-title, so that errata
23 ;;                   in the blurb gets listed correctly. Fixed some major
24 ;;                   problems in aon-errata-replace-all.
25 ;; 2003-09-07: 0.04: Improved usability: aon-errata-replace[-all] presents
26 ;;                   better prompt and a default text, and checks for "same"
27 ;;                   and null replacements. aon-errata-add also checks null
28 ;;                   input. All four user functions take an optional comment.
29 ;; 2003-09-14: 0.05: Now saves all input to the interactive functions to avoid
30 ;;                   losing them if the user changes the selection/point while
31 ;;                   answering the interactive questions. Fixed bug in
32 ;;                   aon-errata-add which asked you for a comment twice.
33 ;;                   Extended aon-fix-markup.
34 ;; 2003-10-12: 0.06: XEmacs does not put mouse-selected region in kill ring by
35 ;;                   default and does not have match-string-no-properties.
36 ;;                   Oops. Now things work in XEmacs, but regressions tests
37 ;;                   with GNU Emacs should probably be done too.
38 ;; 2003-10-31: 0.07: Used defconst to define some of the constants.
39 ;; 2003-11-12: 0.08: Flesh out the aon-nonindexed-sects definition
40 ;; 2004-05-09: 0.09: Fixed too greedy anchor tag regexp in aon-fix-markup &
41 ;;                   added more 04wotw sections to aon-nonindexed-sects
42 ;; 2005-02-05: 0.10: Added a function for inserting large illustrations
43 ;; 2005-05-06: 0.11: Added a function for inserting inline illustrations
44 ;; 2005-05-23: 0.12: Fixed bug in aon-re-get-errata-entry-id.
45
46 ;; A slight limitation: We can only handle one illustrator at a time.
47
48 (defvar aon-illustrator-name "Paul Bonner"
49   "*")
50 (defvar aon-large-illustration-width "386"
51   "*")
52
53
54 ;;; Code:
55
56 (defconst aon-nonindexed-sects
57   (list
58    ;; kai disciplines
59    "camflage" "hunting" "sixthsns" "tracking" "healing" "wepnskll" "wepnskll"
60    "mndblst" "anmlknsp" "mindomtr"
61    ;; magnakai disciplines
62    "wpnmstry" "anmlctrl" "curing" "invsblty" "hntmstry" "pthmnshp" "psisurge"
63    "psiscrn" "nexus" "dvnation" "lcbonus"
64    ;; magnakai improved disciplines
65    "primate" "tutelary" "prncpln" "mentora" "scion" "archmstr"
66    ;; grandmaster disciplines
67    "mksumary" "wpnmstry" "anmlmstr" "deliver" "assimila" "hntmstry" "pthmnshp"
68    "kaisurge" "kaiscrn" "nexus" "gnosis" "magi" "alchemy"
69    ;; grandmaster improved disciplines
70    "guardian" "sunkght"
71    ;; gs lesser magicks
72    "lessmcks" "sorcery" "enchant" "elementl" "alchemy" "prophecy" "psycmncy"
73    "evcation" "staff"
74    ;; gs higher magicks
75    "highmcks" "thamtrgy" "telergy" "physirgy" "theurgy" "visionry" "necrmncy"
76    ;; general
77    "toc" "credits" "howcarry" "howmuch" "howuse" "evasion" "smevazn" "errintro"
78    "errerr" "primill" "secill")
79   "These sections are ignored for errata entries; the enclosing section is
80 used instead (unless it is also 'nonindexed').")
81
82 (defconst aon-default-sect-id "title"
83   "The default section to assign an erratum to if none is found.")
84 (defconst aon-default-sect-title "Title Page"
85   "The title of the 'default section' (see `aon-default-sect-id').")
86
87 ;; Commonly used search regexps
88 (defconst aon-re-errerr-sect "<section.*id=\"errerr\".*>"
89   "Regexp used to locate the start of the errata entry list.")
90 (defconst aon-re-get-sect-id "<section.*id=\"\\(.+?\\)\".*>"
91   "Regexp used to find the id of asection.")
92 (defconst aon-re-get-errata-entry-id "<p>(<a idref=\"\\(.+?\\)\".*>)"
93   "Regexp used to find the id of an errata list entry.")
94 (defconst aon-re-get-title "<title>\\(.+?\\)</title>"
95   "Regexp used to find the title of a section.")
96
97 ;; The errata item texts (as templates to be fed to 'format')
98 ;; These are only (hardcoded) reader-visible texts inserted by this hack!
99 ;; TODO: rename these variabled. defconst?
100 (setq replaceditemtext " Replaced <quote>%s</quote> with <quote>%s</quote>%s.")
101 (setq addeditemtext " Added <quote>%s</quote>%s.")
102 (setq deleteditemtext " Deleted <quote>%s</quote>%s.")
103 (setq replacedallitemtext (concat " Replaced all occurrences of <quote>%s"
104                                   "</quote> with <quote>%s</quote>%s."))
105
106 ;; TODO: an optional second argument should be where the search starts,
107 ;; instead of at the beginning of the file
108 (defun aon-get-sect-pos (sect)
109   "Returns the start position of the named section.
110 Useful for comparing the order of sections."
111   (save-excursion
112     (goto-char (point-min))
113     (re-search-forward (format "<section.*id=\"%s\".*>" sect) nil t)
114     (match-beginning 0)))
115
116 ;; TODO: rewrite this property fetching stuff, it is way too inefficient
117
118 (defun aon-get-sect-property (regexp)
119   "Returns a property, as selected by regexp, of the current indexed section."
120   (save-excursion
121     ;; if we're in an ignored section, go to its beginning and repeat search
122     (if (member (aon-get-unchecked-sect-id) aon-nonindexed-sects)
123         (progn
124           (search-backward "<section" nil t)
125           (aon-get-sect-property regexp))
126       (aon-get-unchecked-sect-property regexp))))
127
128 (defun aon-get-unchecked-sect-property (regexp)
129   "Returns a property, as selected by regexp, of the current section.
130 Unlike aon-get-sect-property, it does not checked if it is indexed."
131   (save-excursion
132     (and (re-search-backward regexp nil t)
133          ;;(match-string-no-properties 1)))) ; Doesn't work in XEmacs
134          (match-string 1))))
135
136 (defun aon-get-unchecked-sect-id ()
137   "Returns the id of the current section.
138 Unlike aon-get-sect-id, it does not check if it is indexed."
139   (or (aon-get-unchecked-sect-property aon-re-get-sect-id)
140       aon-default-sect-id)) ; workaround for the blurb and such
141
142 (defun aon-get-sect-id ()
143   "Returns the id of the current indexed section."
144   (or (aon-get-sect-property aon-re-get-sect-id)
145       aon-default-sect-id)) ; workaround for the blurb and such
146
147 (defun aon-get-sect-title ()
148   "Returns the title of the current indexed section."
149   (if (string= (aon-get-sect-id) "title")
150       aon-default-sect-title ; workaround for the blurb and such
151     (aon-get-sect-property aon-re-get-title)))
152
153 (defun aon-find-errata-entry ()
154   "Finds the insertion position in the errata entry for the current section.
155 If no errata entry exists, returns nil."
156   (save-excursion
157     (let ((id (aon-get-sect-id)))
158       (re-search-forward aon-re-errerr-sect)
159       ;; locate the end of the errata list so that we don't search too far and
160       ;; start finding footnotes instead
161       (when (search-forward (format "<p>(<a idref=\"%s\">" id)
162                             (save-excursion (search-forward "</section>"))
163                             t)
164         (search-forward "</p>") ; place position last in entry
165         (match-beginning 0)))))
166
167 (defun aon-create-errata-entry ()
168   "Creates an errata entry for the current section.
169 Returns the errata item insertion point in the new entry."
170   (interactive) ;; for testing only
171   (setq newerrataentry (format "<p>(<a idref=\"%s\">%s</a>)</p>\n"
172                                (aon-get-sect-id) (aon-get-sect-title)))
173   (save-excursion
174     (aon-goto-new-errata-list-entry-pos (point))
175     (insert newerrataentry)
176     (search-backward "</p>")
177     (indent-according-to-mode)
178     (point))) ; goto insertion point and make it be returned
179
180 ;; This function is HORRIBLY slow due to the calls to aon-get-sect-pos
181 (defun aon-goto-new-errata-list-entry-pos (sectpos)
182   "Places the point at the position where a new errata entry shall be created."
183   (re-search-forward aon-re-errerr-sect)
184   (search-forward "<data>")
185   (setq endoferrata (save-excursion
186                       (if (search-forward "</data>")
187                           (point)
188                         (error "Could not find the end of the errata list!"))))
189   (while (let ((id (save-excursion
190                      (if (re-search-forward aon-re-get-errata-entry-id
191                                             endoferrata
192                                             t)
193                          ;;(match-string-no-properties 1) ; Boohoo XEmacs
194                          (match-string 1)
195                        "footnotz" ; hack warning! to avoid getting too far
196                        ))))
197            (unless (setq thissectpos (aon-get-sect-pos id))
198              (error "Section %s has an errata entry but does not exist!" id))
199            (< thissectpos sectpos))
200     (forward-line 1)))
201
202 ;; TODO: Figure out some way not to create an errata entry before we know that
203 ;; it will work OK. As it is now, an empty errata entry may be left behind
204 ;; if an error occurs. But maybe that's not a problem??
205 (defun aon-insert-errata-entry (errataentry id)
206   "Adds an errata item to the current indexed section's errata entry.
207 An errata entry is created if it does not exist."
208   (save-excursion
209     (goto-char (or (aon-find-errata-entry) ; if it is not found...
210                    (aon-create-errata-entry))) ; ...create the entry
211     (insert (aon-format-errata-item id errataentry))))
212
213 (defun aon-format-errata-item (id errataentry)
214   ""
215   (format "<!--%s-ITEM-->%s<!--/%s-ITEM-->" id errataentry id))
216
217 (defun aon-format-erratum (id newtext)
218   ""
219   (format "<!--%s-->%s<!--/%s-->" id newtext id))
220
221 (defun aon-format-erratum-empty (id)
222   ""
223   (format "<!--%s/-->" id))
224
225 (defun aon-errata-replace (beg end newtext oldtext &optional comment)
226   "Replaces the current region and records the replacement."
227   (interactive
228    ;; save all positions and string as the user may change the selection!
229    (let* ((xbeg (region-beginning))
230           (xend (region-end))
231           ;;(xoldtext (car kill-ring-yank-pointer)) ; Boohoo Xemacs
232           (xoldtext (buffer-substring-no-properties xbeg xend))
233           (xnewtext (read-string (format "Replace '%s' with: " xoldtext)
234                                  xoldtext))
235           (cmnt (read-string "Additional comment (optional): ")))
236      (list xbeg xend xnewtext xoldtext cmnt)))
237   (cond ((string= newtext "")
238          (error "No replacement! To delete text, use 'aon-errata-delete'."))
239         ((string= newtext oldtext)
240          (error "The replacement is the same as the original!")))
241   (let* ((id (aon-get-new-errata-id "RE"))
242          (errataentry (format replaceditemtext
243                               (aon-fix-markup oldtext)
244                               (aon-fix-markup newtext)
245                               comment)))
246     (kill-region beg end)
247     (insert (aon-format-erratum id newtext))
248     (aon-insert-errata-entry errataentry id)))
249
250 (defun aon-errata-delete (beg end oldtext &optional comment)
251   "Deletes the current region and records the deletion."
252   (interactive
253    ;; save all positions and string as the user may change the selection!
254    (let* ((xbeg (region-beginning))
255           (xend (region-end))
256           ;;(xoldtext (car kill-ring-yank-pointer)) ; Boohoo Xemacs
257           (xoldtext (buffer-substring-no-properties xbeg xend))
258           (cmnt (read-string "Additional comment (optional): ")))
259      (list xbeg xend xoldtext cmnt)))
260   (let* ((id (aon-get-new-errata-id "DE"))
261          (errataentry (format deleteditemtext
262                               (aon-fix-markup oldtext)
263                               comment)))
264     (kill-region beg end)
265     (insert (aon-format-erratum-empty id))
266     (aon-insert-errata-entry errataentry id)))
267
268 (defun aon-errata-add (pos newtext &optional comment)
269   "Adds text in the current position and records the addition."
270   (interactive
271    (let ((xpos (point))
272          (string (read-string "Insert text: "))
273          (cmnt (read-string "Additional comment (optional): ")))
274      (list xpos string cmnt)))
275   (if (string= newtext "")
276       (error "No text to add!"))
277   (let* ((id (aon-get-new-errata-id "AD"))
278          (errataentry (format addeditemtext
279                               (aon-fix-markup newtext)
280                               comment)))
281     (insert (aon-format-erratum id newtext))
282     (aon-insert-errata-entry errataentry id)))
283
284 (defun aon-errata-replace-all (beg end newtext oldtext &optional comment)
285   "Replaces all occurrences of the current region and records the replacements.
286 Stretches all over one indexed section, so it might affect text before the
287 selected region as well! It is NOT very intelligent about abstaining from
288 replacing text in markup that should not be touch, so don't even think about
289 replacing, say, 'class'."
290   (interactive
291    ;; save all positions and string as the user may change the selection!
292    (let* ((xbeg (region-beginning))
293           (xend (region-end))
294           ;;(xoldtext (car kill-ring-yank-pointer)) ; Boohoo Xemacs
295           (xoldtext (buffer-substring-no-properties xbeg xend))
296           (xnewtext (read-string (format
297                                   "Replace all occurrences of '%s' with: "
298                                   xoldtext)
299                                  xoldtext))
300           (cmnt (read-string "Additional comment (optional): ")))
301      (list xbeg xend xnewtext xoldtext cmnt)))
302   (if (string= newtext oldtext)
303       (error "The replacement text is the same as the original!"))
304   (let* ((id (aon-get-new-errata-id "RA"))
305          (errataentry (format replacedallitemtext
306                               (aon-fix-markup oldtext)
307                               (aon-fix-markup newtext)
308                               comment))
309          (saved-case-fold-search case-fold-search))
310     (save-excursion
311       (aon-goto-sect-start)
312       (forward-line 1) ; workaround to avoid matching this section start
313       (setq sectend (aon-get-sect-end))
314       (setq case-fold-search nil) ; No case folded false matches thank you
315       (while (re-search-forward (format "[^\"]\\\(%s\\\)[^\"]" oldtext) sectend t)
316         (replace-match (aon-format-erratum id newtext) t t nil 1))
317       (setq case-fold-search saved-case-fold-search)) ; Reset case folding
318     (aon-insert-errata-entry errataentry id)))
319
320 ;; SLOW!!! And doesn't need to be recursive. Rewrite.
321 (defun aon-goto-sect-start ()
322   "Places the point at the start of the current indexed section."
323   (interactive) ; for test purposes
324   (if (member (aon-get-unchecked-sect-id) aon-nonindexed-sects)
325         (progn
326           (search-backward "<section" nil t)
327           (aon-goto-sect-start)) 
328     (search-backward "<section" nil t)))
329
330 (defun aon-get-sect-end ()
331   "Returns the end position of this section."
332   (let ((nextopensect (save-excursion
333                         (search-forward "<section")))
334         (nextclosesect (save-excursion
335                          (search-forward "</section>"))))
336     (if (< nextclosesect nextopensect)
337         nextclosesect ; return the end of this section
338       (save-excursion
339         (goto-char nextopensect) ; place inside nested section
340         (goto-char (aon-get-sect-end)) ; goto end of that section
341         (aon-get-sect-end))))) ; continue search for this section's end
342
343 (defun aon-get-new-errata-id (type)
344   "Returns a new unique errata item id."
345   (let ((id (format "ERRTAG-%s-%s" type (point))))
346     (save-excursion
347       (while (search-forward (format "%s-" id) nil t)
348         (setq id (format "%s1" id)))) ; append '1' and try again
349     id))
350
351 (defun aon-fix-markup (string)
352   "Converts some markup in text from section to fit in an errata item."
353   ;; Quotes - assume it is single quotes, double must be handled manually
354   (while (string-match "<quote>" string)
355     (setq string (replace-match "&lsquot;" nil nil string)))
356   (while (string-match "</quote>" string)
357     (setq string (replace-match "&rsquot;" nil nil string)))
358   ;; TODO: Except for tags that shall become entities, maybe the rest can be
359   ;; handled by a general transformation?
360   ;; Link texts
361   (while (string-match "<link-text>" string)
362     (setq string (replace-match "<!--link-text-->" nil nil string)))
363   (while (string-match "</link-text>" string)
364     (setq string (replace-match "<!--/link-text-->" nil nil string)))
365   ;; idrefs and similar
366   ;; TODO: a complete idref does not need to be replaced!
367   (while (string-match "<\\(a .*?\\)>" string)
368     (setq string (replace-match (format "<!--%s-->" (match-string 1 string))
369                                 nil nil string)))
370   (while (string-match "</a>" string)
371     (setq string (replace-match "<!--/a-->" nil nil string)))  string)
372
373 ;; (defun aon-refix-markup (string)
374 ;;   "Converts some 'fixed' markup in an errata item text to fit in a section.
375 ;; To be used when an erratum is undone and the replaced or deleted text from
376 ;; an errata item shall be re-inserted in the section."
377 ;;   (while (or (string-match "&lsquot;" string)
378 ;;              (string-match "&ldquot;" string))
379 ;;     (setq string (replace-match "<quote>" nil nil string)))
380 ;;   (while (or (string-match "&rsquot;" string)
381 ;;              (string-match "&rdquot;" string))
382 ;;     (setq string (replace-match "</quote>" nil nil string)))
383 ;;   string)
384
385 (defun aon-locate-errata-item-start ()
386   "Return the starting position of the errata block the point is in.
387 Currently, the point may not be within a comment start or end tag.
388 Returns an error message if the point is not within an errata tag or if
389 the errata structure is corrupt."
390   (save-excursion
391     (let ((prevcommentend (save-excursion (search-backward "-->" nil t)))
392           (prevcommentstart (save-excursion (search-backward "<!--" nil t)))
393           (nextcommentend (save-excursion (search-forward "-->" nil t))))
394       (unless (and prevcommentstart nextcommentend)
395         (error "Point is not within an errata item!"))
396       (goto-char prevcommentstart)
397       (cond
398        ;; if we are at the start of an opening tag, it is OK
399        ((looking-at "<!--ERRTAG-..-[0-9]+\\(-ITEM\\)?-->")
400         (point))
401        ;; if we are at the start of a content-free tag, we must check that
402        ;; we were not outside that tag when we started.
403        ((looking-at "<!--ERRTAG-..-[0-9]+/-->")
404         (if (> prevcommentend prevcommentstart)
405             (error "Point is not within an errata item!")
406           (point)))
407        ;; if we are at the start of a closing tag, we must check that we were
408        ;; not outside that tag when we started, and then find the opening tag
409        ((looking-at "<!--/\\(ERRTAG-..-[0-9]+\\(-ITEM\\)?\\)-->")
410         (if (> prevcommentend prevcommentstart)
411             (error "Point is not within an errata item!")
412           (search-backward (format "<!--%s-->" (match-string 1)))))
413        ;; if the comment was not an errata tag
414        (t (error "Point is not within an errata item!"))))))
415
416 (defun aon-errata-jump ()
417   "If the point is within tags, jump to the corresponding erratum/errata item.
418 In the case of a \"replace all\" erratum, jumps to the first location."
419   (interactive)
420   (let ((pos
421          (save-excursion
422            (goto-char (aon-locate-errata-item-start)) ; inefficient!
423            (cond ((looking-at "<!--\\(ERRTAG-..-[0-9]+\\)-ITEM-->")
424                   (goto-char (point-min))
425                   (re-search-forward (format "<!--%s/?-->" (match-string 1)))
426                   (match-beginning 0))
427                  ((looking-at "<!--\\(ERRTAG-..-[0-9]+\\)/?-->")
428                   (goto-char (point-min))
429                   (search-forward (format "<!--%s-ITEM-->" (match-string 1)))
430                   (match-beginning 0))
431                  (t (error "This code can not be reached!"))))))
432     (if pos (goto-char pos)
433       (error "This code can not be reached!")))) ; should have received error
434
435 ;; TODO: Make the illustration functions add entries in the illustrations list
436
437 (defun aon-illustration-large (number height caption)
438   "Adds a large illustration at the insertion point.
439 The illustrations list is not updated (TBD)."
440   (interactive
441    (let ((xnum (read-string "Illustration number: "))
442          (xheight (read-string "Pixel height: "))
443          (xcaption (read-string "Caption: ")))
444      (list xnum xheight xcaption)))
445   (let ((startpos (point))
446         (endpos (save-excursion
447                   (insert (format "<illustration class=\"float\">\n        <meta>\n         <creator>%s</creator>\n         <description>%s</description>\n        </meta>\n        <instance class=\"html\" src=\"ill%s.gif\" width=\"%s\" height=\"%s\"/>\n        <instance class=\"pdf\" src=\"ill%s.pdf\" width=\"%s\" height=\"%s\"/>\n       </illustration>"
448                                   aon-illustrator-name caption number
449                                   aon-large-illustration-width height number
450                                   aon-large-illustration-width height))
451                   (point))))
452     (aon-indent-block startpos endpos)))
453
454 (defun aon-illustration-inline (filename height)
455   "Adds an inline illustration at the insertion point.
456 The illustrations list is not updated (TBD)."
457   (interactive
458    (let ((xfilename (read-string "File name: "))
459          (xheight (read-string "Pixel height: ")))
460      (list xfilename xheight)))
461   (let ((startpos (point))
462         (endpos (save-excursion
463                   (insert (format "<illustration class=\"inline\">\n<meta>\n<creator>%s</creator>\n</meta>\n<instance class=\"html\" src=\"%s.gif\" width=\"%s\" height=\"%s\" mime-type=\"image/gif\" />\n</illustration>"
464                                   aon-illustrator-name filename
465                                   aon-large-illustration-width height))
466                   (point))))
467     (aon-indent-block startpos endpos)))
468
469 (defun aon-indent-block (startpos endpos)
470   "Indents the text between startpos and endpos.
471 The positions need not be start or end of lines. Leaves point at end of block."
472   (dotimes (i (count-lines startpos endpos))
473     (indent-according-to-mode)
474     (forward-line 1)))
475
476 (global-set-key "\C-cr" 'aon-errata-replace)
477 (global-set-key "\C-cd" 'aon-errata-delete)
478 (global-set-key "\C-ca" 'aon-errata-add)
479 (global-set-key "\C-cR" 'aon-errata-replace-all)
480 (global-set-key "\C-cj" 'aon-errata-jump)
481 ;;(global-set-key "\C-cu" 'aon-errata-revert)
482
483 (global-set-key "\C-cl" 'aon-illustration-large)
484 (global-set-key "\C-ci" 'aon-illustration-inline)
485
486
487
488 ;; Errata examples
489 ;; <!--ERRTAG-RE-123--> <!--/ERRTAG-RE-123-->
490 ;; <!--ERRTAG-DE-456/-->
491 ;; Errata item
492 ;; <!--ERRTAG-RE-123-ITEM--> <!--/ERRTAG-RE-123-ITEM-->
493 ;; RE - replace
494 ;; DE - delete
495 ;; AD - add
496 ;; RA - replace all
497
498 ;;; aon.el ends here