line & para breaks

main
Matthew Butterick 5 years ago
parent bb7f40f218
commit 40ab74badf

@ -1,6 +1,6 @@
#lang scribble/manual
@(require racket/runtime-path scribble/example (for-label txexpr (except-in pollen #%module-begin) xml racket/base racket/draw quadwriter)
@(require racket/runtime-path scribble/example quadwriter (for-label txexpr (except-in pollen #%module-begin) xml racket/base racket/draw quadwriter)
pollen/scribblings/mb-tools quad/pict)
@(define my-eval (make-base-eval))
@ -173,7 +173,7 @@ To see this:
()
(q
((page-margin-left "120") (page-margin-top "80") (page-margin-bottom "120") (font-family "default-serif") (line-height "17"))
(q ((break "paragraph")))
(q ((break "para")))
(q ((font-family "default-heading") (first-line-indent "0") (display "block") (font-size "20") (line-height "24.0") (border-width-top "0.5") (border-inset-top "9") (inset-bottom "-3") (inset-top "6") (keep-with-next "true") (id "did-you-know")) "Did you know?")
···
}
@ -315,8 +315,8 @@ Let's see how this works by doing document layout and rendering from within good
@codeblock|{
#lang racket/base
(require quadwriter)
(define qx '(q "Brennan likes fancy sauce."
(q ((break "paragraph")))
(define qx `(q "Brennan likes fancy sauce."
,para-break
"Dale hates fancy sauce."))
(define pdf-path "~/Desktop/new.pdf")
(render-pdf qx pdf-path)
@ -345,22 +345,20 @@ Then we add a simple @racket["pollen.rkt"] that converts the output of our sourc
@fileblock["pollen.rkt"
@codeblock|{
#lang racket
(require pollen/decode)
(provide (all-defined-out))
(require pollen/decode quadwriter)
(provide root render-pdf)
(define (root . xs)
`(q ,@(add-between (decode-paragraphs xs 'q)
'(q ((break "paragraph"))))))
`(q ,@(add-between (decode-paragraphs xs 'q) para-break)))
}|
]
All we're doing here is wrapping our paragraphs in @racket[q] tags (rather than the default @racket[p] tags) and then adding explicit Quadwriter paragraph breaks between them (= @racket['(q ((break "paragraph")))]).
All we're doing here is wrapping our paragraphs in @racket[q] tags (rather than the default @racket[p] tags) and then adding explicit Quadwriter paragraph breaks between them (see @racket[para-break]).
Finally, we add a @racket["template.pdf.p"] that passes the @racket[doc] from the Pollen source to @racket[render-pdf]:
@fileblock["template.pdf.p"
@codeblock|{
◊(local-require quadwriter)
◊(render-pdf doc #false)
}|
]
@ -391,43 +389,59 @@ Every source file written in a @racketmodname[quadwriter] dialect exports an ide
@subsection{Q-expressions}
A Q-expression is an @seclink["X-expressions" #:doc '(lib "pollen/scribblings/pollen.scrbl")]{X-expression} with some extra restrictions.
A Q-expression is an @seclink["X-expressions" #:doc '(lib "pollen/scribblings/pollen.scrbl")]{X-expression}, but more restricted:
@racketgrammar[
#:literals (list q)
qexpr string
(list q (list (list attr-name attr-val) ...) qexpr ...)
(list q (list qexpr ...))
]
@margin-note{Because Q-expressions are a subset of X-expressions, you can apply any tools that work with X-expressions (for instance, the @racketmodname[txexpr] library).}
This grammar means that a Q-expression is either a) a string, b) an X-expression whose tag is @racket[q] and whose elements are themselves Q-expressions.
@examples[#:eval my-eval
(qexpr? "Hello world")
(qexpr? '(q "Hello world"))
(qexpr? '(q () "Hello world"))
(qexpr? '(q ((font-color "pink")) "Hello world"))
(qexpr? '(q ((font-color "pink")) (q "Hello world")))
(code:comment @#,t{malformed Q-expressions})
(qexpr? 42)
(qexpr? '(div "Hello world"))
(qexpr? '(q (("pink" font-color)) "Hello world"))
]
@subsection{Fonts}
Because Q-expressions are a subset of X-expressions, you can apply any tools that work with X-expressions (for instance, the @racketmodname[txexpr] library).
A design goal of Quadwriter is to treat document layout as the result of a program. Along those lines, fonts are handled differently than usual. When you use a word processor, you choose from whatever fonts might be installed on your system.
@margin-note{Unlike X-expressions, Q-expressions do not support character entities or CDATA, because those are inherent to XML-ish markup.}
Quadwriter, by contrast, relies only on fonts that are @emph{in the same directory} as your other project source files. This is a feature: it means that everything necessary to render the document travels together in the same directory. You can re-render it anywhere with identical results. You never have the problem — still with us after 35 years of desktop word processing — that ``oh, you need to install such-and-such font in your system before it will work.'' Bah!
Quadwriter supports the usual TrueType (.ttf) and OpenType (.otf) font files. To add fonts to your Quadwriter experience:
@itemlist[#:style 'ordered
@subsection{Markup}
@item{Within your project directory, create a subdirectory called @racket["fonts"].}
@subsection{Hard breaks}
@item{Within @racket["fonts"], create a subdirectory for each font family you want to use in your Quadwriter document. The names of these subdirectories will become the acceptable values for the @racket[font-family] attribute in your documents.}
@item{If there is only one font file in the family subdirectory, then it is used every time the font family is requested.}
@deftogether[(@defthing[line-break qexpr?]
@defthing[page-break qexpr?])]{
The Q-expressions @racketresult['#,line-break] and @racketresult['#,page-break], respectively. Quadwriter will automatically insert line breaks and page breaks as needed. But you can also add them explicitly (aka ``hard'' breaks) by inserting the Q-expression denoting the break.
}
@item{Alternatively, you can specify styled variants by creating within the family directory style subdirectories called @racket["regular"], @racket["bold"], @racket["italic"], and @racket["bold-italic"].}
]
Though this system may seem like a lot of housekeeping, it's nice for two reasons. First, we use the filesystem to map font names to font files, and avoid having another configuration file floating around our project. Second, we create a layer of abstraction between font names and files. This makes it easy to change the fonts in the document: you just put new fonts in the appropriate font-family directory, and you don't need to faff about with the source file itself.
@defthing[para-break qexpr?]{
The Q-expression @racketresult['#,para-break]. Used to denote the start of a new paragraph.
}
TK: example of font setup
@subsection{Markup}
@subsection{Attributes}
These are the attributes that can be used inside a Q-expression passed to @racketmodname[quadwriter]. Inside a Q-expression, every attribute is a @tech{symbol}, and every attribute value is a @tech{string}.
A @deftech{dimension string} represents a distance in the plane. If unitless, it is treated as points (where 1 point = 1/72 of an inch). If the number has @racket[in], @racket[cm], or @racket[mm] as a suffix, it is treated as inches, centimeters, or millimeters respectively.
@subsection{Page-level attributes}
@subsubsection{Page-level attributes}
@deftogether[(@defthing[#:kind "attribute" page-size symbol?]
@defthing[#:kind "attribute" page-orientation symbol?])]{
@ -448,7 +462,7 @@ Inset values from the page edges. Value is given as a @tech{dimension string}. D
}
@subsection{Block-level attributes}
@subsubsection{Block-level attributes}
A block is a paragraph or other rectangular item (say, a blockquote or code block) with paragraph breaks around it.
@ -493,7 +507,7 @@ How many lines of the quad are kept together near a page break. @racket[keep-fir
}
@defthing[#:kind "attribute" keep-with-next symbol?]{
Whether a quad appears on the same page with the following quad. Activated only when value is @racket["true"]. Essentially this is the "nonbreaking paragraph space".
Whether a quad appears on the same page with the following quad. Activated only when value is @racket["true"]. Essentially this is the ``nonbreaking paragraph space''.
}
@deftogether[(@defthing[#:kind "attribute" line-align symbol?]
@ -514,7 +528,7 @@ Selects the linebreak algorithm. A value of @racket["best"] or @racket["kp"] inv
Whether the block is hyphenated. Activated only when value is @racket["true"].
}
@subsection{Other attributes}
@subsubsection{Other attributes}
@deftogether[(@defthing[#:kind "attribute" font-size symbol?]
@defthing[#:kind "attribute" font-size-adjust symbol?])]{
@ -557,6 +571,30 @@ Compute the layout for @racket[qx] and render it as a PDF to @racket[pdf-path].
The optional @racket[replace?] argument controls whether an existing file is automatically overwritten. The default is @racket[#true].
}
@subsection{Fonts}
A design goal of Quadwriter is to treat document layout as the result of a program. Along those lines, fonts are handled differently than usual. When you use a word processor, you choose from whatever fonts might be installed on your system.
Quadwriter, by contrast, relies only on fonts that are @emph{in the same directory} as your other project source files. This is a feature: it means that everything necessary to render the document travels together in the same directory. You can re-render it anywhere with identical results. You never have the problem — still with us after 35 years of desktop word processing — that ``oh, you need to install such-and-such font in your system before it will work.'' Bah!
Quadwriter supports the usual TrueType (.ttf) and OpenType (.otf) font files. To add fonts to your Quadwriter experience:
@itemlist[#:style 'ordered
@item{Within your project directory, create a subdirectory called @racket["fonts"].}
@item{Within @racket["fonts"], create a subdirectory for each font family you want to use in your Quadwriter document. The names of these subdirectories will become the acceptable values for the @racket[font-family] attribute in your documents.}
@item{If there is only one font file in the family subdirectory, then it is used every time the font family is requested.}
@item{Alternatively, you can specify styled variants by creating within the family directory style subdirectories called @racket["regular"], @racket["bold"], @racket["italic"], and @racket["bold-italic"].}
]
Though this system may seem like a lot of housekeeping, it's nice for two reasons. First, we use the filesystem to map font names to font files, and avoid having another configuration file floating around our project. Second, we create a layer of abstraction between font names and files. This makes it easy to change the fonts in the document: you just put new fonts in the appropriate font-family directory, and you don't need to faff about with the source file itself.
TK: example of font setup
@subsection{Utility}
@defproc[

@ -15,7 +15,7 @@
"attrs.rkt"
"param.rkt"
"font.rkt")
(provide qexpr-para-break qexpr-line-break hrbr lbr pbr render-pdf)
(provide para-break line-break page-break hrbr lbr pbr render-pdf)
(define-quad string-quad quad ())
@ -78,7 +78,7 @@
(define (->string-quad q)
(cond
[(line-break? q) q]
[(q:line-break? q) q]
[else
(struct-copy
quad q:string
@ -162,21 +162,21 @@
[size (make-size-promise last-q str+hyphen)])))]
[_ qs]))
(define-quad line-break quad ())
(define lbr (make-line-break #:printable #f
#:id 'lbr))
(define-quad q:line-break quad ())
(define lbr (make-q:line-break #:printable #f
#:id 'lbr))
;; treat paragraph break as special kind of line break
(define-quad para-break line-break ())
(define pbr (make-para-break #:printable #f
#:id 'pbr))
(define-quad hr-break line-break ())
(define hrbr (make-hr-break #:printable #t
#:id 'hrbr))
(define-quad q:para-break q:line-break ())
(define pbr (make-q:para-break #:printable #f
#:id 'pbr))
(define-quad q:hr-break q:line-break ())
(define hrbr (make-q:hr-break #:printable #t
#:id 'hrbr))
(module+ test
(require rackunit)
(check-true (line-break? (second (quad-elems (q "foo" pbr "bar")))))
(check-true (line-break? (second (atomize (q "foo" pbr "bar"))))))
(check-true (q:line-break? (second (quad-elems (q "foo" pbr "bar")))))
(check-true (q:line-break? (second (atomize (q "foo" pbr "bar"))))))
(define (copy-block-attrs source-hash dest-hash)
(for* ([k (in-list block-attrs)]
@ -278,7 +278,7 @@
(define new-lines
(cond
[(empty? pcs-printing) null]
[(hr-break? ending-q) (list (make-hr-quad line-q))]
[(q:hr-break? ending-q) (list (make-hr-quad line-q))]
[else
;; render hyphen first so that all printable characters are available for size-dependent ops.
(define pcs-with-hyphen (render-hyphen pcs-printing ending-q))
@ -347,13 +347,13 @@
[size (pt wrap-size (pt-y (size q:line)))]))
(apply append
;; next line removes all para-break? quads as a consequence
(for/list ([qs (in-list (filter-split qs para-break?))])
(for/list ([qs (in-list (filter-split qs q:para-break?))])
(wrap qs
(λ (q idx) (- wrap-size (quad-ref q 'inset-left 0) (quad-ref q 'inset-right 0)))
#:nicely (match (or (current-line-wrap) (and (pair? qs) (quad-ref (car qs) 'line-wrap)))
[(or "best" "kp") #true]
[_ #false])
#:hard-break line-break?
#:hard-break q:line-break?
#:soft-break soft-break-for-line?
#:finish-wrap (finish-line-wrap line-q)))))
@ -546,13 +546,13 @@
;; stick a pbr on the front if there isn't one already
;; because of the "lookahead" style of iteration
(define qs (match qs-in
[(list (? para-break?) _ ...) qs-in]
[(list (? q:para-break?) _ ...) qs-in]
[_ (cons pbr qs-in)]))
(for/fold ([qs-out null]
#:result (reverse qs-out))
([q (in-list qs)]
[next-q (in-list (cdr qs))])
(match (and (para-break? q) (quad-ref next-q 'first-line-indent 0))
(match (and (q:para-break? q) (quad-ref next-q 'first-line-indent 0))
[(or #false 0) (cons next-q qs-out)]
[indent-val (list* next-q (make-quad #:from 'bo
#:to 'bi
@ -562,14 +562,15 @@
#:size (pt indent-val 10)) qs-out)])))
(define qexpr-para-break '(q ((break "paragraph"))))
(define qexpr-line-break '(q ((break "line"))))
(define para-break '(q ((break "para"))))
(define line-break '(q ((break "line"))))
(define page-break '(q ((break "page"))))
(define (replace-breaks x)
(map-elements (λ (el)
(match el
[(== qexpr-para-break) pbr]
[(== qexpr-line-break) lbr]
[(== para-break) pbr]
[(== line-break) lbr]
[_ el])) x))
(require racket/contract sugar/coerce pitfall/page)
@ -642,7 +643,8 @@
[qs (time-name page-wrap (page-wrap qs page-wrap-size page-quad))]
[qs (time-name position (position (struct-copy quad q:doc [elems qs])))])
(time-name draw (draw qs pdf))
(displayln (format "wrote PDF to ~a" pdf-path))))
(when pdf-path-arg
(displayln (format "wrote PDF to ~a" pdf-path)))))
(unless pdf-path-arg
(begin0

@ -23,9 +23,9 @@
;; markdown parser returns list of paragraphs
(root null (match strs
[(list str) strs]
[_ (add-between strs (list qexpr-para-break)
#:before-first (list qexpr-para-break)
#:after-last (list qexpr-para-break)
[_ (add-between strs (list para-break)
#:before-first (list para-break)
#:after-last (list para-break)
#:splice? #true)])))
(make-module-begin doc-proc)

@ -80,7 +80,7 @@
(for*/list ([expr (in-list exprs)]
[str (in-list (string-split (string-join (get-elements expr) "") "\n"))])
`(,(get-tag expr) ,(get-attrs expr) ,(string-replace str " " " ")))
qexpr-line-break))
line-break))
(qexpr (list* '(display "block") '(background-color "aliceblue")
'(first-line-indent "0")
'(font-family "default-mono") '(font-size "11") '(line-height "14")
@ -99,7 +99,7 @@
(for/list ([(expr idx) (in-indexed exprs)]
#:when (txexpr? expr))
(list* (get-tag expr) (cons (list 'list-index (or bullet-val (format "~a" (add1 idx)))) (get-attrs expr)) (get-elements expr)))
qexpr-para-break)))
para-break)))
(define-tag-function (ol attrs exprs) (list-base attrs exprs))
(define-tag-function (ul attrs exprs) (list-base attrs exprs ""))

Loading…
Cancel
Save