;;; book-pages.ss: drafting Web pages containing bibliographical data
;;; John David Stone
;;; Department of Mathematics and Computer Science
;;; Grinnell College
;;; stone@math.grin.edu
;;; created November 5, 1997
;;; last revised May 4, 1998
;; This program takes the facts of publication (author, title, publisher,
;; date of publication, and place of publication) about various books and
;; writes them out to one or more files that can be displayed by Netscape,
;; Internet Explorer, and other browsers as documents on the World Wide
;; Web. To achieve this, the program also writes ``markup tags'' into the
;; files -- short character sequences that tell browsers how the rest of
;; the text is supposed to be displayed. (The browser usually does not
;; display the markup tags along with the rest of the text; when it
;; recognizes the tags as instructions to itself, it filters them out of
;; the document before displaying it.) The effect of each markup tag is
;; specified by the Hypertext Markup Language (HTML), a standard set of
;; conventions that all World Wide Web browsers observe and understand.
;; Actually, several revisions of the Hypertext Markup Language standard
;; have appeared, as the set of conventions has gradually increased in size
;; and complexity to reflect the increasing variety of treatments of text
;; that the creators of World Wide Web documents have insisted on. The
;; documents produced by this program conform to version 4.0 of the
;; standard.
;; Some browsers require that a World Wide Web document begin with an
;; announcement of what version of the HTML standard it conforms to and
;; what language it is written in. The conventional way to announce a
;; version 4.0 document written in English is to place the following string
;; at the beginning of the document:
(define document-type-definition-specification
"")
;; In other words: ``This is a document containing Hypertext Markup
;; Language tags, using no additional private or proprietary conventions.
;; The intended meanings of the markup tags are specified in a Document
;; Type Definition (DTD) prepared by the World Wide Web Consortium (W3C)
;; and entitled `HTML 4.0' -- revision 4.0 of the HTML standard. The
;; current document is written in English (EN). See the document
;; http://www.w3.org/TR/REC-html40/strict.dtd, a section of the World Wide
;; Web Consortium's Technical Report defining HTML 4.0, for the specifics
;; of that Document Type Definition.''
;; We shall ensure that this string appears at the beginning of each output
;; file.
;; This program performs all of the actual writing out of text and markup
;; tags to files by invoking output port procedures (that is, procedures
;; that take an output port as their only argument). It is therefore
;; helpful to have some utilities for creating and operating on such
;; procedures. In this program, we adopt (for readability) the convention
;; that the name of every output port procedure and of every procedure that
;; creates output port procedures ends with the character %.
;; Here, for instance, is a procedure that takes any number of Scheme
;; values as arguments and returns an output port procedure that, when
;; invoked, calls the Scheme DISPLAY procedure to write out all those
;; values. It's called OUTPUT%:
(define output%
(lambda arguments
(lambda (port)
;; Make sure PORT is an output port.
(if (not (output-port? port))
(error 'output% "Output must be sent to an output port"))
;; Display each argument through that port.
(for-each (lambda (value)
(display value port))
arguments))))
;; So, for instance, when the following output port procedure, DTD%,
;; is invoked, it writes the DOCUMENT-TYPE-DEFINITION-SPECIFICATION string
;; on a line by itself through the port that it receives as its argument
;; and into a file.
(define dtd%
(output% document-type-definition-specification #\newline))
;; Everything in a World Wide Web document after the DTD specification is
;; supposed to be enclosed between a matched pair of markup tags: at
;; the beginning, at the end. Many HTML tags come in similar
;; matched pairs, surrounding the text to which some kind of special
;; treatment is to be given. We shall ensure that appears as the
;; second line of each of the output files, and as the last line.
;; One approach to creating the matched pair would be to write a procedure
;; ENCLOSE-IN-HTML-TAGS% that takes as its argument an output port
;; procedure that writes out the text to be enclosed, and returns an output
;; port procedure that writes out the initial tag, then the enclosed text,
;; then the final tag:
;; (define enclose-in-html-tags%
;; (lambda (text%)
;; (lambda (port)
;; ((output% "" #\newline) port)
;; (text% port)
;; ((output% "" #\newline) port)))))
;; However, this idea can be usefully generalized in two ways. First, we
;; will often wish to write an output port procedure that, like this one,
;; consists entirely of a succession of calls to other output port
;; procedures, with the same port argument for each one. This control
;; structure is common enough to deserve a name and a definition; we'll
;; call it a ``chain'' of calls, and provide a definition of a CHAIN%
;; procedure that takes any number of output port procedures as arguments
;; and returns an output port procedure that simply calls each of those
;; output port procedures in turn:
(define chain%
(lambda procedures
(lambda (port)
;; Make sure PORT is an output port.
(if (not (output-port? port))
(error 'chain% "Output must be sent to an output port"))
;; Apply each of the output port procedures to that port.
(for-each (lambda (procedure%)
(procedure% port))
procedures))))
;; So, for example, we could now write the definition of
;; ENCLOSE-IN-HTML-TAGS% like this:
;; (define enclose-in-html-tags%
;; (lambda (text%)
;; (chain% (output% "" #\newline)
;; text%
;; (output% "" #\newline))))
;; The second generalization is prompted by the fact that the matched pair
;; and is only one of many such pairs in HTML -- for
;; instance, one also writes
and
to enclose a paragraph of text,
;; to enclose an unnumbered list, and so on. So it will be
;; helpful to have a general-purpose enclosing procedure that takes as one
;; of its arguments the string of letters that indicates which particular
;; tag-pair is to be used for enclosing the text:
(define enclose%
(lambda (tag-string text%)
;; Make sure that TAG-STRING is a string and TEXT% is a procedure.
(if (not (string? tag-string))
(error 'enclose% "The first argument must be a string"))
(if (not (procedure? text%))
(error 'enclose%
"The second argument must be an output port procedure"))
;; Chain together the opening tag, the enclosed text, and the closing
;; tag.
(chain% (output% #\< tag-string #\> #\newline)
text%
(output% #\< #\/ tag-string #\> #\newline))))
;; The tags inserted by ENCLOSE% are placed on separate lines, before and
;; after the enclosed text. We also sometimes use matched pairs of markup
;; tags placed immediately next to the enclosed text. For instance, to ask
;; the browser to emphasize a particular word, we can put the tag just
;; before the word and its mate just after it, thus:
;; Emphasize this word.
;; So it will also be helpful to have a variant of ENCLOSE% that does not
;; insert newlines:
(define enclose-on-same-line%
(lambda (tag-string text%)
;; Make sure that TAG-STRING is a string and TEXT% is a procedure.
(if (not (string? tag-string))
(error 'enclose-on-same-line%
"The first argument must be a string"))
(if (not (procedure? text%))
(error 'enclose-on-same-line%
"The second argument must be an output port procedure"))
;; Chain together the opening tag, the enclosed text, and the closing
;; tag.
(chain% (output% #\< tag-string #\>)
text%
(output% #\< #\/ tag-string #\>))))
;; Between the and tags in a World Wide Web document, one is
;; supposed to provide (1) a header and (2) a body.
;; The header contains the document's title and may also contain other
;; information that can be used by automatic indexing and cross-referencing
;; programs; we'll stick with just the title. The header section begins
;; with the tag and ends with its mate, and the title that is
;; given between these tags is similarly enclosed between the tags
;; and . A typical header therefore looks like this:
;;
;; CSC 151: Fundamentals of computer science I
;;
;; The following procedure, HEADER%, takes a string argument that gives the
;; desired document title and returns an output port procedure that writes
;; the entire header through a port:
(define header%
(lambda (title-string)
;; Make sure that the argument is a string.
(if (not (string? title-string))
(error 'header% "The title must be given as a string"))
;; Enclose the title in ... tags, add a newline after
;; the enclose title, and enclose the result between and
;; .
(enclose% "head"
(chain% (enclose-on-same-line% "title"
(output% title-string))
(output% #\newline)))))
;; The body that follows the header is supposed to begin with the tag
;; and end with the matching tag . What lies between these
;; tags is what the browser actually displays in the window.
;; Here, then, is the overall outline of a World Wide Web document
;; generator:
(define WWW-document%
(lambda (title-string displayed-text%)
;; Make sure that the title string is indeed a string and that
;; DISPLAYED-TEXT% is a procedure.
(if (not (string? title-string))
(error 'WWW-document% "The first argument must be a string"))
(if (not (procedure? displayed-text%))
(error 'WWW-document%
"The second argument must be an output port procedure"))
;; Enclose the displayed text in ... tags, chain the
;; header to the front of it, enclose the result in ..
;; tags, and chain the document type definition specification at the
;; very beginning.
(chain% dtd%
(enclose% "html" (chain% (header% title-string)
(enclose% "body" displayed-text%))))))
;; Now let's consider how to come up with the title string and the output
;; port procedure that writes out the displayed text, given the
;; bibliographical database. The database is given in the form of an
;; association list in which each key is the name of an author (as a
;; string) and the associated value is a list of one or more books written
;; by that author. Each book is itself represented as a record with four
;; fields, none of them mutable: The first field is the book's title (as a
;; string), the second is the name of the publisher (as a string), the
;; third is the place of publication (as a string), and the fourth is the
;; year of publication (as an exact integer).
;; Here are the definitions relating to this record type, as constructed
;; by the GENERATE-RECORD-DEFINITION-FILE procedure that we studied in the
;; lab on metaprogramming. I've edited out the mutators.
(define make-book
(lambda (title
publisher
place-of-publication
year-of-publication)
(vector
'book
title
publisher
place-of-publication
year-of-publication)))
(define book?
(lambda (obj)
(and (vector? obj)
(= (vector-length obj) 5)
(eq? (vector-ref obj 0) 'book))))
(define book-title
(lambda (b)
(if (not (book? b))
(error 'book-title "The argument must be of type BOOK"))
(vector-ref b 1)))
(define book-publisher
(lambda (b)
(if (not (book? b))
(error 'book-publisher "The argument must be of type BOOK"))
(vector-ref b 2)))
(define book-place-of-publication
(lambda (b)
(if (not (book? b))
(error 'book-place-of-publication
"The argument must be of type BOOK"))
(vector-ref b 3)))
(define book-year-of-publication
(lambda (b)
(if (not (book? b))
(error 'book-year-of-publication
"The argument must be of type BOOK"))
(vector-ref b 4)))
;; Now here is the database:
(define facts-of-publication
`(("Sagan, Carl"
,(make-book "Cosmos" "Ballantine Books" "New York" 1980)
,(make-book "Pale Blue Dot" "Ballantine Books" "New York" 1994)
,(make-book "The Demon-Haunted World" "Random House" "New York" 1995))
("Hofstadter, Douglas R."
,(make-book "Goedel, Escher, Bach: an Eternal Golden Braid"
"Basic Books, Inc., Publishers" "New York" 1979))
("Commoner, Barry"
,(make-book "Making Peace with the Planet" "The New Press" "New York"
1992))
("Smullyan, Raymond"
,(make-book "This Book Needs No Title" "Prentice-Hall, Inc"
"Englewood Cliffs, New Jersey" 1980)
,(make-book "5000 B.C." "St. Martin's Press" "New York" 1983))
("Gould, Stephen Jay"
,(make-book "Hen's Teeth and Horse's Toes" "W. W. Norton and Company"
"New York" 1983)
,(make-book "Wonderful Life" "W. W. Norton and Company" "New York"
1989)
,(make-book "The Mismeasure of Man" "W. W. Norton and Company"
"New York" 1996)
,(make-book "Questioning the Millennium" "Harmony Books" "New York"
1997))
("Gardner, Martin"
,(make-book "The Night Is Large" "St. Martin's Press" "New York" 1996)
,(make-book "The Flight of Peter Fromm" "William Kaufmann, Inc."
"Los Altos, California" 1973))))
;; The FACTS% procedure takes a book as its argument and returns an output
;; port procedure that writes out a bibliography entry for that book, in a
;; standard format.
(define facts%
(lambda (book)
;; Check that BOOK is indeed a book.
(if (not (book? book))
(error 'facts% "The argument must be a book"))
;; Put the title in italics by enclosing it in ... tags.
;; Chain the result with the other data. The specific format of
;; the eventual output will be:
;;
;; The title. Place: Publisher and Sons, Inc., 1999.
(chain% (enclose-on-same-line% "i" (output% (book-title book)))
(output% #\. #\space
(book-place-of-publication book) #\: #\space
(book-publisher book) #\, #\space
(book-year-of-publication book) #\.))))
;; The BOOK-ROSTER% procedure takes as its argument a list of books and
;; returns an output port procedrue that writes out an unnumbered sequence
;; of bibliography entries, one for each book. The unnumbered sequence is
;; enclosed by tags (``unordered list''), and each entry is
;; enclosed by ... tags (``list item''), as specified in the
;; HTML standard.
(define book-roster%
(lambda (book-list)
;; Make sure BOOK-LIST is a list.
(if (not (list? book-list))
(error 'book-roster% "The argument must be a list"))
;; Enclose each bibliography entry in ... tags to make it a
;; ``list item.'' Chain the result to a newline. Do this for every
;; book on the list and chain the results together. Finally, enclose
;; the entire chain in tags to make an unnumbered
;; sequence out of it
(enclose% "ul"
(apply chain%
(map (lambda (book)
(chain% (enclose-on-same-line% "li"
(facts% book))
(output% #\newline)))
book-list)))))
;; Each element of the FACTS-OF-PUBLICATION list is a pair that has as its
;; car the name of an author and as its cdr a list of that author's books.
;; The EXHIBIT% procedure takes such a pair as its argument and returns an
;; output port procedure suitable for use as the DISPLAYED-TEXT% argument
;; to the WWW-DOCUMENT% procedure above: The displayed text consists a
;; large header, giving the author's name, followed by a sequence of
;; bibliography entries, as constructed by BOOK-ROSTER%.
(define exhibit%
(lambda (pair)
;; Check that PAIR is indeed a pair.
(if (not (pair? pair))
(error 'exhibit% "The argument must be a pair"))
;; Separate the author from the books.
(let ((author (car pair))
(books (cdr pair)))
;; Put the author's name in a primary header by enclosing it in
;; ...
tags (``header, size 1'' -- the largest size). Leave a
;; blank line after the header, then chain in the unnumbered sequence
;; of bibliography entries returned by BOOK-ROSTER%.
(chain% (enclose-on-same-line% "h1" (output% author))
(output% #\newline #\newline)
(book-roster% books)))))
;; The SURNAME procedure takes as argument a person's name, as a string in
;; the format "Lastname, Firstname(s)", and returns the part of the string
;; that occurs before the comma. (If the string contains two or more
;; commas, the surname is assumed to be the part up to the leftmost comma;
;; if the string contains no commas, the surname is assumed to be the
;; entire string.
(define surname
(lambda (full-name)
;; Check that FULL-NAME is a string.
(if (not (string? full-name))
(error 'surname "The argument must be a string"))
;; Search FULL-NAME from left to right, looking for a comma.
(let ((len (string-length full-name)))
(let loop ((position 0))
(cond
;; If the end of the string is reached, return the whole thing.
((= position len) full-name)
;; If a comma is encountered, return the part of the string that
;; has so far been traversed.
((char=? #\, (string-ref full-name position))
(substring full-name 0 position))
;; Otherwise, keep searching.
(else (loop (+ position 1))))))))
;; The BUILD-DOCUMENT procedure takes one author-and-books element from the
;; bibliographical database and creates a World Wide Web document for that
;; author in the current working directory. The name of the file will be
;; the surname of the author followed by the suffix .html (which is typical
;; for HTML documents) -- for example, the World Wide Web document listing
;; the Carl Sagan books will be in the file Sagan.html.
(define build-document
(lambda (pair)
;; Check that PAIR is indeed a pair.
(if (not (pair? pair))
(error 'build-document "The argument must be a pair"))
;; Recover the author's surname and use it to construct the name of the
;; output file.
(let* ((author (car pair))
(file-name (string-append (surname author) ".html")))
;; Obtain an output port procedure that will push the entire document
;; through a port and use CALL-WITH-OUTPUT-FILE to invoke that
;; procedure.
(call-with-output-file file-name
(WWW-document% author (exhibit% pair))))))
;; Here's the main program -- the command that sets all the others in
;; motion:
;; To produce the complete set of World Wide Web documents, one for each
;; author, we apply the BUILD-DOCUMENT procedure to each element of the
;; bibliographical database.
(for-each build-document facts-of-publication)
;; When all the documents have been built, shut down Scheme.
(exit)