(defpackage :footnotes
(:use :common-lisp :cl-ppcre)
(:export #:reorder-footnotes-by-order-of-appearence
#:reorder-footnotes-by-order-listing-order)
(:documentation "A simple tool for re-ordering footnotes in a text file."))
(in-package :footnotes)
(defun replace-with-reordered-notes (line notes-mapping)
"Given a line of text, which may or may not contain footnote markers, replace any markers with the
numbers specified in NOTES-MAPPING.
LINE is simply a string. NOTES-MAPPING is an alist mapping the original marker, a string like
\"[X]\", to its replacement \"[Y]\"."
(regex-replace-all
"\\[\\d+\\]"
line
(lambda (target-string start end match-start match-end reg-starts reg-ends)
(declare (ignore start end reg-starts reg-ends))
(let ((replacement (second (find (subseq target-string match-start match-end) notes-mapping :test #'string= :key #'first))))
(unless replacement
(setf replacement "[X]")
(cerror "Use \"[X]\" instead."
"Unable to find replacement for ~S in ~S." (subseq target-string match-start match-end) notes-mapping))
replacement))))
(defun collect-notes-in-line (line notes-mapping)
"Adds to NOTES-MAPPING any markers found in the string LINE.
The notes in LINE are mapped to new notes according to their order of appearence in LINE."
(do-matches-as-strings (footnote-marker "\\[(\\d+)\\]" line)
(pushnew (list footnote-marker (format nil "[~D]" (1+ (length notes-mapping))))
notes-mapping
:key #'first :test #'string=))
notes-mapping)
(defun reorder-footnotes-by-order-of-appearence (filename)
"Rewrites the footnotes in FILENAME based on their order of appearence in the original text."
(with-open-file (input filename :direction :input :if-does-not-exist :error)
(let ((notes-mapping '()))
(loop
for line = (read-line input nil input nil)
until (eq line input)
do (setf notes-mapping (collect-notes-in-line line notes-mapping))
do (write-line (replace-with-reordered-notes line notes-mapping)
*standard-output*)
when (string= "@footnote:" line) do (return))
(loop
for line = (read-line input nil input nil)
until (eq line input)
do (write-line (replace-with-reordered-notes line notes-mapping)
*standard-output*)))))
(defun reorder-footnotes-by-order-listing-order (filename)
"Rewrites the footnotes in FILENAME based on their order of appearence in the @foonote: section.
In order to avoid keeping the whole file in memory we perform two passes over FILENAME. The first
pass collects the notes in the @footnote: section and computes their replacements, the second pass
rewrites the text, outputing it to *standard-output*."
(let ((notes-mapping '()))
(with-open-file (input filename :direction :input)
(loop
for line = (read-line input nil input nil)
until (eq line input)
when (string= "@footnote:" line)
do (return))
(loop
for line = (read-line input nil input nil)
until (eq line input)
do (setf notes-mapping (collect-notes-in-line line notes-mapping))))
(with-open-file (input filename :direction :input)
(loop
for line = (read-line input nil input nil)
until (eq line input)
do (write-line (replace-with-reordered-notes line notes-mapping)
*standard-output*)))))