andyjpb's Markdown added by andyjpb on Mon Jul 16 11:11:36 2012

$ more Markdown.scm 
(module Markdown
 (*
  Markdown)

(import scheme)
(import chicken)
(use extras)
(use md5)
(use matchable)
(use srfi-13) ; string-downcase
(require-extension regex)
(import irregex)

(define *tab-width* 4)
(define *less-than-tab* (- *tab-width* 1))

(define *escape-table*
  (let gen ((t '(#\\ #\` #\* #\_ #\{ #\} #\[ #\] #\( #\) #\# #\+ #\- #\. #\!)))
    (if (null? t) '()
      (cons `(,(car t) . ,(md5-digest (string (car t)))) (gen (cdr t))))))

;(define cons-and-carry-on begin)

(define (is-blank? str)
  (irregex-match "^[[:space:]]*$" str))

(define (generator lst)
 (let ((lst lst))
  (lambda ()
   (if (null? lst) #f
     (let ((item (car lst)))
       (set! lst (cdr lst))
       item)))))


; Main procedure
; The order in which other procs are called here is essential. Link and image
; substitutions need to happen before _EscapeSpecialChars, so that any *'s or
; _'s in the  and  tags get encoded.
;
; We read from current-input-port and return sxml in a list.
;
(define (Markdown)
 ; Standardize the line endings
 ; read-line returns the lines without newline characters

 (let* ((lines
          ; Here we read every line of the file converting tabs to aligned spaces
          ; (_Detab regex) as we go. We also transform consecutive blank lines
          ; (i.e. lines with no characers or consisting of whitespace only) into a
          ; single blank line (i.e. lines with no characters).
          ; Canonicalising blank lines makes subsequent regexes easier to write, because
          ; we can match consecutive blank lines with /\n+/ instead of something
          ; contorted like /[ \t]*\n+/ . Of course, in this version there are no
          ; I'm not sure in which situations it's dangerous to change tabs to spaces but
          ; it's what the original Perl version does.
          (read-file (current-input-port)
            (let ((prev-line '()))
             (lambda (port)
              (let loop ((str (read-line port)))
               (cond
                ((eof-object? str) str)
                ((and (is-blank? str) (is-blank? prev-line)) (loop (read-line port))) ; Strip consecutive blank lines
                (else
                  (set! prev-line str)
                  (if (is-blank? str) ""                        ; Return empty lines as blank
                     (irregex-replace/all "(.*?)\t" str         ; _Detab : Convert all tabs to spaces
                      (lambda (match)                           ; Regex from http://www.nntp.perl.org/group/perl.macperl.anyperl/2002/03/msg154.html
                       (let ((match (irregex-match-substring match 1)))
                        (string-append
                         match
                         (make-string (- *tab-width* (modulo (string-length match) *tab-width*)) #\ )))))))))))))
        (next-line (generator lines))
        ; TODO: Pull out all the preformed HTML sections as we don't render them. See _HashHTMLBlocks, pg.6.
        ; We can put them back into the list as sxml lists and we can ignore them later because they won't be strings.
        (lines
          ; We only want to do this for block-level HTML tags, such as headers,
          ; lists, and tables. That's because we still want to wrap 

s around ; "paragraphs" that are wrapped in non-block-level tags, such as anchors, ; phrase emphasis, and spans. The list of tags we're looking for is ; hard-coded: (let ((block-tags-a (string->sre "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del")) (block-tags-b "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math") (lines (make-parameter '()))) ; First, look for nested blocks, e.g.: ;

;
; tags for inner block must be indented. ;
;
; ; The outermost tags must start at the left margin for this to match, and ; the inner nested divs must be indented. ; We need to do this before the next, more liberal match, because the next ; match will start at the first `
` and stop at the first `
`. (let loop ((line (next-line)) (prev-line '())) (if line (if (not (string? line)) ;(cons-and-carry-on ; Already processed (begin ; Already processed (lines (cons line (lines))) (loop (next-line) line)) (let ((irmatch (irregex-match (irregex `(: bos "<" (submatch ,block-tags-a) eow (* nonl)) 'i ) line))) (if irmatch (let* ((tag (irregex-match-substring irmatch 1))) ; TODO: This bit (display (format "found starting tag~A\n" tag)))) (lines (cons line (lines))) (loop (next-line) line) )))) (lines))) (lines (reverse lines)) (next-line (generator lines)) ; Here we loop through every line and try to decide if there's a link ; to strip. If we find one then we strip it. We are careful to not ; leave consecutive blank lines in the file. _StripLinkDefinitions ; We must strip links before anything else to have the same "re-use id" ; semantics as the original parser. (link-refs (make-parameter '())) (lines (let ((lines (make-parameter '()))) (let loop ((line (next-line)) (prev-line '())) (if line ; TODO: (if !string (cons-and-carry-on) (else... ; Is it a link reference? (let ((irmatch (irregex-match (string-append "^[ ]{0," (format "~A" *less-than-tab*) "}" ; Up to "3" spaces. "\\[(.+)\\]:" ; Then the link id in square brackets followed by a colon. "[ ]*" ; Maybe some space. "(?)?" ; Then maybe the URL itself, possibly enclosed between <>. "[ ]*" ; Maybe some more space. "([\"(](.+)[\")])?" ; Then maybe an optional title maybe enclosed in " or ( or possibly a mixture. "[ ]*" ; Possibly some trailing space. ) line))) (if irmatch (let* ((id (irregex-match-substring irmatch 1)) (url (irregex-match-substring irmatch 2)) (url (if url (irregex-replace "^?$" url 1) #f)) (title (irregex-match-substring irmatch 4)) (stow-and-continue (lambda (id url title next) ; stow the id, url and title in the dictionary (link-refs (cons (list (string-downcase id) url title) (link-refs))) ; Preserve "no consecutive blank lines". ; If the previous line was blank, strip the next line if it's also blank and pass on a blank "prev-line" ; otherwise pass on the next line and the previous line, not the current line as we're stripping it. (if (and (is-blank? prev-line) (is-blank? next)) (loop (next-line) prev-line) (loop next prev-line)))) (find-title (lambda (id url) ; There's a chance that the title will be on the next line. (let* ((next (next-line)) (irmatch (irregex-match (string-append "[ ]*" ; Always allowing the blank space. "([\"(](.+)[\")])" ; If we're lucky there will be a title, maybe enclosed in " or ( or possibly a mixture. "[ ]*" ; Nothing else to be found on this line except maybe those whitespaces. ) next))) (if irmatch (let ((title (irregex-match-substring irmatch 1))) (stow-and-continue id url title (next-line))) (stow-and-continue id url #f next)))))) ; This next line wasn't a title so stow this link without one. (match (list id url title) ((#f #f #f) ; Something went horribly wrong: just cons up the line and carry on. ;(cons-and-carry-on (begin (lines (cons line (lines))) (loop (next-line) line))) ((_ #f #f) ; We might have found a link, provided the URL (and maybe also title) are on the next line. (let* ((next (next-line)) (irmatch (irregex-match (string-append "[ ]*" ; Maybe some space. "(?)" ; Then definitely the URL itself, possibly enclosed between <>. "[ ]*" ; Maybe a bit more space. "([\"(](.+)[\")])?" ; Then maybe an optional title maybe enclosed in " or ( or possibly a mixture. "[ ]*" ; Of course, space is allowed. ) next))) ; This will match unless the line is blank. The original parser can produce all sorts of bogus URLS. (if irmatch (let ((url (irregex-replace "^?$" (irregex-match-substring irmatch 1) 1)) (title (irregex-match-substring irmatch 2))) (match (list id url title) ((_ _ #f) ; We found a URL but not a title. (find-title id url)) ((_ _ _ ) ; We've got everything. (stow-and-continue id url title (next-line))))) ;(cons-and-carry-on ; Oops.. we didn't really find a link. (begin ; Oops.. we didn't really find a link. (lines (cons line (lines))) (loop next line))))) ((_ _ #f) ; We got an ID and URL but not a title. (find-title id url)) ((_ _ _ ) ; Everything all on one line: super! (stow-and-continue id url title (next-line))))) ;(cons-and-carry-on ; This doesn't look like a link reference. (begin ; This doesn't look like a link reference. (lines (cons line (lines))) (loop (next-line) line)))))) (lines))) (lines (reverse lines)) (next-line (generator lines))) ; Work out the type of that line and enclose it in the ; correct sxml forms, escaping and converting as we go... ; ; Write something that just returns the line prefixed by the type we think it is? ; that's virtually the whole thing anyway... ; This clause needs to evaluate to the sxml document ; The document will probably need a final processing step to render all the ; links. (let loop ((line (next-line))) (if line (begin (display line) (newline) (loop (next-line))))) ;(link-refs)) "done") ; From the syntax document: ; Inline HTML: *blocks* are in their own paragraph and the first tag starts ; in column 1. ; Make sure that the input ends with a couple of newlines ; Why?? ; Turn block-level HTML blocks into hash entries ; Strip link definitions, store in hashes. ; -- strip these as we go and store them somewhere. ; Whenever we generate a link, generate a procedure that we can evaluate ; later and pass in the store of definitions. ; Converting inline html to sxml: use irregex to find the start of the tag ; match. emit the text until that point, then emit an sxml form containing the ; substring from the match. This will need to be recursive. ) )