#lang racket/base
( require xml txexpr racket/string racket/match racket/list ( prefix-in html: pollen/html ) sugar/list sugar/container sugar/len sugar/define sugar/coerce sugar/test )
( require " debug.rkt " " world.rkt " )
( define ( symbols? x ) ( and ( list? x ) ( andmap symbol? x ) ) )
( define+provide ( to-string x )
( if ( string? x )
x ; fast exit for strings
( with-handlers ( [ exn:fail? ( λ ( exn ) ( error ( format " Pollen decoder: can't convert ~v to ~a " x ' string ) ) ) ] )
( cond
[ ( equal? ' ( ) x ) " " ]
[ ( symbol? x ) ( symbol->string x ) ]
[ ( number? x ) ( number->string x ) ]
[ ( path? x ) ( path->string x ) ]
[ ( char? x ) ( format " ~a " x ) ]
[ ( void? x ) " " ]
;; todo: guard against weird shit like lists of procedures
[ ( or ( list? x ) ( hash? x ) ( vector? x ) ) ( format " ~v " x ) ] ; ok to convert datatypes
[ else ( error ) ] ) ) ) ) ; but things like procedures should throw an error
( define decode-proc-output-contract ( or/c xexpr? ( non-empty-listof xexpr? ) ) )
( define multiple-entity-signal ( gensym " multiple-entity " ) )
( define ( ->list/tx x )
;; same as ->list but catches special case of single txexpr,
;; which is itself a list, but in this case should be wrapped into a list,
;; for use with append-map.
( cond
;; use multiple-entity-signal to distinguish list of entities from txexprs
;; ambiguous example '(copy copy)
;; but with signal, it's '(multiple-entity-signal copy copy)
[ ( and ( pair? x ) ( eq? ( car x ) multiple-entity-signal ) ) ( cdr x ) ]
[ ( txexpr? x ) ( list x ) ]
[ else ( ->list x ) ] ) )
;; decoder wireframe
( define+provide/contract ( decode tx-in
#:txexpr-tag-proc [ txexpr-tag-proc ( λ ( x ) x ) ]
#:txexpr-attrs-proc [ txexpr-attrs-proc ( λ ( x ) x ) ]
#:txexpr-elements-proc [ txexpr-elements-proc ( λ ( x ) x ) ]
#:block-txexpr-proc [ block-txexpr-proc ( λ ( x ) x ) ]
#:inline-txexpr-proc [ inline-txexpr-proc ( λ ( x ) x ) ]
#:string-proc [ string-proc ( λ ( x ) x ) ]
#:entity-proc [ entity-proc ( λ ( x ) x ) ]
#:cdata-proc [ cdata-proc ( λ ( x ) x ) ]
#:exclude-tags [ excluded-tags ' ( ) ]
#:exclude-attrs [ excluded-attrs ' ( ) ] )
( ( xexpr/c )
( #:txexpr-tag-proc ( txexpr-tag? . -> . txexpr-tag? )
#:txexpr-attrs-proc ( txexpr-attrs? . -> . txexpr-attrs? )
#:txexpr-elements-proc ( txexpr-elements? . -> . txexpr-elements? )
#:block-txexpr-proc ( block-txexpr? . -> . decode-proc-output-contract )
#:inline-txexpr-proc ( txexpr? . -> . decode-proc-output-contract )
#:string-proc ( string? . -> . decode-proc-output-contract )
#:entity-proc ( ( or/c symbol? valid-char? ) . -> . decode-proc-output-contract )
#:cdata-proc ( cdata? . -> . decode-proc-output-contract )
#:exclude-tags ( listof txexpr-tag? )
#:exclude-attrs txexpr-attrs? ) . ->* . txexpr? )
( let loop ( [ x tx-in ] )
( cond
[ ( txexpr? x ) ( let-values ( [ ( tag attrs elements ) ( txexpr->values x ) ] )
( if ( or ( member tag excluded-tags ) ( ormap ( λ ( attr ) ( member attr excluded-attrs ) ) attrs ) )
x ; because it's excluded
;; we apply processing here rather than do recursive descent on the pieces
;; because if we send them back through loop, certain element types are ambiguous
;; e.g., ((p "foo")) tests out as both txexpr-attrs and txexpr-elements
( let ( [ decoded-txexpr
( apply make-txexpr ( list ( txexpr-tag-proc tag )
( txexpr-attrs-proc attrs )
( txexpr-elements-proc ( append-map ( compose1 ->list/tx loop ) elements ) ) ) ) ] )
( ( if ( block-txexpr? decoded-txexpr )
block-txexpr-proc
inline-txexpr-proc ) decoded-txexpr ) ) ) ) ]
[ ( string? x ) ( string-proc x ) ]
[ ( or ( symbol? x ) ( valid-char? x ) )
( define result ( entity-proc x ) )
( if ( list? result )
;; add a signal to list of multiple entities to avoid downstream ambiguity with txexpr
;; for instance '(copy copy) is a list of entities, but also a txexpr
;; stick a signal on the front, which will be picked up later
( cons multiple-entity-signal result )
result ) ]
[ ( cdata? x ) ( cdata-proc x ) ]
[ else ( error " decode: can't decode " x ) ] ) ) )
( module-test-external
( require racket/list txexpr racket/function )
( define ( doubler x ) ( list x x ) )
( check-equal? ( decode #:txexpr-elements-proc identity ' ( p " foo " ) ) ' ( p " foo " ) )
;; can't use doubler on txexpr-elements because it eneds a list, not list of lists
( check-equal? ( decode #:txexpr-elements-proc ( λ ( elems ) ( append elems elems ) ) ' ( p " foo " ) ) ' ( p " foo " " foo " ) )
( check-equal? ( decode #:block-txexpr-proc identity ' ( p " foo " ) ) ' ( p " foo " ) )
( check-equal? ( decode #:block-txexpr-proc doubler ' ( p " foo " ) ) ( list ' ( p " foo " ) ' ( p " foo " ) ) )
( check-equal? ( decode #:inline-txexpr-proc identity ' ( p ( span " foo " ) ) ) ' ( p ( span " foo " ) ) )
( check-equal? ( decode #:inline-txexpr-proc doubler ' ( p ( span " foo " ) ) ) ' ( p ( span " foo " ) ( span " foo " ) ) )
( check-equal? ( decode #:string-proc identity ' ( p ( span " foo " ) ) ) ' ( p ( span " foo " ) ) )
( check-equal? ( decode #:string-proc doubler ' ( p ( span " foo " ) ) ) ' ( p ( span " foo " " foo " ) ) )
( check-equal? ( decode #:entity-proc identity ' ( p ( span " foo " ' amp ) ) ) ' ( p ( span " foo " ' amp ) ) )
( check-equal? ( decode #:entity-proc identity ' ( p 42 ) ) ' ( p 42 ) )
( check-equal? ( decode #:entity-proc doubler ' ( p 42 ) ) ' ( p 42 42 ) )
( check-equal? ( decode #:entity-proc identity ' ( p amp ) ) ' ( p amp ) )
( check-equal? ( decode #:entity-proc doubler ' ( p amp ) ) ' ( p amp amp ) )
( check-equal? ( decode-elements #:string-proc identity ' ( " foo " ) ) ' ( " foo " ) )
( check-equal? ( decode-elements #:string-proc doubler ' ( " foo " ) ) ' ( " foo " " foo " ) ) )
;; it would be nice to not repeat this, but with all the keywords, it's simpler to repeat than do a macro
( define+provide/contract ( decode-elements elements
#:txexpr-tag-proc [ txexpr-tag-proc ( λ ( x ) x ) ]
#:txexpr-attrs-proc [ txexpr-attrs-proc ( λ ( x ) x ) ]
#:txexpr-elements-proc [ txexpr-elements-proc ( λ ( x ) x ) ]
#:block-txexpr-proc [ block-txexpr-proc ( λ ( x ) x ) ]
#:inline-txexpr-proc [ inline-txexpr-proc ( λ ( x ) x ) ]
#:string-proc [ string-proc ( λ ( x ) x ) ]
#:entity-proc [ entity-proc ( λ ( x ) x ) ]
#:cdata-proc [ cdata-proc ( λ ( x ) x ) ]
#:exclude-tags [ excluded-tags ' ( ) ]
#:exclude-attrs [ excluded-attrs ' ( ) ] )
( ( txexpr-elements? )
( #:txexpr-tag-proc ( txexpr-tag? . -> . txexpr-tag? )
#:txexpr-attrs-proc ( txexpr-attrs? . -> . txexpr-attrs? )
#:txexpr-elements-proc ( txexpr-elements? . -> . txexpr-elements? )
#:block-txexpr-proc ( block-txexpr? . -> . decode-proc-output-contract )
#:inline-txexpr-proc ( txexpr? . -> . decode-proc-output-contract )
#:string-proc ( string? . -> . decode-proc-output-contract )
#:entity-proc ( ( or/c symbol? valid-char? ) . -> . decode-proc-output-contract )
#:cdata-proc ( cdata? . -> . decode-proc-output-contract )
#:exclude-tags ( listof txexpr-tag? )
#:exclude-attrs txexpr-attrs? ) . ->* . txexpr? )
( define temp-tag ( gensym " temp-tag " ) )
( define decode-result ( decode ` ( temp-tag ,@ elements )
#:txexpr-tag-proc txexpr-tag-proc
#:txexpr-attrs-proc txexpr-attrs-proc
#:txexpr-elements-proc txexpr-elements-proc
#:block-txexpr-proc block-txexpr-proc
#:inline-txexpr-proc inline-txexpr-proc
#:string-proc string-proc
#:entity-proc entity-proc
#:cdata-proc cdata-proc
#:exclude-tags excluded-tags
#:exclude-attrs excluded-attrs ) )
( get-elements decode-result ) )
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Blocks
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; initial set of block tags: from html
( define+provide project-block-tags
( make-parameter html:block-tags ) )
;; tags are inline unless they're registered as block tags.
( define+provide/contract ( block-txexpr? x )
( any/c . -> . boolean? )
( and ( txexpr? x ) ( member ( get-tag x ) ( project-block-tags ) ) #t ) )
( define+provide/contract ( register-block-tag tag )
( txexpr-tag? . -> . void? )
( project-block-tags ( cons tag ( project-block-tags ) ) ) )
( module-test-external
( check-true ( begin ( register-block-tag ' barfoo ) ( block-txexpr? ' ( barfoo " foo " ) ) ) ) )
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Typography
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
( define ( make-replacer query+replacement )
( let ( [ queries ( map car query+replacement ) ]
[ replacements ( map second query+replacement ) ] )
;; reverse because first in list should be first applied to str (and compose1 works right-to-left)
( apply compose1 ( reverse ( map ( λ ( query replacement ) ( λ ( str ) ( regexp-replace* query str replacement ) ) ) queries replacements ) ) ) ) )
( define+provide/contract ( smart-dashes str )
( string? . -> . string? )
( define dashes
;; fix em dashes first, else they'll be mistaken for en dashes
;; \\s is whitespace + #\u00A0 is nonbreaking space
' ( ( #px"[\\s#\u00A0]*(---|—)[\\s#\u00A0]*" " — " ) ; em dash
( #px"[\\s#\u00A0]*(--|– )[\\s#\u00A0]*" " – " ) ) ) ; en dash
( ( make-replacer dashes ) str ) )
( module-test-external
( check-equal? ( smart-dashes " I had --- maybe 13 -- 20 --- hob-nobs. " ) " I had—maybe 13– 20—hob-nobs. " )
( check-equal? ( smart-quotes " \" Why, \" she could've asked, \" are we in O‘ ahu watching 'Mame'? \" " )
" “Why,” she could’ ve asked, “are we in O‘ ahu watching ‘ Mame’ ?” " )
( check-equal? ( smart-quotes " \" \' Impossible. \' Yes. \" " ) " “‘ Impossible.’ Yes.” " ) )
( define+provide/contract ( smart-quotes str )
( string? . -> . string? )
( define quotes
' ( ( #px"(?<=\\w)'(?=\\w)" " ’ " ) ; apostrophe
( #px"(?<!\\w)'(?=\\S)" " ‘ " ) ; single_at_beginning
( #px"(?<=\\S)'(?!\\w)" " ’ " ) ; single_at_end
( #px"(?<!\\w)\"(?=\\S)" " “ " ) ; double_at_beginning
( #px"(?<=\\S)\"(?!\\w)" " ” " ) ) ) ; double_at_end
( ( make-replacer quotes ) str ) )
;; insert nbsp between last two words
( define+provide/contract ( nonbreaking-last-space x #:nbsp [ nbsp ( ->string #\u00A0 ) ]
#:minimum-word-length [ minimum-word-length 6 ]
#:last-word-proc [ last-word-proc ( λ ( x ) x ) ] )
( ( txexpr? ) ( #:nbsp string? #:minimum-word-length integer? #:last-word-proc procedure? ) . ->* . txexpr? )
;; todo: parameterize this, as it will be different for each project
( define tags-to-pay-attention-to ' ( p aside ) ) ; only apply to paragraphs
( define ( replace-last-space str )
( if ( #\space . in? . str )
( let ( [ reversed-str-list ( reverse ( string->list str ) ) ]
[ reversed-nbsp ( reverse ( string->list ( ->string nbsp ) ) ) ] )
( define-values ( last-word-chars other-chars )
( splitf-at reversed-str-list ( λ ( i ) ( not ( eq? i #\space ) ) ) ) )
( define front-chars ( if ( < ( len last-word-chars ) minimum-word-length ) ; OK for long words to be on their own line
; first char of other-chars will be the space, so use cdr
( string-append ( list->string ( reverse ( cdr other-chars ) ) ) ( ->string nbsp ) )
( list->string ( reverse other-chars ) ) ) )
( define last-word ( list->string ( reverse last-word-chars ) ) )
` ( , front-chars , ( last-word-proc last-word ) ) ) ; don't concatenate last word bc last-word-proc might be a txexpr wrapper
( list str ) ) )
( define ( find-last-word-space x ) ; recursively traverse xexpr
( cond
[ ( string? x ) ( replace-last-space x ) ] ; todo: this assumes a paragraph only has one string in it.
[ ( txexpr? x )
( let-values ( [ ( tag attr elements ) ( txexpr->values x ) ] )
( if ( > ( length elements ) 0 ) ; elements is list of xexprs
( let-values ( [ ( all-but-last last ) ( split-at elements ( sub1 ( length elements ) ) ) ] )
( define result ( find-last-word-space ( car last ) ) )
( define result-items ( if ( txexpr? result ) ( list result ) result ) ) ; might be txexpr, or list of new elements
( make-txexpr tag attr ` ( ,@ all-but-last ,@ result-items ) ) )
x ) ) ]
[ else x ] ) )
( if ( ( car x ) . in? . tags-to-pay-attention-to )
( find-last-word-space x )
x ) )
( module-test-external
;; todo: make some tougher tests, it gets flaky with edge cases
( check-equal? ( nonbreaking-last-space ' ( p " Hi there " ) ) ' ( p " Hi " " there " ) ) ; nbsp in between last two words
( check-equal? ( nonbreaking-last-space ' ( p " Hi there " ) #:nbsp " Ø " ) ' ( p " HiØ " " there " ) ) ; but let's make it visible
( check-equal? ( nonbreaking-last-space ' ( p " Hi there " ) #:nbsp " _up_ " ) ' ( p " Hi_up_ " " there " ) )
( check-equal? ( nonbreaking-last-space ' ( p " Hi there " ) #:nbsp " _up_ " #:minimum-word-length 3 )
' ( p " Hi " " there " ) )
( check-equal? ( nonbreaking-last-space ' ( p " Hi here " ( em " ho there " ) ) #:nbsp " Ø " ) ' ( p " Hi here " ( em " hoØ " " there " ) ) ) )
; wrap initial quotes for hanging punctuation
; todo: improve this
; does not handle <p>“<em>thing</em> properly
( define+provide/contract ( wrap-hanging-quotes nx
#:single-prepend [ single-pp ' ( squo ) ]
#:double-prepend [ double-pp ' ( dquo ) ] )
( ( txexpr? ) ( #:single-prepend list? #:double-prepend list? ) . ->* . txexpr? )
( define two-or-more-char-string? ( λ ( i ) ( and ( string? i ) ( >= ( len i ) 2 ) ) ) )
( define-values ( tag attr elements ) ( txexpr->values nx ) )
( make-txexpr tag attr
( if ( and ( list? elements ) ( not ( empty? elements ) ) )
( let ( [ new-car-elements ( match ( car elements )
[ ( ? two-or-more-char-string? tcs )
( define str-first ( get tcs 0 ) )
( define str-rest ( get tcs 1 ( len tcs ) ) )
( cond
[ ( str-first . in? . ' ( " \" " " “ " ) )
;; can wrap with any inline tag
;; so that linebreak detection etc still works
` ( ,@ double-pp , ( ->string #\“ ) , str-rest ) ]
[ ( str-first . in? . ' ( " \' " " ‘ " ) )
` ( ,@ single-pp , ( ->string #\‘ ) , str-rest ) ]
[ else tcs ] ) ]
[ ( ? txexpr? nx ) ( wrap-hanging-quotes nx ) ]
[ else ( car elements ) ] ) ] )
( cons new-car-elements ( cdr elements ) ) )
elements ) ) )
( module-test-external
( check-equal? ( wrap-hanging-quotes ' ( p " \" Hi \" there " ) ) ' ( p ( dquo " “ " " Hi \" there " ) ) )
( check-equal? ( wrap-hanging-quotes ' ( p " 'Hi' there " ) ) ' ( p ( squo " ‘ " " Hi' there " ) ) )
( check-equal? ( wrap-hanging-quotes ' ( p " 'Hi' there " ) #:single-prepend ' ( foo ( ( bar " ino " ) ) ) )
' ( p ( foo ( ( bar " ino " ) ) " ‘ " " Hi' there " ) ) )
;; make sure txexpr without elements passes through unscathed
( check-equal? ( wrap-hanging-quotes ' ( div ( ( style " height:2em " ) ) ) ) ' ( div ( ( style " height:2em " ) ) ) ) )
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Lines, blocks, paragraphs
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; turn the right items into <br> tags
( define+provide/contract ( detect-linebreaks xc
#:separator [ newline ( world:current-linebreak-separator ) ]
#:insert [ linebreak ' ( br ) ] )
( ( txexpr-elements? ) ( #:separator string? #:insert xexpr? ) . ->* . txexpr-elements? )
;; todo: should this test be not block + not whitespace?
( define not-block? ( λ ( i ) ( not ( block-txexpr? i ) ) ) )
( filter-not empty?
( for/list ( [ i ( in-range ( len xc ) ) ] )
( let ( [ item ( get xc i ) ] )
( cond
;; skip first and last
[ ( or ( = i 0 ) ( = i ( sub1 ( len xc ) ) ) ) item ]
[ ( equal? item newline )
( match ( get xc ( - i 1 ) ( + i 2 ) ) ; a three-element slice with x[i] in the middle
;; only convert if neither adjacent tag is a block
;; (because blocks automatically force a newline before & after)
[ ( list ( ? not-block? ) newline ( ? not-block? ) ) linebreak ]
[ else empty ] ) ] ; otherwise delete
[ else item ] ) ) ) ) )
( module-test-external
( check-equal? ( detect-linebreaks ' ( " foo " " \n " " bar " ) ) ' ( " foo " ( br ) " bar " ) )
( check-equal? ( detect-linebreaks ' ( " \n " " foo " " \n " " bar " " \n " ) ) ' ( " \n " " foo " ( br ) " bar " " \n " ) )
( check-equal? ( detect-linebreaks ' ( ( p " foo " ) " \n " ( p " bar " ) ) ) ' ( ( p " foo " ) ( p " bar " ) ) )
( check-equal? ( detect-linebreaks ' ( " foo " " \n " ( p " bar " ) ) ) ' ( " foo " ( p " bar " ) ) )
( check-equal? ( detect-linebreaks ' ( " foo " " moo " " bar " ) ) ' ( " foo " " moo " " bar " ) )
( check-equal? ( detect-linebreaks ' ( " foo " " moo " " bar " ) #:insert " moo " ) ' ( " foo " " moo " " bar " ) )
( check-equal? ( detect-linebreaks ' ( " foo " " \n \n " " bar " ) ) ' ( " foo " " \n \n " " bar " ) ) )
( define+provide/contract ( whitespace? x [ nbsp? #f ] )
( ( any/c ) ( boolean? ) . ->* . coerce/boolean? )
( define pat ( pregexp ( format " ^[ \\ s~a]+$ " ( if nbsp? #\u00A0 " " ) ) ) )
( cond
[ ( equal? " " x ) #t ] ; empty string is deemed whitespace
[ ( or ( string? x ) ( symbol? x ) ) ( regexp-match pat ( ->string x ) ) ]
[ ( or ( list? x ) ( vector? x ) ) ( and ( not ( empty? x ) ) ( andmap ( λ ( i ) ( whitespace? i nbsp? ) ) ( ->list x ) ) ) ] ; andmap returns #t for empty lists
[ else #f ] ) )
( module-test-external
( require racket/format )
( check-true ( whitespace? " " ) )
( check-false ( whitespace? ( ~a #\u00A0 ) ) )
( check-true ( whitespace/nbsp? ( ~a #\u00A0 ) ) )
( check-true ( whitespace/nbsp? ( vector ( ~a #\u00A0 ) ) ) )
( check-false ( whitespace? ( format " ~a " #\u00A0 ) ) )
( check-true ( whitespace/nbsp? ( format " ~a " #\u00A0 ) ) ) )
( define+provide/contract ( whitespace/nbsp? x )
( any/c . -> . coerce/boolean? )
( whitespace? x #t ) )
;; is x a paragraph break?
( define+provide/contract ( paragraph-break? x #:separator [ sep ( world:current-paragraph-separator ) ] )
( ( any/c ) ( #:separator pregexp? ) . ->* . coerce/boolean? )
( define paragraph-pattern ( pregexp ( format " ^~a+$ " sep ) ) )
( and ( string? x ) ( regexp-match paragraph-pattern x ) ) )
( define ( newline? x )
( and ( string? x ) ( equal? ( world:current-newline ) x ) ) )
( define ( not-newline? x )
( not ( newline? x ) ) )
( define ( do-merge xs [ acc ' ( ) ] )
( if ( empty? xs )
acc
;; Try to peel the newlines off the front.
( let-values ( [ ( leading-newlines remainder ) ( splitf-at xs newline? ) ] )
( if ( not ( empty? leading-newlines ) ) ; if you got newlines ...
;; combine them into a string and append them to the accumulator,
;; and recurse on the rest
( do-merge remainder ( append acc ( list ( apply string-append leading-newlines ) ) ) )
;; otherwise peel off elements up to the next newline, append them to accumulator,
;; and recurse on the rest
( do-merge ( dropf remainder not-newline? )
( append acc ( takef remainder not-newline? ) ) ) ) ) ) )
;; Find adjacent newline characters in a list and merge them into one item
;; Scribble, by default, makes each newline a separate list item
;; In practice, this is worthless.
( define+provide/contract ( merge-newlines x )
( txexpr-elements? . -> . txexpr-elements? )
( cond
[ ( list? x ) ( do-merge ( map merge-newlines x ) ) ]
[ else x ] ) )
( module-test-external
( check-equal? ( merge-newlines ' ( p " \n " " foo " " \n " " \n " " bar " ( em " \n " " \n " " \n " ) ) )
' ( p " \n " " foo " " \n \n " " bar " ( em " \n \n \n " ) ) ) )
;; detect paragraphs
;; todo: unit tests
( define+provide/contract ( detect-paragraphs elements #:tag [ tag ' p ]
#:separator [ sep ( world:current-paragraph-separator ) ]
#:linebreak-proc [ linebreak-proc detect-linebreaks ]
#:force? [ force-paragraph #f ] )
( ( txexpr-elements? ) ( #:tag symbol? #:separator string? #:linebreak-proc ( txexpr-elements? . -> . txexpr-elements? ) #:force? boolean? )
. ->* . txexpr-elements? )
;; prepare elements for paragraph testing
( define ( prep-paragraph-flow elems )
( linebreak-proc ( merge-newlines ( trimf elems whitespace? ) ) ) )
( define my-paragraph-break? ( λ ( x ) ( and ( paragraph-break? x #:separator sep ) #t ) ) )
( define ( wrap-paragraph elems )
( match elems
[ ( list ( ? block-txexpr? bxs ) ... ) bxs ] ; leave a series of block xexprs alone
[ else ( list ( make-txexpr tag empty elems ) ) ] ) ) ; otherwise wrap in p tag
( let ( [ elements ( prep-paragraph-flow elements ) ] )
( define explicit-or-implicit-paragraph-break? ( λ ( x ) ( or ( my-paragraph-break? x ) ( block-txexpr? x ) ) ) )
( if ( ormap explicit-or-implicit-paragraph-break? elements ) ; need this condition to prevent infinite recursion
;; use append-map on wrap-paragraph rather than map to permit return of multiple elements
( append-map wrap-paragraph ( append-map ( λ ( es ) ( filter-split es my-paragraph-break? ) ) ( slicef elements block-txexpr? ) ) ) ; split into ¶¶, using both implied and explicit paragraph breaks
( if force-paragraph
( append-map wrap-paragraph ( slicef elements block-txexpr? ) ) ; upconverts non-block elements to paragraphs
elements ) ) ) )
( module-test-external
( check-equal? ( detect-paragraphs ' ( " First para " " \n \n " " Second para " ) )
' ( ( p " First para " ) ( p " Second para " ) ) )
( check-equal? ( detect-paragraphs ' ( " First para " " \n \n " " Second para " " \n " " Second line " ) )
' ( ( p " First para " ) ( p " Second para " ( br ) " Second line " ) ) )
( check-equal? ( detect-paragraphs ' ( " First para " " \n \n " ( div " Second block " ) ) )
' ( ( p " First para " ) ( div " Second block " ) ) )
( check-equal? ( detect-paragraphs ' ( ( div " First block " ) " \n \n " ( div " Second block " ) ) )
' ( ( div " First block " ) ( div " Second block " ) ) )
( check-equal? ( detect-paragraphs ' ( " First para " " \n \n " " Second para " ) #:tag ' ns:p )
' ( ( ns:p " First para " ) ( ns:p " Second para " ) ) )
( check-equal? ( detect-paragraphs ' ( " First para " " \n \n " " Second para " " \n " " Second line " )
#:linebreak-proc ( λ ( x ) ( detect-linebreaks x #:insert ' ( newline ) ) ) )
' ( ( p " First para " ) ( p " Second para " ( newline ) " Second line " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " " \n \n " ( div " bar " ) ( div " zam " ) ) )
' ( ( p " foo " ) ( div " bar " ) ( div " zam " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " " \n \n " ( div " bar " ) " \n \n " ( div " zam " ) ) )
' ( ( p " foo " ) ( div " bar " ) ( div " zam " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " ) ) ' ( " foo " ) )
( check-equal? ( detect-paragraphs ' ( " foo " ) #:force? #t ) ' ( ( p " foo " ) ) )
( check-equal? ( detect-paragraphs ' ( ( div " foo " ) ) ) ' ( ( div " foo " ) ) )
( check-equal? ( detect-paragraphs ' ( ( div " foo " ) ) #:force? #t ) ' ( ( div " foo " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " " \n \n " ( div " bar " ) ) ) ' ( ( p " foo " ) ( div " bar " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " ( div " bar " ) ) ) ' ( ( p " foo " ) ( div " bar " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " ( div " bar " ) ) #:force? #t ) ' ( ( p " foo " ) ( div " bar " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " ( div " bar " ) " zam " ) ) ' ( ( p " foo " ) ( div " bar " ) ( p " zam " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " ( span " zing " ) ( div " bar " ) " zam " ) ) ' ( ( p " foo " ( span " zing " ) ) ( div " bar " ) ( p " zam " ) ) )
( check-equal? ( detect-paragraphs ' ( " foo " ( span " zing " ) ( div " bar " ) " zam " ) #:force? #t ) ' ( ( p " foo " ( span " zing " ) ) ( div " bar " ) ( p " zam " ) ) ) )