hyper-literate/scribble-lib/scribble/markdown-render.rkt
2019-12-06 04:59:35 -07:00

390 lines
15 KiB
Racket
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#lang racket/base
(require "core.rkt"
"base-render.rkt"
"private/render-utils.rkt"
racket/class racket/port racket/list racket/string racket/match
scribble/text/wrap)
(provide render-mixin
current-markdown-link-sections)
(define current-markdown-link-sections (make-parameter #f))
(define current-indent (make-parameter 0))
(define (make-indent amt)
(+ amt (current-indent)))
(define (indent)
(define i (current-indent))
(unless (zero? i) (display (make-string i #\space))))
(define (indented-newline)
(newline)
(indent))
(define note-depth (make-parameter 0))
(define in-toc (make-parameter #f))
(define markdown-part-tag 'markdown-section)
(define (render-mixin %)
(class %
(define/override (current-render-mode)
'(markdown))
(define/override (get-suffix) #".md")
(define/override (get-substitutions)
'((#rx"---" "\U2014")
(#rx"--" "\U2013")
(#rx"``" "\U201C")
(#rx"''" "\U201D")
(#rx"'" "\U2019")))
(inherit render-block
format-number
number-depth)
(define/override (collect-part-tags d ci number)
(for ([t (part-tags d)])
(let ([t (generate-tag t ci)])
(collect-put! ci
t
(vector (or (part-title-content d) '("???"))
(add-current-tag-prefix t)
number
markdown-part-tag)))))
(define/override (render-part d ht)
(let ([number (collected-info-number (part-collected-info d ht))])
(unless (part-style? d 'hidden)
(printf (string-append (make-string (add1 (number-depth number)) #\#) " "))
(let ([s (format-number number '() #t)])
(unless (null? s)
(printf "~a~a"
(car s)
(if (part-title-content d)
" "
"")))
(when (part-title-content d)
(render-content (part-title-content d) d ht))
(when (or (pair? number) (part-title-content d))
(newline)
(newline))))
(render-flow (part-blocks d) d ht #f)
(let loop ([pos 1]
[secs (part-parts d)]
[need-newline? (pair? (part-blocks d))])
(unless (null? secs)
(when need-newline? (newline))
(render-part (car secs) ht)
(loop (add1 pos) (cdr secs) #t)))))
(define/override (render-flow f part ht starting-item?)
(if (null? f)
null
(append*
(render-block (car f) part ht starting-item?)
(for/list ([p (in-list (cdr f))])
(indented-newline)
(render-block p part ht #f)))))
(define/override (render-intrapara-block p part ri first? last? starting-item?)
(unless first? (indented-newline))
(super render-intrapara-block p part ri first? last? starting-item?))
(define/override (render-table i part ht inline?)
(define flowss (table-blockss i))
(define tick? (member (style-name (table-style i))
(list 'boxed "defmodule" "RktBlk")))
(cond
[(null? flowss) null]
[(and tick? (not (in-code?)))
(displayln "```racket")
(parameterize ([in-code? #t])
(render-table i part ht inline?))
(displayln "```")]
[else
(define strs (map (lambda (flows)
(map (lambda (d)
(if (eq? d 'cont)
d
(let ([o (open-output-string)])
(parameterize ([current-indent 0]
[current-output-port o])
(render-block d part ht #f))
(regexp-split
#rx"\n"
(regexp-replace #rx"\n$"
(get-output-string o)
"")))))
flows))
flowss))
(define widths (map (lambda (col)
(for/fold ([d 0]) ([i (in-list col)])
(if (eq? i 'cont)
0
(apply max d (map string-length i)))))
(apply map list strs)))
(define x-length (lambda (col) (if (eq? col 'cont) 0 (length col))))
(for/fold ([indent? #f]) ([row (in-list strs)])
(let ([h (apply max 0 (map x-length row))])
(let ([row* (for/list ([i (in-range h)])
(for/list ([col (in-list row)])
(if (i . < . (x-length col))
(list-ref col i)
"")))])
(for/fold ([indent? indent?]) ([sub-row (in-list row*)])
(when indent? (indent))
(for/fold ([space? #f])
([col (in-list sub-row)]
[w (in-list widths)])
(let ([col (if (eq? col 'cont) "" col)])
(display (regexp-replace* #rx"\uA0" col " "))
(display (make-string (max 0 (- w (string-length col))) #\space)))
#t)
(newline)
#t)))
#t)])
null)
(define/override (render-itemization i part ht)
(let ([flows (itemization-blockss i)])
(if (null? flows)
null
(append*
(begin (printf "* ")
(parameterize ([current-indent (make-indent 2)])
(render-flow (car flows) part ht #t)))
(for/list ([d (in-list (cdr flows))])
(indented-newline)
(printf "* ")
(parameterize ([current-indent (make-indent 2)])
(render-flow d part ht #f)))))))
(define/override (render-paragraph p part ri)
(define (write-note)
(write-string (make-string (note-depth) #\>))
(unless (zero? (note-depth))
(write-string " ")))
(cond
[(in-toc)
(write-note)
(super render-paragraph p part ri)
;; two spaces at a line end creates a line break:
(write-string " ")]
[else
(define o (open-output-string))
(parameterize ([current-output-port o])
(super render-paragraph p part ri))
;; 1. Remove newlines so we can re-wrap the text.
;;
;; 2. Combine adjacent code spans into one. These result from
;; something like @racket[(x y)] being treated as multiple
;; RktXXX items rather than one. (Although it would be
;; more-correct to handle them at that level, I don't easily see
;; how. As a result I'm handling it after-the-fact, at the
;; text/Markdown stage.)
(define to-wrap (regexp-replaces (get-output-string o)
'([#rx"\n" " "] ;1
[#rx"``" ""]))) ;2
(define lines (wrap-line (string-trim to-wrap) (- 72 (current-indent))))
(write-note)
(write-string (car lines))
(for ([line (in-list (cdr lines))])
(newline) (indent) (write-note) (write-string line))])
(newline)
null)
(define/private (content-style e)
(cond
[(element? e) (element-style e)]
[(multiarg-element? e) (multiarg-element-style e)]
[else #f]))
(define in-bold? (make-parameter #f))
(define in-italic? (make-parameter #f))
(define in-code? (make-parameter #f))
(define in-link? (make-parameter #f))
(define preserving-spaces? (make-parameter #f))
(define (bold? i)
(and (element? i) (eq? (element-style i) 'bold)))
(define (italic? i)
(and (element? i) (eq? (element-style i) 'italic)))
(define (emph? i)
(and (element? i) (eq? (element-style i) 'emph)))
(define (code? i)
(and (element? i)
(let ([s (element-style i)])
(or (eq? 'tt s)
(and (style? s)
(style-name s)
(regexp-match? #rx"^Rkt[A-Z]" (style-name s)))))))
(define (link? i)
(let ([s (content-style i)])
(and (style? s) (findf target-url? (style-properties s)))))
(define (link-from i)
(target-url-addr (findf target-url? (style-properties (content-style i)))))
(define (preserve-spaces? i)
(and (element? i)
(let ([s (element-style i)])
(or (eq? 'hspace s)
(and (style? s)
(eq? 'hspace (style-name s)))))))
(define (sanitize-parens str)
(regexp-replace #rx"[\\(\\)]" str "\\&"))
(define/override (render-content i part ri)
(define (recurse-wrapped str param)
(display str)
(begin0
(parameterize ([param #t])
(render-content i part ri))
(display str)))
(cond
[(and (code? i) (not (in-code?)))
(recurse-wrapped "`" in-code?)]
[(and (bold? i) (not (in-bold?)) (not (in-code?)))
(recurse-wrapped "**" in-bold?)]
[(and (italic? i) (not (in-italic?)) (not (in-code?)))
(recurse-wrapped "_" in-italic?)]
[(and (emph? i) (not (in-code?)))
(display "_") ;; zero-width space, underscore
(begin0
(super render-content i part ri)
(display "_"))] ;; underscore, zero-width space
[(and (preserve-spaces? i) (not (preserving-spaces?)))
(parameterize ([preserving-spaces? #t])
(render-content i part ri))]
[(and (link? i) (not (in-link?)))
(let ([link (link-from i)])
(display "[")
(begin0
(parameterize ([in-link? #t])
(render-content i part ri))
(printf "](~a)" (sanitize-parens link))))]
[(and (link-element? i)
(current-markdown-link-sections)
(not (in-link?))
;; Link to a part within this document?
(let ([vec (resolve-get part ri (link-element-tag i))])
(and (vector? vec)
(= 4 (vector-length vec))
(eq? markdown-part-tag (vector-ref vec 3))
vec)))
=> (lambda (vec)
(define s (string-append
(let ([s (if (vector-ref vec 2)
(format-number (vector-ref vec 2) '() #t)
'())])
(if (null? s)
""
(string-append (car s) " ")))
(content->string (vector-ref vec 0))))
(display "[")
(begin0
(parameterize ([in-link? #t])
(super render-content i part ri))
(display "](#")
(display (regexp-replace* #" "
(regexp-replace* #rx"[^a-zA-Z0-9_ -]" (string-downcase s) "")
#"-"))
(display ")")))]
[else (super render-content i part ri)]))
(define/override (render-nested-flow i part ri starting-item?)
(define s (nested-flow-style i))
(unless (memq 'decorative (style-properties s))
(define note? (equal? (style-name s) "refcontent"))
(define toc? (equal? (style-name s) 'table-of-contents))
(when note?
(note-depth (add1 (note-depth))))
(begin0 (parameterize ([in-toc (or toc? (in-toc))])
(super render-nested-flow i part ri starting-item?))
(when note?
(note-depth (sub1 (note-depth)))))))
(define/override (render-other i part ht)
(cond
[(symbol? i)
(display (case i
[(mdash) "\U2014"]
[(ndash) "\U2013"]
[(ldquo) "\U201C"]
[(rdquo) "\U201D"]
[(lsquo) "\U2018"]
[(rsquo) "\U2019"]
[(lang) "<"]
[(rang) ">"]
[(rarr) "->"]
[(nbsp) "\uA0"]
[(prime) "'"]
[(alpha) "\u03B1"]
[(infin) "\u221E"]
[else (error 'markdown-render "unknown element symbol: ~e"
i)]))]
[(string? i)
(let* ([i (cond
[(in-code?)
(regexp-replace** i '([#rx"``" . "\U201C"]
[#rx"''" . "\U201D"]))]
[(or (in-link?)
(regexp-match? #rx"^[(]" i)
(regexp-match? #rx"[]][(]" i))
(regexp-replace* #px"([#_*`\\[\\(\\]\\)]{1})" i "\\\\\\1")]
[else
;; Avoid escaping parentheses
(regexp-replace* #px"([#_*`\\[\\]]{1})" i "\\\\\\1")])]
[i (if (preserving-spaces?)
(regexp-replace* #rx" " i "\uA0")
i)])
(display i))]
[else (write i)])
null)
(define/override (table-of-contents part ri)
(define t (super table-of-contents part ri))
(cond
[(current-markdown-link-sections)
;; Table generated by `table-of-contents` always has one
;; column, and each row has one paragraph that starts
;; with a 'hspace element to indent
(nested-flow
(style 'table-of-contents null)
(for/list ([p (map car (table-blockss t))])
(define c (paragraph-content p))
(define keep-c (cdr c))
(define (spaces->depth n)
(add1 (quotient (- n 4) 2)))
(for/fold ([p (paragraph plain keep-c)]) ([s (in-range
(spaces->depth
(string-length (car (element-content (car c))))))])
(nested-flow (style "refcontent" null) (list p)))))]
[else t]))
(super-new)))
(define (regexp-replace** str ptns&reps)
(for/fold ([str str])
([ptn (map car ptns&reps)]
[rep (map cdr ptns&reps)])
(regexp-replace* ptn str rep)))