;;; Commentary: ;; This extracts the code examples from the current buffer, which ;; should be learning_gnu_c.texinfo, and writes them in the examples ;; subdirectory of the lgc distribution (which you should create ;; first), along with a Makefile with which to build them all. ;; Some limitations: the makefile assumes each file is a standalone ;; program. Give me a consistent method to tell how to parse and ;; combine them, and I will. On the other hand, if you only plan to ;; use it a few times, it may be easier just to special-case those ;; files. ;; The bit that removes Texinfo markup from example sources only ;; changes @{ and @} to what they should be. ;; Duplicate filenames are silently ignored, so you get make warnings ;; about duplicate targets. This is good, because otherwise the last ;; file just gets written, and all the others disappear. ;; On the plus side, it is smart enough to not try to compile .h ;; files. But it does not compute .h dependencies. ;;; Code: ;;; MAGIC (defconst lgc-c-section-regexp "^@unnumberedsubsubsec[[:blank:]]+\\([[:graph:]]+?\\.[ch]\\)" "Regexp for what comes before each complete C example. This should not include the @smallexample and @group, if present. The snarfer accounts for that separately. The first match group MUST be the file name to which the example shall be written.") (defconst lgc-example-begin-regexp "^@\\(?:small\\)?example\n+\\(?:@group\n+\\)?" "Regexp for beginning of an example.") (defconst lgc-example-end-regexp "^\\(?:@end group\n+\\)?@end \\(?:small\\)?example") (defconst lgc-c-output-dir "examples" "Directory in which to place example C files and Makefile for them. May be either absolute, or relative to the source Texinfo file.") (defconst lgc-c-makeable-regexp "\.c$" "Regexp to recognize whether or not to make an executable.") ;;; Don't forget to update the other two list members if you change ;;; the template text! (defconst lgc-c-makefile-template '("## Makefile for examples in `Learning GNU C' ## Generated by lgc-examples.el from the Texinfo source # The C compiler to use. CC=gcc # Extra flags to pass to the C compiler. CFLAGS=-pipe -Wall # Files deleted by make clean, and made with all. OUTPUTS= # Default make target. all: $(OUTPUTS) clean: \x9-rm -f $(OUTPUTS) .PHONY: all clean # Makefile ends here\n" 10 15) "List specifying a template for the examples Makefile. `lgc-init' and `lgc-snarf-examples' use this to set up a Makefile for `lgc-add-makefile-entry'. The elements of the list are as follows: * A string to insert. * Line number at the end of which to insert output filenames, separated by spaces, for the clean and all targets. * Line number at the beginning of which to start inserting make targets on which the all rule depends. Line numbers are one-indexed, as it is with Emacs.") ;;; Keep compile from complaining; normally, this is only set ;;; dynamically by anyone who wants to use lgc-add-makefile-entry. (eval-when-compile (defconst lgc-lame-init nil)) ;;; COMMANDS (defun lgc-snarf-examples (&optional beg end) "Create example files and Makefile from Texinfo in current buffer." (interactive) (unless beg (setq beg (point-min))) (unless end (setq end (point-max))) (save-excursion (message "Created %d example files" (let ((lgc-lame-init (lgc-init-makefile))) (lgc-apply-examples beg end 'lgc-create-example-file 'lgc-add-makefile-entry 'lgc-nullify-txibeg))))) (defun lgc-test-example-parse (debugbuf) "Debug the callback arguments from `lgc-apply-examples'. DEBUGBUF, interactively prompted, is the buffer in which to write." (interactive "BParsing forms output buffer: ") (save-excursion (set-buffer debugbuf) (erase-buffer)) (message "Parsed %d examples" (lgc-apply-examples (point-min) (point-max) #'(lambda (fname fbody txibeg) (save-excursion (set-buffer debugbuf) (insert "fname: ") (print fname (current-buffer)) (insert "\nfbody: ") (print fbody (current-buffer)) (insert "\ntxibeg: ") (print txibeg (current-buffer)) (insert ?\n))) ))) ;;; PARSING DRIVER (defun lgc-apply-examples (beg end &rest funcs) "With all parsed and reformatted examples in region, call FUNCS. Each function in FUNCS receives arguments as follows: \(fname fbody txibeg) where FNAME is the file name as interpreted from the Texinfo section name, FBODY is the example body, TXIFILE is the name of the Texinfo file, and TXIBEG is a marker for the beginning of FBODY in the Texinfo file, suitable for computing the position and filename. Return the number of examples processed." (goto-char beg) (let ((example-count 0)) ;; loop until any search-failed (condition-case nil (while t (let* ((fname (progn (re-search-forward lgc-c-section-regexp end) (match-string 1))) ;; find the region, then modify in temp buffer (fmarkers (lgc-wrap-next-example end)) (fbody (lgc-region-to-untexified-string (car fmarkers) (cdr fmarkers)))) ;; don't need end marker anymore (set-marker (cdr fmarkers) nil) (dolist (func funcs) (funcall func fname fbody (car fmarkers)))) (setq example-count (1+ example-count))) (search-failed example-count)))) ;;; Currently, both markers have nil-type motion. You may want to ;;; change this. (defun lgc-wrap-next-example (end) "Return a pair of markers wrapping the next Texinfo example. The CAR is the beginning, and the CDR is the end. The text is wrapped, not including the @example tags et al." (re-search-forward lgc-example-begin-regexp end) (let ((begin-mark (point-marker))) (re-search-forward lgc-example-end-regexp end) (cons begin-mark (set-marker (make-marker) (match-beginning 0))))) ;;; This doesn't remove *all* Texinfo formatting that may be in the ;;; text. It only changes @{ to { and @} to }, currently. If you use ;;; more Texinfo in such regions, add relevant transforms to this ;;; function. (defun lgc-region-to-untexified-string (beg end) "Remove Texinfo formatting from region without modifying region, and return a string containing the new text." (let ((oriregion (current-buffer))) (with-temp-buffer (insert-buffer-substring oriregion beg end) ;; do transform until search crash (condition-case nil (while t (re-search-backward "@[{}]") (delete-char 1)) (search-failed nil)) (buffer-string)))) ;;; OUTPUT PROCESSING (defun lgc-create-example-file (fname fbody txibeg) "Create file FNAME in `lgc-c-output-dir' with contents FBODY." (save-excursion (find-file (expand-file-name fname (expand-file-name lgc-c-output-dir))) (erase-buffer) ;; If you want error reporting to refer to the Texinfo source ;; rather than the generated C file, extract that information here ;; using TXIBEG. (insert fbody))) (defun lgc-add-makefile-entry (fname fbody txibeg) "Add a Makefile entry for FNAME. Note that lgc-lame-init must be defined in the dynamic context of every call to this function. For the format of lgc-lame-init, see `lgc-init-makefile'." (when (string-match lgc-c-makeable-regexp fname) (save-excursion (set-buffer (car lgc-lame-init)) (goto-char (cadr lgc-lame-init)) (let ((ofile (file-name-sans-extension fname))) (insert ? ofile) (goto-char (car (cddr lgc-lame-init))) (insert ofile ": " fname " \x9$(CC) $(CFLAGS) -o $@ $<\n\n"))))) (defun lgc-init-makefile () "Return list of Makefile-relevant items: * Makefile buffer. * Marker at which to write output file names, space before each one. * Marker at which to write make rules for said output, two newlines after each one. Both markers have insertion type t." (save-excursion (find-file (expand-file-name "Makefile" (expand-file-name lgc-c-output-dir))) (erase-buffer) (insert (car lgc-c-makefile-template)) (list (current-buffer) (let (ofiles) (goto-line (cadr lgc-c-makefile-template)) (end-of-line) (setq ofiles (point-marker)) (set-marker-insertion-type ofiles t) ofiles) (let (rules) (goto-line (car (cddr lgc-c-makefile-template))) (beginning-of-line) (setq rules (point-marker)) (set-marker-insertion-type rules t) rules)))) ;;; MISC UTIL (defun lgc-nullify-txibeg (fname fbody txibeg) "Point marker TXIBEG at nothing." (set-marker txibeg nil)) ;;; lgc-examples.el ends here