diff --git a/pollen/core.rkt b/pollen/core.rkt new file mode 100644 index 0000000..dfa3a94 --- /dev/null +++ b/pollen/core.rkt @@ -0,0 +1,124 @@ +#lang racket/base +(require (for-syntax racket/base "world.rkt")) +(require txexpr xml/path sugar/define sugar/coerce sugar/test racket/string) +(require "private/file-utils.rkt" + "world.rkt" + "cache.rkt" + "pagetree.rkt" + "private/to-string.rkt") + +(define is-meta-value? hash?) +(define is-doc-value? txexpr?) +(define identity (λ(x) x)) +(define not-false? identity) + +(define+provide define-meta identity) ;; stub so it will be picked up for docs + + +(define+provide/contract (select* key value-source) + (coerce/symbol? (or/c is-meta-value? is-doc-value? pagenode? pathish?) . -> . (or/c #f txexpr-elements?)) + (define metas-result (and (not (is-doc-value? value-source)) (select-from-metas key value-source))) + (define doc-result (and (not (is-meta-value? value-source)) (select-from-doc key value-source))) + (define result (filter not-false? (apply append (map ->list (list metas-result doc-result))))) + (and (pair? result) result)) + + +(define+provide/contract (select key value-source) + (coerce/symbol? (or/c is-meta-value? is-doc-value? pagenode? pathish?) . -> . (or/c #f txexpr-element?)) + (define result (select* key value-source)) + (and (pair? result) (car result))) + + +(module-test-external + (check-equal? (select* 'key '#hash((key . "value"))) '("value")) + (check-equal? (select 'key '#hash((key . "value"))) "value") + (check-false (select* 'absent-key '#hash((key . "value")))) + (check-false (select 'absent-key '#hash((key . "value")))) + (check-equal? (select* 'key '(root (key "value"))) '("value")) + (check-equal? (select 'key '(root (key "value"))) "value") + (check-false (select* 'absent-key '(root (key "value")))) + (check-false (select 'absent-key '(root (key "value")))) + (let ([metas '#hash((key . "value"))]) + (check-equal? (select* 'key metas) '("value")) + (check-equal? (select 'key metas) "value") + (check-false (select* 'absent-key metas)) + (check-false (select 'absent-key metas))) + (let ([doc '(root (key "value"))]) + (check-equal? (select* 'key doc) '("value")) + (check-equal? (select 'key doc) "value") + (check-false (select* 'absent-key doc)) + (check-false (select 'absent-key doc)))) + + +(define+provide/contract (select-from-metas key metas-source) + ;; output contract is a single txexpr-element + ;; because metas is a hash, and a hash has only one value for a key. + (coerce/symbol? (or/c is-meta-value? pagenode? pathish?) . -> . (or/c #f txexpr-element?)) + (define metas (if (is-meta-value? metas-source) + metas-source + (get-metas metas-source))) + (and (hash-has-key? metas key) (hash-ref metas key))) + +(module-test-external + (let ([metas '#hash((key . "value"))]) + (check-equal? (select-from-metas 'key metas) "value") + (check-false (select-from-metas 'absent-key metas)))) + + +(define+provide/contract (select-from-doc key doc-source) + ;; output contract is a list of elements + ;; because doc is a txexpr, and a txexpr can have multiple values for a key + (coerce/symbol? (or/c is-doc-value? pagenode? pathish?) . -> . (or/c #f txexpr-elements?)) + (define doc (if (is-doc-value? doc-source) + doc-source + (get-doc doc-source))) + (define result (se-path*/list (list key) doc)) + (and (pair? result) result)) + +(module-test-external + (check-equal? (select-from-doc 'key '(root (key "value"))) '("value")) + (check-false (select-from-doc 'absent-key '(root (key "value")))) + (let ([doc '(root (key "value"))]) + (check-equal? (select-from-doc 'key doc) '("value")) + (check-false (select-from-doc 'absent-key doc)))) + + +(define (convert+validate-path pagenode-or-path caller) + (let ([path (get-source (if (pagenode? pagenode-or-path) + (build-path (world:current-project-root) (symbol->string pagenode-or-path)) + pagenode-or-path))]) + (unless path + (error (format "~a no source found for '~a' in directory ~a" caller path (current-directory)))) + path)) + + +(define+provide/contract (get-metas pagenode-or-path) + ((or/c pagenode? pathish?) . -> . is-meta-value?) + (cached-metas (convert+validate-path pagenode-or-path 'get-metas))) + + +(define+provide/contract (get-doc pagenode-or-path) + ((or/c pagenode? pathish?) . -> . (or/c is-doc-value? string?)) + (cached-doc (convert+validate-path pagenode-or-path 'get-doc))) + + +(provide when/splice) +(define-syntax (when/splice stx) + (syntax-case stx () + [(_ COND BODY ...) + (with-syntax ([SPLICING-TAG (datum->syntax stx (world:current-splicing-tag))]) + #'(if COND + (with-handlers ([exn:fail? (λ(exn) (error (format "within when/block, ~a" (exn-message exn))))]) + (list 'SPLICING-TAG BODY ...)) + ""))])) + +(provide when/block) ; bw compat +(define-syntax (when/block stx) + (syntax-case stx () + [(_ condition body ...) + #'(if condition (string-append* + (with-handlers ([exn:fail? (λ(exn) (error (format "within when/block, ~a" (exn-message exn))))]) + (map to-string (list body ...)))) + "")])) + + diff --git a/pollen/decode.rkt b/pollen/decode.rkt index 59fc413..0c874bc 100644 --- a/pollen/decode.rkt +++ b/pollen/decode.rkt @@ -154,6 +154,7 @@ ;; Find adjacent newline characters in a list and merge them into one item ;; Scribble, by default, makes each newline a separate list item. +;; Ignore empty strings. (define+provide/contract (merge-newlines x) (txexpr-elements? . -> . txexpr-elements?) (define newline-pat (regexp (format "^~a+$" (world:current-newline)))) @@ -161,16 +162,18 @@ (define (merge-if-newlines xs) (if (newlines? (car xs)) (list (apply string-append xs)) - xs)) + xs)) + (define not-empty-string? (λ(x) (not (and (string? x) (= (string-length x) 0))))) (let loop ([x x]) (if (pair? x) - (let ([xs (map loop x)]) + (let ([xs (map loop (filter not-empty-string? x))]) (append-map merge-if-newlines (slicef xs newlines?))) x))) (module-test-external (require racket/list) (check-equal? (merge-newlines empty) empty) + (check-equal? (merge-newlines '(p "\n" "" "\n")) '(p "\n\n")) (check-equal? (merge-newlines '(p "\n" "\n" "foo" "\n" "\n\n" "bar" (em "\n" "\n" "\n"))) '(p "\n\n" "foo" "\n\n\n" "bar" (em "\n\n\n")))) diff --git a/pollen/private/main-base.rkt b/pollen/private/main-base.rkt index 2dfdf40..f509665 100644 --- a/pollen/private/main-base.rkt +++ b/pollen/private/main-base.rkt @@ -31,9 +31,9 @@ (module inner pollen/private/doclang-raw DOC-RAW ; positional arg for doclang-raw that sets name of export. - (require pollen/top pollen/world) + (require pollen/top pollen/world pollen/core) (require (submod ".." META-MOD)) - (provide (all-defined-out) #%top (all-from-out (submod ".." META-MOD))) + (provide (all-defined-out) #%top (all-from-out (submod ".." META-MOD) pollen/core)) EXPR-WITHOUT-METAS (... ...)) (require 'inner) diff --git a/pollen/private/splice.rkt b/pollen/private/splice.rkt index 357fbad..b6dc9c0 100644 --- a/pollen/private/splice.rkt +++ b/pollen/private/splice.rkt @@ -2,19 +2,21 @@ (provide (all-defined-out)) (define (splice x [splicing-tag '@]) + ; (listof txexpr-elements?) . -> . (listof txexpr-elements?)) + (define spliceable? (λ(x) (and (pair? x) (eq? (car x) splicing-tag)))) + (define not-null-string? (λ(x) (not (and (string? x) (= (string-length x) 0))))) (let loop ([x x]) - (if (list? x) - (apply append - (map (λ(xi) (let ([proc (if (and (pair? xi) (eq? (car xi) splicing-tag)) - cdr ; expose elements - list)]) ; wrap in list - (proc (loop xi)))) x)) - x))) + (if (list? x) + (apply append (map (λ(x) ((if (spliceable? x) + cdr + list) (loop x))) (filter not-null-string? x))) + x))) (module+ test (require rackunit) - (check-equal? (splice '(@ 1 (@ 2 (@ 3 (div 4 (@ 5))) 6) 7)) - '(@ 1 2 3 (div 4 5) 6 7)) - (check-equal? (splice '((@ "foo" "bar"))) '("foo" "bar")) - (check-equal? (splice '(@ "foo" "bar")) '(@ "foo" "bar")) ; this is correct, for composable behavior + (check-equal? (splice '((div 1 (@ 2 "" (@ 3 (div 4 (@ 5))) 6) "" 7))) + '((div 1 2 3 (div 4 5) 6 7))) + (check-equal? (splice '((@ 1 (@ 2 "" (@ 3 (div 4 (@ 5))) 6) "" 7))) + '(1 2 3 (div 4 5) 6 7)) + (check-equal? (splice '((@ "foo" "" "bar"))) '("foo" "bar")) (check-equal? (splice null) null)) \ No newline at end of file diff --git a/pollen/render.rkt b/pollen/render.rkt index cdd6148..cc59570 100644 --- a/pollen/render.rkt +++ b/pollen/render.rkt @@ -8,6 +8,7 @@ "private/cache-utils.rkt" "pagetree.rkt" "template.rkt" + "core.rkt" "private/rerequire.rkt" "world.rkt") @@ -180,7 +181,7 @@ (parameterize ([current-pagetree (make-project-pagetree ,(world:current-project-root))]) (let ([,(world:current-main-export) (cached-doc ,(path->string source-path))] [,(world:current-meta-export) (cached-metas ,(path->string source-path))]) - (local-require pollen/template pollen/top) + (local-require pollen/template pollen/top pollen/core) (define here (path->pagenode (or (select-from-metas ',(world:current-here-path-key) ,(world:current-meta-export)) 'unknown))) (cond diff --git a/pollen/scribblings/command.html b/pollen/scribblings/command.html deleted file mode 100644 index 5ecf240..0000000 --- a/pollen/scribblings/command.html +++ /dev/null @@ -1,3 +0,0 @@ - -Pollen command syntax
Pollen command syntax
1 The golden rule
2 The lozenge glyph (◊)
2.1 Lozenge helpers
2.1.1 AHK script
2.1.2 Emacs script
3 The two command modes:   Pollen mode & Racket mode
3.1 The command name
3.1.1 Invoking tag functions
3.1.2 Invoking other functions
3.1.3 Inserting the value of a variable
3.1.4 Inserting metas
3.1.5 Retrieving metas
3.1.6 Inserting a comment
3.2 The Racket arguments
3.3 The text argument
4 Embedding character entities
5 Adding Pollen-mode commands to a Racket file
6 Further reading
6.2.900.15

Pollen command syntax

1 The golden rule

Pollen uses a special character — the lozenge, which looks like this: ◊ — to mark commands within a Pollen source file. So when you put a ◊ in your source, whatever comes next will be treated as a command. If you don’t, it will just be interpreted as plain text.

2 The lozenge glyph (◊)

I chose the lozenge as the command character because a) it appears in almost every font, b) it’s barely used in ordinary typesetting, c) it’s not used in any programming language that I know of, and d) its shape and color allow it to stand out easily in code without being distracting.

Here’s how you type it:

Mac: option + shift + V

Windows: holding down alt, type 9674 on the num pad

Ubuntu: ctrl + shift + U, then 25CA

Still, if you don’t want to use the lozenge as your command character, you can set Pollen’s world:command-char value to whatever character you want (see also (part "settable-values")).

Scribble uses the @ sign as a delimiter. It’s not a bad choice if you only work with Racket files. But as you use Pollen to work on other kinds of text-based files that commonly contain @ signs — HTML pages especially — it gets cumbersome. So I changed it.

But don’t knock the lozenge till you try it.

2.1 Lozenge helpers

2.1.1 AHK script

Courtesy of Matt Wilkie: “Here’s a working AHK script to have double-tap backtick send the lozenge character. It took way more time than I want to think about, once started I couldn’t let go.”

"tick-tick-lozenge.ahk"

;

; Double-tap backtick sends Pollen command character (◊)

; For some reason I needed to use Unicode 'U+25CA'

; instead of the standard alt-code '9674'

;

; Adapted from http://ahkscript.org/docs/commands/SetTimer.htm

; and http://www.autohotkey.com/board/topic/145507-how-to-use-double-tap-backtick-with-pass-through/

;

#EscapeChar \

$`::

if winc_presses > 0

    {winc_presses += 1

    return

    }

 

winc_presses = 1

SetTimer, TheKey, 250 ; Wait for more presses, in milliseconds

return

 

TheKey:

    SetTimer, TheKey, off

    if winc_presses = 1 ; The key was pressed once.

        {Send `

        }

    else if winc_presses = 2 ; The key was pressed twice.

        {Send {U+25CA}

        }

 

; Regardless of which action above was triggered, reset the count to

; prepare for the next series of presses:

winc_presses = 0

return

2.1.2 Emacs script

Courtesy of Richard Le: “If you’re using Emacs, I tried to write a tiny function that inserts the lozenge character. I chose M-\ because that’s the key for the lambda character in DrRacket.”

;; Put this in your Emacs .init file:

;; enable lozenge for Pollen

;; ◊◊◊◊◊◊◊◊◊◊◊◊◊

;; 'mule-unicode part from

;; https://lists.gnu.org/archive/html//emacs-devel/2005-03/msg01187.html

(defun insert-lozenge ()

  "inserts the lozenge character for use with Pollen"

  ;; enables function through M-x

  (interactive)

  ;; insert the proper character

  (insert (make-char

           'mule-unicode-2500-33ff 34 42)))

 

;; Bind key to M-\ a la DrRacket for lambda

(global-set-key "\M-\\" 'insert-lozenge)

3 The two command modes: Pollen mode & Racket mode

Pollen commands can be entered in one of two modes: Pollen mode or Racket mode. Both modes start with a lozenge ():

 command name [ Racket arguments ... ] { text argument }
 ( Racket expression )

Pollen-mode commands

A Pollen-mode command has the three possible parts after the :

Each of the three parts is optional. You can also nest commands within each other. However:

Here are a few examples of correct Pollen-mode commands:

#lang pollen
variable-name
tag{Text inside the tag.}
tag['attr: "value"]{Text inside the tag}
get-customer-id["Brennan Huff"]
tag{His ID is get-customer-id["Brennan Huff"].}

And some incorrect examples:

#lang pollen
tag {Text inside the tag.} ; space between first and second parts
tag[Text inside the tag] ; text argument needs to be within braces
tag{Text inside the tag}['attr: "value"] ; wrong order

The next section describes each of these parts in detail.

Racket-mode commands

If you’re familiar with Racket expressions, you can use the Racket-mode commands to embed them within Pollen source files. It’s simple: any Racket expression can become a Pollen command by adding to the front. So in Racket, this code:

#lang racket
(define song "Revolution")
(format "~a #~a" song (* 3 3))

Can be converted to Pollen like so:

#lang pollen
(define song "Revolution")
(format "~a #~a" song (* 3 3))

And in DrRacket, they produce the same output:

Revolution #9

Beyond that, there’s not much to say about Racket mode — any valid expression you can write in Racket will also be a valid Racket-mode Pollen command.

The relationship of Pollen mode and Racket mode

Even if you don’t plan to write a lot of Racket-mode commands, you should be aware that under the hood, Pollen is converting all commands in Pollen mode to Racket mode. So a Pollen-mode command that looks like this:

headline[#:size 'enormous]{Man Bites Dog!}

Is actually being turned into a Racket-mode command like this:

(headline #:size 'enormous "Man Bites Dog!")

Thus a Pollen-mode command is just an alternate way of writing a Racket-mode command. (More broadly, all of Pollen is just an alternate way of using Racket.)

The corollary is that you can always write Pollen commands using whichever mode is more convenient or readable. For instance, the earlier example, written in the Racket mode:

#lang pollen
(define song "Revolution")
(format "~a #~a" song (* 3 3))

Can be rewritten using Pollen mode:

#lang pollen
define[song]{Revolution}
format["~a #~a" song (* 3 3)]

And it will work the same way.

3.1 The command name

In Pollen, you’ll typically use the command name for one of four purposes:

3.1.1 Invoking tag functions

By default, Pollen treats every command name as a tag function. The default tag function creates a tagged X-expression with the command name as the tag, and the text argument as the content.

#lang pollen
strong{Fancy Sauce, $1}

'(strong "Fancy Sauce, $1")

To streamline markup, Pollen doesn’t restrict you to a certain set of tags, nor does it make you define your tags ahead of time. Just type a tag, and you can start using it.

#lang pollen
utterlyridiculoustagname{Oh really?}

'(utterlyridiculoustagname "Oh really?")

The one restriction is that you can’t invent names for tags that are already being used for other commands. For instance, map is a name permanently reserved by the Racket function map. It’s also a rarely-used HTML tag. But gosh, you really want to use it. Problem is, if you invoke it directly, Pollen will think you mean the other map:

#lang pollen
map{Fancy Sauce, $1}

map: arity mismatch;
the expected number of arguments does not match the given number
  given: 1
  arguments...:
    "Fancy Sauce, $1"

What to do? Read on.

3.1.2 Invoking other functions

Though every command name starts out as a default tag function, it doesn’t necessarily end there. You have two options for invoking other functions: defining your own, or invoking others from Racket.

Defining your own functions

Use the define command to create your own function for a command name. After that, when you use the command name, you’ll get the new behavior. For instance, recall this example showing the default tag-function behavior:

#lang pollen
strong{Fancy Sauce, $1}

'(strong "Fancy Sauce, $1")

We can define strong to do something else, like add to the text:

#lang pollen
(define (strong text) `(strong ,(format "Hey! Listen up! ~a" text)))
strong{Fancy Sauce, $1}

'(strong "Hey! Listen up! Fancy Sauce, $1")

The replacement function has to accept any arguments that might get passed along, but it doesn’t have to do anything with them. For instance, this function definition won’t work because strong is going to get a text argument that it’s not defined to handle:

#lang pollen
(define (strong) '(fib "1 1 2 3 5 8 13 ..."))
strong{Fancy Sauce, $1}

strong: arity mismatch;
the expected number of arguments does not match the given number
  expected: 0
  given: 1
  arguments...:
    "Fancy Sauce, $1"

Whereas in this version, strong accepts an argument called text, but then ignores it:

#lang pollen
(define (strong text) '(fib "1 1 2 3 5 8 13 ..."))
strong{Fancy Sauce, $1}

'(fib "1 1 2 3 5 8 13 ...")

You can attach any behavior to a command name. As your project evolves, you can also update the behavior of a command name. In that way, Pollen commands become a set of hooks to which you can attach more elaborate processing.

Using Racket functions

You aren’t limited to functions you define. Any function from Racket, or any Racket library, can be invoked directly by using it as a command name. Here’s the function range, which creates a list of numbers:

#lang pollen
range[1 20]

'(range 1 20)

Hold on — that’s not what we want. Where’s the list of numbers? The problem here is that we didn’t explicitly import the racket/list library, which contains the definition for range. (If you need to find out what library contains a certain function, the Racket documentation will tell you.) Without racket/list, Pollen just thinks we’re trying to use range as a tag function (and if we had been, then

'(range 1 20)

would’ve been the right result).

We fix this by using the require command to bring in the racket/list library, which contains the range we want:

#lang pollen
(require racket/list)
range[1 20]

'(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19)

Of course, you can also invoke Racket functions indirectly, by attaching them to functions you define for command names:

#lang pollen
(require racket/list)
(define (rick start finish) (range start finish))
rick[1 20]

'(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19)

Let’s return to the problem that surfaced in the last section — the fact that some command names can’t be used as tag functions because they’re already being used for other things. You can work around this by defining your own tag function with a non-conflicting name.

For instance, suppose we want to use map as a tag even though Racket is using it for its own function called map. First, we invent a command name that doesn’t conflict. Let’s call it my-map. As you learned above, Pollen will treat a new command name as a tag function by default:

#lang pollen
my-map{How I would love this to be a map.}

'(my-map "How I would love this to be a map.")

But my-map is not the tag we want. We need to define my-map to be a tag function for map. We can do this with the Pollen helper make-default-tag-function. That function lives in pollen/tag, so we require that too:

#lang pollen
(require pollen/tag)
(define my-map (make-default-tag-function 'map))
my-map{How I would love this to be a map.}

'(map "How I would love this to be a map.")

Problem solved.

3.1.3 Inserting the value of a variable

A Pollen command name usually refers to a function, but it can also refer to a variable, which is a data value. Once you define the variable, you can insert it into your source by using the ◊ notation without any other arguments:

#lang pollen
(define foo "bar")
The value of foo is foo

The value of foo is bar

Be careful — if you include arguments, even blank ones, Pollen will treat the command name as a function. This won’t work, because a variable is not a function:

To understand what happens here, recall the relationship between Pollen’s command modes. The Pollen-mode command ◊foo[] becomes the Racket-mode command (foo), which after variable substitution becomes ("bar"). If you try to evaluate ("bar") — e.g., in DrRacket — you’ll get the same error.

#lang pollen
(define foo "bar")
The value of foo is foo[]

application: not a procedure;
expected a procedure that can be applied to arguments
  given: "bar"
  arguments...: [none]

The reason we can simply drop ◊foo into the text argument of another Pollen command is that the variable foo holds a string (i.e., a text value).

In preprocessor source files, Pollen will convert a variable to a string in a sensible way. For instance, numbers are easily converted:

#lang pollen
(define zam 42)
The value of zam is zam

The value of zam is 42

In an unsaved DrRacket file, or a file without a special Pollen source extension, the #lang pollen designation invokes the Pollen preprocessor by default. You can explicitly invoke preprocessor mode by starting a file with #lang pollen/pre. See also (part "Preprocessor___pp_extension_").

If the variable holds a container datatype (like a list, hash, or vector), Pollen will produce the Racket text representation of the item. Here, zam is a list of integers:

#lang pollen
(define zam (list 1 2 3))
The value of zam is zam

The value of zam is '(1 2 3)

This feature is included for your convenience. But in general, your readers won’t want to see the Racket representation of a container. So in these cases, you should convert to a string manually in some sensible way. Here, the integers in the list are converted to strings, which are then combined using string-join from the racket/string library:

#lang pollen
(require racket/string)
(define zam (list 1 2 3))
The value of zam is string-join[(map number->string zam)]{ and }

The value of zam is 1 and 2 and 3

Pollen will still produce an error if you try to convert an esoteric value to a string. Here, zam is the addition function (+):

#lang pollen
(define zam +)
The value of zam is zam

Pollen decoder: can't convert #<procedure:+> to string

Moreover, Pollen will not perform any automatic text conversion in Pollen markup source files. Suppose we take the example above — which worked as a preprocessor source file — and change the language to pollen/markup:

#lang pollen/markup
(define zam (list 1 2 3))
The value of zam is zam

This time, the file will produce an error:

pollen markup error: in '(root "The value of zam is " (1 2 3)), '(1 2 3) is not a valid element (must be txexpr, string, symbol, XML char, or cdata)

One special case to know about. In the examples above, there’s a word space between the variable and the other text. But suppose you need to insert a variable into text so that there’s no space in between. The simple ◊ notation won’t work, because it won’t be clear where the variable name ends and the text begins.

For instance, suppose we want to use a variable edge next to the string px:

#lang pollen
(define edge 100)
p { margin-left: edgepx; }

Pollen decoder: can't convert #<procedure:...t/pollen/tag.rkt:6:2> to string

The example fails because Pollen reads the whole string after the as the single variable name edgepx. Since edgepx isn’t defined, it’s treated as a tag function, and since Pollen can’t convert a function to a string, we get an error.

In these situations, surround the variable name with vertical bars ◊|like so| to explicitly indicate where the variable name ends. The bars are not treated as part of the name, nor are they included in the result. Once we do that, we get what we intended:

#lang pollen
(define edge 100)
p { margin-left: ◊|edge|px; }

p { margin-left: 100px; }

If you use this notation when you don’t need to, nothing bad will happen. The vertical bars are always ignored.

#lang pollen
(define edge 100)
The value of edge is ◊|edge| pixels}

The value of edge is 100 pixels

3.1.4 Inserting metas

Metas are key–value pairs embedded in a source file that are not included in the main output when the source is compiled. Rather, they’re gathered and exported as a separate hash table called, unsurprisingly, metas. This hashtable is a good place to store information about the document that you might want to use later (for instance, a list of topic categories that the document belongs to).

Pollen occasionally uses metas internally. For instance, the get-template-for function will look in the metas of a source file to see if a template is explicitly specified. The pollen/template module also contains functions for working with metas, such as select-from-metas.

To make a meta, you create a tag with the special define-meta name. Then you have two choices: you can either embed the key-value pair as an attribute, or as a tagged X-expression within the meta (using the key as the tag, and the value as the body):

#lang pollen
 
define-meta[dog]{Roxy} ; Pollen-mode syntax
some-tag['key: "value"]{Normal tag}
(define-meta cat "Chopper") ; equivalent Racket-mode syntax
some-tag['key: "value"]{Another normal tag}

When you run a source file with metas in it, two things happen. First, the metas are removed from the output:

'(some-tag ((key "value")) "Normal tag")

'(some-tag ((key "value")) "Another normal tag")

Second, the metas are collected into a hash table that is exported with the name metas. To see this hash table, run the file above in DrRacket, then switch to the interactions window and type metas at the prompt:

> metas

'#hash((dog . "Roxy") (cat . "Chopper") (here-path . "unsaved-editor"))

The only key that’s automatically defined in every meta table is here-path, which is the absolute path to the source file. (In this case, because the file hasn’t been saved, you’ll see the unsaved-editor name instead.)

Still, you can override this too:

#lang pollen
 
define-meta[dog]{Roxy}
some-tag['key: "value"]{Normal tag}
(define-meta cat "Chopper")
some-tag['key: "value"]{Another normal tag}
(define-meta here-path "tesseract")

When you run this code, the result will be the same as before, but this time the metas will be different:

> metas

'#hash((dog . "Roxy") (cat . "Chopper") (here-path . "tesseract"))

It doesn’t matter how many metas you put in a source file, nor where you put them. They’ll all be extracted into the metas hash table. The order of the metas is not preserved (because order is not preserved in a hash table). But if you have two metas with the same key, the later one will supersede the earlier one:

#lang pollen
define-meta[dog]{Roxy}
(define-meta dog "Lex")

In this case, though there are two metas named dog (and they use different forms) only the second one persists:

> metas

'#hash((dog . "Lex") (here-path . "unsaved-editor"))

Pro tip: the metas hashtable is available when you import a Pollen source file in the usual way, but it’s also made available through a submodule called, unsurprisingly, metas.

#lang racket/base
(require "pollen-source.rkt") ; doc and metas and everything else
(require (submod "pollen-source.rkt" metas)) ; just metas

The metas submodule is useful because it gives you access to the metas hashtable without compiling the rest of the file. So if you need to collect metas from a set of source files — for instance, page titles (for a table of contents) or categories — getting the metas through the submodule is likely to be faster.

3.1.5 Retrieving metas

The metas hashtable is available immediately within the body of your source file. You can use hash-ref to get values out of metas.

#lang pollen
(define-meta dog "Roxy")
(hash-ref metas 'dog)

Roxy

Because the metas are collected first, you can actually invoke a meta before you define it:

#lang pollen
(hash-ref metas 'dog)
(define-meta dog "Roxy")
(define-meta dog "Spooky")

Spooky

This can be useful for setting up fields that you want to include in metas but also have visible in the body of a document, like a title.

#lang pollen/markup
(define-meta title "The Amazing Truth")
h1{(hash-ref metas 'title)}

The result of this file will be:

'(root (h1 "The Amazing Truth"))

And the metas: -

> metas

'#hash((title . "The Amazing Truth") (here-path . "unsaved-editor"))

You cannot, however, use hash-set! or other similar functions, because metas is an immutable hash.

3.1.6 Inserting a comment

Two options.

To comment out the rest of a single line, use a lozenge followed by a semicolon ◊;.

#lang pollen
span{This is not a comment}
span{Nor is this} ◊;span{But this is}

'(span "This is not a comment")
'(span "Nor is this")

To comment out a multiline block, use the lozenge–semicolon signal ◊; with curly braces, ◊;{like so}.

#lang pollen
◊;{
span{This is not a comment}
span{Nor is this} ◊;span{But this is}
}
Actually, it's all a comment now

Actually, it's all a comment now

3.2 The Racket arguments

The middle part of a Pollen-mode command contains the Racket arguments [between square brackets.] Most often, you’ll see these used to pass extra information to commands that operate on text.

For instance, tag functions. Recall from before that any not-yet-defined command name in Pollen is treated as a tag function:

#lang pollen
title{The Beginning of the End}

'(title "The Beginning of the End")

But what if you wanted to add attributes to this tag, so that it comes out like this?

'(title ((class "red")(id "first")) "The Beginning of the End")

You can do it with Racket arguments.

Here’s the hard way. You can type out your list of attributes in Racket format and drop them into the brackets as a single argument:

#lang pollen
title['((class "red")(id "first"))]{The Beginning of the End}

'(title ((class "red") (id "first")) "The Beginning of the End")

But that’s a lot of parentheses to think about. So here’s the easy way. Anytime you use a tag function, there’s a shortcut for inserting attributes. You can enter them as a series of keyword arguments between the Racket-argument brackets. The only caveat is that the values for these keyword arguments have to be strings. So taken together, they look like this:

#lang pollen
title[#:class "red" #:id "first"]{The Beginning of the End}

'(title ((class "red") (id "first")) "The Beginning of the End")

The string arguments can be any valid Racket expressions that produce strings. For instance, this will also work:

#lang pollen
title[#:class (format "~a" (* 6 7)) #:id "first"]{The Beginning of the End}

'(title ((class "42") (id "first")) "The Beginning of the End")

Since Pollen commands are really just Racket arguments underneath, you can use those too. Here, we’ll define a variable called name and use it in the Racket arguments of title:

#lang pollen
(define name "Brennan")
title[#:class "red" #:id name]{The Beginning of the End}

'(title ((class "read") (id "Brennan")) "The Beginning of the End")

When used in custom tag functions, keyword arguments don’t have to represent attributes. Instead, they can be used to provide options for a particular Pollen command, to avoid redundancy. Suppose that instead of using the h1 ... h6 tags, you want to consolidate them into one command called heading and select the level separately. You can do this with a keyword, in this case #:level, which is passed as a Racket argument:

#lang pollen
(define (heading #:level which text)
   `(,(string->symbol (format "h~a" which)) ,text))
 
heading[#:level 1]{Major league}
heading[#:level 2]{Minor league}
heading[#:level 6]{Trivial league}

'(h1 "Major league")
'(h2 "Minor league")
'(h6 "Trivial league")

3.3 The text argument

The third part of a Pollen-mode command is the text argument. The text argument {appears between curly braces}. It can contain any text you want. The text argument can also contain other Pollen commands with their own text arguments. And they can contain other Pollen commands ... and so on, all the way down.

#lang pollen
div{Do it again. div{And again. div{And yet again.}}}

'(div "Do it again. " (div "And again. " (div "And yet again.")))

Three small details to know about the text argument.

First, the only character that needs special handling in a text argument is the lozenge . A lozenge ordinarily marks a new command. So if you want an actual lozenge to appear in the text, you have to escape it by typing ◊"◊".

#lang pollen
definition{This is the lozenge: "◊"}

'(definition "This is the lozenge: ◊")

Second, the whitespace-trimming policy. Here’s the short version: if there’s a carriage return at either end of the text argument, it is trimmed, and whitespace at the end of each line is selectively trimmed in an intelligent way. So this text argument, with carriage returns on the ends:

#lang pollen
div{
Roomy!
 
I agree.
}

'(div "Roomy!" "\n" "\n" "I agree.")

Yields the same result as this one:

#lang pollen
div{Roomy!
 
I agree.}

'(div "Roomy!" "\n" "\n" "I agree.")

For the long version, please see [future link: Spaces, Newlines, and Indentation].

Third, within a multiline text argument, newline characters become individual strings that are not merged with adjacent text. So what you end up with is a list of strings, not a single string. That’s why in the last example, we got this:

'(div "Roomy!" "\n" "\n" "I agree.")

Instead of this:

'(div "Roomy!\n\nI agree.")

Under most circumstances, these two tagged X-expressions will behave the same way. The biggest exception is with functions. A function that operates on multiline text arguments needs to be able to handle an indefinite number of strings. For instance, this jejune function only accepts a single argument. It will work with a single-line text argument, because that produces a single string:

#lang pollen
(define (jejune text)
   `(jejune ,text))
jejune{Irrational confidence}

'(jejune "Irrational confidence")

But watch what happens with a multiline text argument:

#lang pollen
(define (jejune text)
   `(jejune ,text))
jejune{Deeply
        chastened}

jejune: arity mismatch;
the expected number of arguments does not match the given number
  expected: 1
  given: 3
  arguments...:
   "Deeply"
   "\n"
   "chastened"

The answer is to use a rest argument in the function, which takes the “rest” of the arguments — however many there may be — and combines them into a single list. If we rewrite jejune with a rest argument, we can fix the problem:

#lang pollen
(define (jejune . texts)
   `(jejune ,@texts))
jejune{Deeply
        chastened}

'(jejune "Deeply" "\n" "chastened")

4 Embedding character entities

XML and HTML support character entities, a way of encoding Unicode characters with a name or number. For instance, in HTML, the copyright symbol © can be encoded by name as &copy; or by number as &#169;.

Entities originated as a way of embedding Unicode characters in an ASCII data stream. Pollen and Racket, however, support Unicode directly. So does every major web browser (though your document may need a Unicode character-set declaration to trigger it). So usually, your best bet is insert Unicode characters directly into your source rather than use entities.

But if you really need entities, here’s what to do. Pollen treats everything as text by default, so you can’t insert entities merely by typing them, because they’ll just be converted to text:

#lang pollen
div{copy
     169}

'(div "copy" "\n" "169")

Instead, named entities are handled as (part ("(lib scribblings/guide/guide.scrbl)" "symbols")) and numeric entities are, unsurprisingly, (part ("(lib scribblings/guide/guide.scrbl)" "numbers")). So you can use the string->symbol and string->number functions to convert your entity input:

#lang pollen
div{string->symbol{copy}
     string->number{169}}

'(div copy "\n" 169)

Notice that in the output, there are no more quote marks around copy and 169, indicating that they’re not strings. When you pass this result to a converter like ->html, the entities will be escaped correctly:

#lang pollen
(require pollen/template)
 
->html{div{copy 169}}
 
->html{div{string->symbol{copy} string->number{169}}}

<div>copy 169</div>

<div>&copy; &#169;</div>

For numeric entities, you can also use a four-digit Unicode hex number by prefacing it with #x, which is the standard Racket prefix for a hex number:

#lang pollen
div{string->number{169}
     string->number{#x00a9}}

'(div 169 "\n" 169)

Of course, you don’t need to use string->symbol and string->number directly in your source. You can also define tag functions that generate entities. The key point is that to be treated as an entity, the return value must be a symbol or number, rather than a string.

5 Adding Pollen-mode commands to a Racket file

 #lang pollen/exp package: pollen

Just as you can embed any Racket-mode command in a Pollen source file, you can go the other way and embed Pollen-mode commands in a Racket file. For instance, in your (part "The__pollen_rkt__file"), you may find it convenient to use Pollen mode for certain values.

You enable Pollen mode within your source file by adding pollen/exp to your #lang line at the top of your source:

"pollen.rkt"
#lang pollen/exp racket/base
(require pollen/tag)
 
(define link (make-default-tag-function 'a))
 
(define (home-link)
  (link #:href "index.html" "Click to go home"))
 
(define (home-link-pollen-mode)
  link[#:href "index.html"]{Click to go home})
 

Here, both (home-link) and (home-link-pollen-mode) will produce the same X-expression as a result:

'(a ((href "index.html")) "Click to go home")

Of course, you can use pollen/exp in any Racket source file, not just "pollen.rkt".

Keep in mind that pollen/exp is just a syntactic convenience. It doesn’t change any of the underlying semantics of your Racket source file. Your Pollen-mode commands are being translated into Racket commands and compiled along with everything else.

Another good way to use Pollen-mode commands in Racket is for unit tests with rackunit. With pollen/exp, you can write your unit tests in Pollen mode or Racket mode (or mix them).

Unit tests are little one-line tests you put into your code to verify it does what you expect. You do this with the rackunit library, which is beloved by all Racket programmers. For more, see (part ("(lib rackunit/scribblings/rackunit.scrbl)" "quick-start")).

"pollen.rkt"
#lang pollen/exp racket/base
(require rackunit)
 
(define (tag-fn arg . text-args)
  `(div ((class ,arg)) ,@text-args))
 
(check-equal? tag-fn["42"]{hello world}
              '(div ((class "42")) "hello world"))
 
(check-equal? (tag-fn "42" "hello world")
              '(div ((class "42")) "hello world"))
 
(check-equal? tag-fn["42"]{hello world}
              'div[((class "42"))]{hello world})
 

6 Further reading

The Pollen language is a variant of Racket’s own text-processing language, called Scribble. Thus, most things that can be done with Scribble syntax can also be done with Pollen syntax. For the sake of clarity & brevity, I’ve only shown you the highlights here. But if you want the full story, see (part ("(lib scribblings/scribble/scribble.scrbl)" "reader")) in the Scribble documentation.

 
\ No newline at end of file diff --git a/pollen/scribblings/command.scrbl b/pollen/scribblings/command.scrbl index d1d389c..2aee158 100644 --- a/pollen/scribblings/command.scrbl +++ b/pollen/scribblings/command.scrbl @@ -1,7 +1,7 @@ #lang scribble/manual @(require scribble/bnf scribble/eval "utils.rkt" "mb-tools.rkt" (for-syntax racket/base) - (for-label rackunit pollen/world pollen/render pollen/template (only-in scribble/reader + (for-label rackunit pollen/core pollen/world pollen/render pollen/template (only-in scribble/reader use-at-readtable))) @(define read-eval (make-base-eval)) @@ -522,7 +522,7 @@ The value of edge is ◊|edge| pixels} @margin-note{Pollen occasionally uses metas internally. For instance, the @racket[get-template-for] function will look in the metas of a source file to see if a template is explicitly specified. The @racket[pollen/template] module also contains functions for working with metas, such as @racket[select-from-metas].} -To make a meta, you create a tag with the special @code{define-meta} name. Then you have two choices: you can either embed the key-value pair as an attribute, or as a tagged X-expression within the meta (using the key as the tag, and the value as the body): +To make a meta, you create a tag with the special @racket[define-meta] name. Then you have two choices: you can either embed the key-value pair as an attribute, or as a tagged X-expression within the meta (using the key as the tag, and the value as the body): @codeblock{ #lang pollen diff --git a/pollen/scribblings/core.scrbl b/pollen/scribblings/core.scrbl new file mode 100644 index 0000000..4fe29b1 --- /dev/null +++ b/pollen/scribblings/core.scrbl @@ -0,0 +1,137 @@ +#lang scribble/manual + +@(require scribble/eval pollen/cache pollen/world (for-label racket (except-in pollen #%module-begin) pollen/render txexpr xml pollen/pagetree sugar/coerce pollen/core pollen/world)) + +@(define my-eval (make-base-eval)) +@(my-eval `(require pollen pollen/core xml)) + +@title{Core} + +@defmodule[pollen/core] + +These functions are automatically imported into every Pollen source file (meaning, as if they had been included in your @filepath{pollen.rkt}). + + + +@section{Syntactic forms} + +@defform[(define-meta name value)] +Add @racket[_value] to the metas of the current document, using @racket[_name] as the key. + +You can retrieve a meta value — even in the same document where you define it — with @racket[(select-from-metas _name metas)]. + +For an introduction to metas, see @secref["Inserting_metas"]. + + +@defform[(when/splice condition pollen-args)] +If @racket[_condition] is true, put the @racket[_pollen-args] into the document. Within a template file, usually invoked like so: + +@verbatim{◊when/splice[@racketvarfont{condition}]{The text to insert.}} + +The inserted text can contain its own nested Pollen commands. + +@racket[when/splice] can be more convenient than @racket[when], because @racket[when] will only use the last argument between the curly braces. @racket[when/splice], by contrast, treats everything between the curly braces as a block. + + +@section{Data helpers} + +Functions for retrieving data out of Pollen source files. These are not the only options – you can, of course, use any of the usual Racket functions. + + +@defproc[ +(get-doc +[doc-source (or/c pagenode? pathish?)]) +(or/c txexpr? string?)] +Retrieve the @racket[doc] export from @racket[_doc-source], which can be either a path, path string, or pagenode that can be resolved into a source path. If @racket[_doc-source] cannot be resolved, raise an error. + +If @racket[_doc-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). + +If @racket[world:current-main-export] has been overridden with a project-specific value, then that is retrieved instead. + + +@defproc[ +(get-metas +[meta-source (or/c pagenode? pathish?)]) +hash?] +Retrieve the @racket[metas] export from @racket[_meta-source], which can be either a path, path string, or pagenode that can be resolved into a source path. If @racket[_meta-source] cannot be resolved, raise an error. + +If @racket[_meta-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). + +If @racket[world:current-meta-export] has been overridden with a project-specific value, then that is retrieved instead. + + +@deftogether[( + +@defproc[ +(select +[key symbolish?] +[value-source (or/c hash? txexpr? pagenode? pathish?)]) +(or/c #f xexpr?)] + +@defproc[ +(select* +[key symbolish?] +[value-source (or/c hash? txexpr? pagenode? pathish?)]) +(or/c #f (listof xexpr?))] + +)] +Find matches for @racket[_key] in @racket[_value-source]. The @racket[_value-source] can be 1) a hashtable of @racket[metas], 2) a tagged X-expression representing a @racket[doc], or 3) a pagenode or path that identifies a source file that provides @racket[metas] and @racket[doc]. In that case, first look for @racket[_key] in @code{metas} (using @racket[select-from-metas]) and then in @code{doc} (using @racket[select-from-doc]). + +With @racket[select], you get the first result; with @racket[select*], you get them all. + +In both cases, you get @racket[#f] if there are no matches. + +Note that if @racket[_value-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). + +@examples[#:eval my-eval +(module nut-butters pollen/markup +'(div (question "Flavor?") + (answer "Cashew") (answer "Almond"))) +(code:comment @#,t{Import doc from 'nut-butters submodule}) +(require 'nut-butters) +(select 'question doc) +(select 'answer doc) +(select* 'answer doc) +(select 'nonexistent-key doc) +(select* 'nonexistent-key doc) +] + + +@defproc[ +(select-from-doc +[key symbolish?] +[doc-source (or/c txexpr? pagenodeish? pathish?)]) +(or/c #f (listof xexpr?))] +Look up the value of @racket[_key] in @racket[_doc-source]. The @racket[_doc-source] argument can be either 1) a tagged X-expression representing a @racket[doc] or 2) a pagenode or source path that identifies a source file that provides @racket[doc]. If no value exists for @racket[_key], you get @racket[#f]. + +Note that if @racket[_doc-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). + +@examples[#:eval my-eval +(module gelato pollen/markup +'(div (question "Flavor?") + (answer "Nocciola") (answer "Pistachio"))) +(code:comment @#,t{Import doc from 'gelato submodule}) +(require 'gelato) +(select-from-doc 'question doc) +('answer . select-from-doc . doc) +(select-from-doc 'nonexistent-key doc) +] + + + +@defproc[ +(select-from-metas +[key symbolish?] +[meta-source (or/c hash? pagenodeish? pathish?)]) +(or/c #f xexpr?)] +Look up the value of @racket[_key] in @racket[_meta-source]. The @racket[_meta-source] argument can be either 1) a hashtable representing @racket[metas] or 2) a pagenode or source path that identifies a source file that provides @racket[metas]. If no value exists for @racket[_key], you get @racket[#f]. + +Note that if @racket[_meta-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). + +@examples[#:eval my-eval +(define metas (hash 'template "sub.xml.pp" 'target "print")) +(select-from-metas 'template metas) +('target . select-from-metas . metas) +(select-from-metas 'nonexistent-key metas) +] + diff --git a/pollen/scribblings/decode.html b/pollen/scribblings/decode.html deleted file mode 100644 index afb3617..0000000 --- a/pollen/scribblings/decode.html +++ /dev/null @@ -1,2 +0,0 @@ - -Decode
Decode
decode
decode-elements
block-txexpr?
1 Typography
whitespace?
whitespace/  nbsp?
smart-quotes
smart-dashes
merge-newlines
detect-linebreaks
detect-paragraphs
wrap-hanging-quotes
6.3.0.14

Decode

 (require pollen/decode) package: pollen

The doc export of a Pollen markup file is a simple X-expression. Decoding refers to any post-processing of this X-expression. The pollen/decode module provides tools for creating decoders.

The decode step can happen separately from the compilation of the file. But you can also attach a decoder to the markup file’s root node, so the decoding happens automatically when the markup is compiled, and thus automatically incorporated into doc. (Following this approach, you could also attach multiple decoders to different tags within doc.)

You can, of course, embed function calls within Pollen markup. But since markup is optimized for authors, decoding is useful for operations that can or should be moved out of the authoring layer.

One example is presentation and layout. For instance, detect-paragraphs is a decoder function that lets authors mark paragraphs in their source simply by using two carriage returns.

Another example is conversion of output into a particular data format. Most Pollen functions are optimized for HTML output, but one could write a decoder that targets another format.

procedure

(decode tagged-xexpr 
  [#:txexpr-tag-proc txexpr-tag-proc 
  #:txexpr-attrs-proc txexpr-attrs-proc 
  #:txexpr-elements-proc txexpr-elements-proc 
  #:txexpr-proc txexpr-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 tags-to-exclude 
  #:exclude-attrs attrs-to-exclude]) 
  (or/c xexpr/c (listof xexpr/c))
  tagged-xexpr : txexpr?
  txexpr-tag-proc : (txexpr-tag? . -> . txexpr-tag?)
   = (λ(tag) tag)
  txexpr-attrs-proc : (txexpr-attrs? . -> . txexpr-attrs?)
   = (λ(attrs) attrs)
  txexpr-elements-proc : (txexpr-elements? . -> . txexpr-elements?)
   = (λ(elements) elements)
  txexpr-proc : (txexpr? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(tx) tx)
  block-txexpr-proc : (block-txexpr? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(tx) tx)
  inline-txexpr-proc : (txexpr? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(tx) tx)
  string-proc : (string? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(str) str)
  entity-proc : ((or/c symbol? valid-char?) . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(ent) ent)
  cdata-proc : (cdata? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(cdata) cdata)
  tags-to-exclude : (listof txexpr-tag?) = null
  attrs-to-exclude : txexpr-attrs? = null
Recursively process a tagged-xexpr, usually the one exported from a Pollen source file as doc.

This function doesn’t do much on its own. Rather, it provides the hooks upon which harder-working functions can be hung.

Recall that in Pollen, all (part "tags-are-functions"). By default, the tagged-xexpr from a source file is tagged with root. So the typical way to use decode is to attach your decoding functions to it, and then define root to invoke your decode function. Then it will be automatically applied to every doc during compile.

For instance, here’s how decode is attached to root in Butterick’s Practical Typography. There’s not much to it —

(define (root . items)
  (decode (txexpr 'root '() items)
          #:txexpr-elements-proc detect-paragraphs
          #:block-txexpr-proc (compose1 hyphenate wrap-hanging-quotes)
          #:string-proc (compose1 smart-quotes smart-dashes)
          #:exclude-tags '(style script)))

The hyphenate function is not part of Pollen, but rather the hyphenate package, which you can install separately.

This illustrates another important point: even though decode presents an imposing list of arguments, you’re unlikely to use all of them at once. These represent possibilities, not requirements. For instance, let’s see what happens when decode is invoked without any of its optional arguments.

Examples:
> (define tx '(root "I wonder" (em "why") "this works."))
> (decode tx)

'(root "I wonder" (em "why") "this works.")

Right — nothing. That’s because the default value for the decoding arguments is the identity function, (λ (x) x). So all the input gets passed through intact unless another action is specified.

The *-proc arguments of decode take procedures that are applied to specific categories of elements within txexpr.

The txexpr-tag-proc argument is a procedure that handles X-expression tags.

Examples:
> (define tx '(p "I'm from a strange" (strong "namespace")))
; Tags are symbols, so a tag-proc should return a symbol
> (decode tx #:txexpr-tag-proc (λ(t) (string->symbol (format "ns:~a" t))))

'(ns:p "I'm from a strange" (ns:strong "namespace"))

The txexpr-attrs-proc argument is a procedure that handles lists of X-expression attributes. (The txexpr module, included at no extra charge with Pollen, includes useful helper functions for dealing with these attribute lists.)

Examples:
> (define tx '(p ((id "first")) "If I only had a brain."))
; Attrs is a list, so cons is OK for simple cases
> (decode tx #:txexpr-attrs-proc (λ(attrs) (cons '[class "PhD"] attrs)))

'(p ((class "PhD") (id "first")) "If I only had a brain.")

Note that txexpr-attrs-proc will change the attributes of every tagged X-expression, even those that don’t have attributes. This is useful, because sometimes you want to add attributes where none existed before. But be careful, because the behavior may make your processing function overinclusive.

Examples:
> (define tx '(div (p ((id "first")) "If I only had a brain.")
  (p "Me too.")))
; This will insert the new attribute everywhere
> (decode tx #:txexpr-attrs-proc (λ(attrs) (cons '[class "PhD"] attrs)))

'(div

  ((class "PhD"))

  (p ((class "PhD") (id "first")) "If I only had a brain.")

  (p ((class "PhD")) "Me too."))

; This will add the new attribute only to non-null attribute lists
> (decode tx #:txexpr-attrs-proc
  (λ(attrs) (if (null? attrs) attrs (cons '[class "PhD"] attrs))))

'(div (p ((class "PhD") (id "first")) "If I only had a brain.") (p "Me too."))

The txexpr-elements-proc argument is a procedure that operates on the list of elements that represents the content of each tagged X-expression. Note that each element of an X-expression is subject to two passes through the decoder: once now, as a member of the list of elements, and also later, through its type-specific decoder (i.e., string-proc, entity-proc, and so on).

Examples:
> (define tx '(div "Double" "\n" "toil" amp "trouble"))
; Every element gets doubled ...
> (decode tx #:txexpr-elements-proc (λ(es) (append-map (λ(e) (list e e)) es)))

'(div "Double" "Double" "\n" "\n" "toil" "toil" amp amp "trouble" "trouble")

; ... but only strings get capitalized
> (decode tx #:txexpr-elements-proc (λ(es) (append-map (λ(e) (list e e)) es))
  #:string-proc (λ(s) (string-upcase s)))

'(div "DOUBLE" "DOUBLE" "\n" "\n" "TOIL" "TOIL" amp amp "TROUBLE" "TROUBLE")

So why do you need txexpr-elements-proc? Because some types of element decoding depend on context, thus it’s necessary to handle the elements as a group. For instance, paragraph detection. The behavior is not merely a map across each element, because elements are being removed and altered contextually:

Examples:
> (define (paras tx) (decode tx #:txexpr-elements-proc detect-paragraphs))
; Context matters. Trailing whitespace is ignored ...
> (paras '(body "The first paragraph." "\n\n"))

'(body "The first paragraph.")

; ... but whitespace between strings is converted to a break.
> (paras '(body "The first paragraph." "\n\n" "And another."))

'(body (p "The first paragraph.") (p "And another."))

; A combination of both types
> (paras '(body "The first paragraph." "\n\n" "And another." "\n\n"))

'(body (p "The first paragraph.") (p "And another."))

The txexpr-proc, block-txexpr-proc, and inline-txexpr-proc arguments are procedures that operate on tagged X-expressions. If the X-expression meets the block-txexpr? test, it’s processed by block-txexpr-proc. Otherwise, it’s inline, so it’s processed by inline-txexpr-proc. (Careful, however — these aren’t mutually exclusive, because block-txexpr-proc operates on all the elements of a block, including other tagged X-expressions within.) Then both categories are processed by txexpr-proc.

Examples:
> (define tx '(div "Please" (em "mind the gap") (h1 "Tuesdays only")))
> (define add-ns (λ(tx) (make-txexpr
      (string->symbol (format "ns:~a" (get-tag tx)))
      (get-attrs tx)
      (get-elements tx))))
; div and h1 are block elements, so this will only affect them
> (decode tx #:block-txexpr-proc add-ns)

'(ns:div "Please" (em "mind the gap") (ns:h1 "Tuesdays only"))

; em is an inline element, so this will only affect it
> (decode tx #:inline-txexpr-proc add-ns)

'(div "Please" (ns:em "mind the gap") (h1 "Tuesdays only"))

; this will affect all elements
> (decode tx #:block-txexpr-proc add-ns #:inline-txexpr-proc add-ns)

'(ns:div "Please" (ns:em "mind the gap") (ns:h1 "Tuesdays only"))

; as will this
> (decode tx #:txexpr-proc add-ns)

'(ns:div "Please" (ns:em "mind the gap") (ns:h1 "Tuesdays only"))

The string-proc, entity-proc, and cdata-proc arguments are procedures that operate on X-expressions that are strings, entities, and CDATA, respectively. Deliberately, the output contracts for these procedures accept any kind of X-expression (meaning, the procedure can change the X-expression type).

Examples:
; A div with string, entity, and cdata elements
> (define tx `(div "Moe" amp 62 ,(cdata #f #f "3 > 2;")))
> (define rulify (λ(x) '(hr)))
; The rulify function is selectively applied to each
> (print (decode tx #:string-proc rulify))

(list 'div '(hr) 'amp 62 (cdata #f #f "3 > 2;"))

> (print (decode tx #:entity-proc rulify))

(list 'div "Moe" #0='(hr) #0# (cdata #f #f "3 > 2;"))

> (print (decode tx #:cdata-proc rulify))

'(div "Moe" amp 62 (hr))

Note that entities come in two flavors — symbolic and numeric — and entity-proc affects both. If you only want to affect one or the other, you can add a test within entity-proc. Symbolic entities can be detected with symbol?, and numeric entities with valid-char?:

Examples:
> (define tx `(div amp 62))
> (define symbolic-detonate (λ(x) (if (symbol? x) 'BOOM x)))
> (print (decode tx #:entity-proc symbolic-detonate))

'(div BOOM 62)

> (define numeric-detonate (λ(x) (if (valid-char? x) 'BOOM x)))
> (print (decode tx #:entity-proc numeric-detonate))

'(div amp BOOM)

The five previous procedures — block-txexpr-proc, inline-txexpr-proc, string-proc, entity-proc, and cdata-proc — can return either a single X-expression, or a list of X-expressions, which will be spliced into the parent at the same point.

For instance, earlier we saw how to double elements by using txexpr-elements-proc. But you can accomplish the same thing on a case-by-case basis by returning a list of values:

Examples:
; A div with string, entity, and inline-txexpr elements
> (define tx `(div "Axl" amp (span "Slash")))
> (define doubler (λ(x) (list x x)))
; The doubler function is selectively applied to each type of element
> (print (decode tx #:string-proc doubler))

'(div "Axl" "Axl" amp (span "Slash" "Slash"))

> (print (decode tx #:entity-proc doubler))

'(div "Axl" (amp amp) (span "Slash"))

> (print (decode tx #:inline-txexpr-proc doubler))

'(div "Axl" amp (span "Slash") (span "Slash"))

Caution: when returning list values, it’s possible to trip over the unavoidable ambiguity between a txexpr? and a list of xexpr?s that happens to begin with a symbolic entity:

Examples:
; An ambiguous expression
> (define amb '(guitar "player-name"))
> (and (txexpr-elements? amb) (txexpr? amb))

#t

; Ambiguity in context
> (define x '(gnr "Izzy" "Slash"))
> (define rockit (λ(str) (list 'guitar str)))
; Expecting '(gnr guitar "Izzy" guitar "Slash") from next line,
but return value will be treated as tagged X-expression
> (decode x #:string-proc rockit)

'(gnr (guitar "Izzy") (guitar "Slash"))

; Changing the order makes it unambiguous
> (define rockit2 (λ(str) (list str 'guitar)))
> (decode x #:string-proc rockit2)

'(gnr "Izzy" guitar "Slash" guitar)

The tags-to-exclude argument is a list of tags that will be exempted from decoding. Though you could get the same result by testing the input within the individual decoding functions, that’s tedious and potentially slower.

Examples:
> (define tx '(p "I really think" (em "italics") "should be lowercase."))
> (decode tx #:string-proc string-upcase)

'(p "I REALLY THINK" (em "ITALICS") "SHOULD BE LOWERCASE.")

> (decode tx #:string-proc string-upcase #:exclude-tags '(em))

'(p "I REALLY THINK" (em "italics") "SHOULD BE LOWERCASE.")

The tags-to-exclude argument is useful if you’re decoding source that’s destined to become HTML. According to the HTML spec, material within a <style> or <script> block needs to be preserved literally. In this example, if the CSS and JavaScript blocks are capitalized, they won’t work. So exclude '(style script), and problem solved.

Examples:
> (define tx '(body (h1 ((class "Red")) "Let's visit Planet Telex.")
  (style ((type "text/css")) ".Red {color: green;}")
  (script ((type "text/javascript")) "var area = h * w;")))
> (decode tx #:string-proc string-upcase)

'(body

  (h1 ((class "Red")) "LET'S VISIT PLANET TELEX.")

  (style ((type "text/css")) ".RED {COLOR: GREEN;}")

  (script ((type "text/javascript")) "VAR AREA = H * W;"))

> (decode tx #:string-proc string-upcase #:exclude-tags '(style script))

'(body

  (h1 ((class "Red")) "LET'S VISIT PLANET TELEX.")

  (style ((type "text/css")) ".Red {color: green;}")

  (script ((type "text/javascript")) "var area = h * w;"))

Finally, the attrs-to-exclude argument works the same way as tags-to-exclude, but instead of excluding an element based on its tag, it excludes based on whether the element has a matching attribute/value pair.

Examples:
> (define tx '(p (span "No attrs") (span ((id "foo")) "One attr")))
> (decode tx #:string-proc string-upcase)

'(p (span "NO ATTRS") (span ((id "foo")) "ONE ATTR"))

> (decode tx #:string-proc string-upcase #:exclude-attrs '((id "foo")))

'(p (span "NO ATTRS") (span ((id "foo")) "One attr"))

procedure

(decode-elements elements 
  [#:txexpr-tag-proc txexpr-tag-proc 
  #:txexpr-attrs-proc txexpr-attrs-proc 
  #:txexpr-elements-proc txexpr-elements-proc 
  #:txexpr-proc txexpr-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 tags-to-exclude 
  #:exclude-attrs attrs-to-exclude]) 
  (or/c xexpr/c (listof xexpr/c))
  elements : txexpr-elements?
  txexpr-tag-proc : (txexpr-tag? . -> . txexpr-tag?)
   = (λ(tag) tag)
  txexpr-attrs-proc : (txexpr-attrs? . -> . txexpr-attrs?)
   = (λ(attrs) attrs)
  txexpr-elements-proc : (txexpr-elements? . -> . txexpr-elements?)
   = (λ(elements) elements)
  txexpr-proc : (txexpr? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(tx) tx)
  block-txexpr-proc : (block-txexpr? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(tx) tx)
  inline-txexpr-proc : (txexpr? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(tx) tx)
  string-proc : (string? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(str) str)
  entity-proc : ((or/c symbol? valid-char?) . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(ent) ent)
  cdata-proc : (cdata? . -> . (or/c xexpr? (listof xexpr?)))
   = (λ(cdata) cdata)
  tags-to-exclude : (listof txexpr-tag?) = null
  attrs-to-exclude : txexpr-attrs? = null
Identical to decode, but takes txexpr-elements? as input rather than a whole tagged X-expression, and likewise returns txexpr-elements? rather than a tagged X-expression. A convenience variant for use inside tag functions.

procedure

(block-txexpr? v)  boolean?

  v : any/c
Predicate that tests whether v has a tag that is among the world:current-block-tags. If not, it is treated as inline.

This predicate affects the behavior of other functions. For instance, detect-paragraphs knows that block elements in the markup shouldn’t be wrapped in a p tag. So if you introduce a new block element called bloq without configuring it as a block, misbehavior will follow:

Examples:
> (define (paras tx) (decode tx #:txexpr-elements-proc detect-paragraphs))
> (paras '(body "I want to be a paragraph." "\n\n" (bloq "But not me.")))

'(body (p "I want to be a paragraph.") (p (bloq "But not me.")))

; Wrong: bloq should not be wrapped

To change how this test works, use a config submodule as described in (part "settable-values"):

(module config racket/base
  (provide (all-defined-out))
  (require pollen/world)
  (define block-tags (cons 'bloq world:block-tags)))

After that change, the result will be:

’(body (p "I want to be a paragraph.") (bloq "But not me."))

The default block tags are:

root address article aside blockquote body canvas dd div dl fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hgroup hr li main nav noscript ol output p pre section table tfoot ul video

1 Typography

An assortment of typography & layout functions, designed to be used with decode. These aren’t hard to write. So if you like these, use them. If not, make your own.

procedure

(whitespace? v)  boolean?

  v : any/c
A predicate that returns #t for any stringlike v that’s entirely whitespace, but also the empty string, as well as lists and vectors that are made only of whitespace? members. Following the regexp-match convention, whitespace? does not return #t for a nonbreaking space. If you prefer that behavior, use whitespace/nbsp?.

Examples:
> (whitespace? "\n\n   ")

#t

> (whitespace? (string->symbol "\n\n   "))

#t

> (whitespace? "")

#t

> (whitespace? '("" "  " "\n\n\n" " \n"))

#t

> (define nonbreaking-space (format "~a" #\u00A0))
> (whitespace? nonbreaking-space)

#f

procedure

(whitespace/nbsp? v)  boolean?

  v : any/c
Like whitespace?, but also returns #t for nonbreaking spaces.

Examples:
> (whitespace/nbsp? "\n\n   ")

#t

> (whitespace/nbsp? (string->symbol "\n\n   "))

#t

> (whitespace/nbsp? "")

#t

> (whitespace/nbsp? '("" "  " "\n\n\n" " \n"))

#t

> (define nonbreaking-space (format "~a" #\u00A0))
> (whitespace/nbsp? nonbreaking-space)

#t

procedure

(smart-quotes str)  string?

  str : string?
Convert straight quotes in str to curly according to American English conventions.

Examples:
> (define tricky-string
  "\"Why,\" she could've asked, \"are we in O‘ahu watching 'Mame'?\"")
> (display tricky-string)

"Why," she could've asked, "are we in O‘ahu watching 'Mame'?"

> (display (smart-quotes tricky-string))

“Why,” she could’ve asked, “are we in O‘ahu watching ‘Mame’?”

procedure

(smart-dashes str)  string?

  str : string?
In str, convert three hyphens to an em dash, and two hyphens to an en dash, and remove surrounding spaces.

Examples:
> (define tricky-string "I had a few --- OK, like 6--8 --- thin mints.")
> (display tricky-string)

I had a few --- OK, like 6--8 --- thin mints.

> (display (smart-dashes tricky-string))

I had a few—OK, like 6–8—thin mints.

; Monospaced font not great for showing dashes, but you get the idea

procedure

(merge-newlines elements)  (listof xexpr?)

  elements : (listof xexpr?)
Within elements, merge sequential newline characters ("\n") into a single whitespace element. Helper function used by detect-paragraphs.

Example:
> (merge-newlines '(p "\n" "\n" "foo" "\n" "\n\n" "bar"
    (em "\n" "\n" "\n")))

'(p "\n\n" "foo" "\n\n\n" "bar" (em "\n\n\n"))

procedure

(detect-linebreaks tagged-xexpr-elements    
  [#:separator linebreak-sep    
  #:insert linebreak])  (listof xexpr?)
  tagged-xexpr-elements : (listof xexpr?)
  linebreak-sep : string? = (world:current-linebreak-separator)
  linebreak : xexpr? = '(br)
Within tagged-xexpr-elements, convert occurrences of linebreak-sep ("\n" by default) to linebreak, but only if linebreak-sep does not occur between blocks (see block-txexpr?). Why? Because block-level elements automatically display on a new line, so adding linebreak would be superfluous. In that case, linebreak-sep just disappears.

Examples:
> (detect-linebreaks '(div "Two items:" "\n" (em "Eggs") "\n" (em "Bacon")))

'(div "Two items:" (br) (em "Eggs") (br) (em "Bacon"))

> (detect-linebreaks '(div "Two items:" "\n" (div "Eggs") "\n" (div "Bacon")))

'(div "Two items:" (div "Eggs") (div "Bacon"))

procedure

(detect-paragraphs elements 
  [#:separator paragraph-sep 
  #:tag paragraph-tag 
  #:linebreak-proc linebreak-proc 
  #:force? force-paragraph?]) 
  (listof xexpr?)
  elements : (listof xexpr?)
  paragraph-sep : string? = (world:current-paragraph-separator)
  paragraph-tag : symbol? = 'p
  linebreak-proc : ((listof xexpr?) . -> . (listof xexpr?))
   = detect-linebreaks
  force-paragraph? : boolean? = #f
Find paragraphs within elements and wrap them with paragraph-tag. Also handle linebreaks using detect-linebreaks.

What counts as a paragraph? Any elements that are either a) explicitly set apart with paragraph-sep, or b) adjacent to a block-txexpr? (in which case the paragraph-ness is implied).

Examples:
> (detect-paragraphs '("Explicit para" "\n\n" "Explicit para"))

'((p "Explicit para") (p "Explicit para"))

> (detect-paragraphs '("Explicit para" "\n\n" "Explicit para" "\n" "Explicit line"))

'((p "Explicit para") (p "Explicit para" (br) "Explicit line"))

> (detect-paragraphs '("Implied para" (div "Block") "Implied para"))

'((p "Implied para") (div "Block") (p "Implied para"))

If element is already a block, it will not be wrapped as a paragraph (because in that case, the wrapping would be superfluous). Thus, as a consequence, if paragraph-sep occurs between two blocks, it will be ignored (as in the example below using two sequential div blocks.) Likewise, paragraph-sep will also be ignored if it occurs between a block and a non-block (because a paragraph break is already implied).

Examples:
; The explicit "\n\n" makes no difference in these cases
> (detect-paragraphs '((div "First block") "\n\n" (div "Second block")))

'((div "First block") (div "Second block"))

> (detect-paragraphs '((div "First block") (div "Second block")))

'((div "First block") (div "Second block"))

> (detect-paragraphs '("Para" "\n\n" (div "Block")))

'((p "Para") (div "Block"))

> (detect-paragraphs '("Para" (div "Block")))

'((p "Para") (div "Block"))

The paragraph-tag argument sets the tag used to wrap paragraphs.

Example:
> (detect-paragraphs '("First para" "\n\n" "Second para") #:tag 'ns:p)

'((ns:p "First para") (ns:p "Second para"))

The linebreak-proc argument allows you to use a different linebreaking procedure other than the usual detect-linebreaks.

Example:
> (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"))

The #:force? option will wrap a paragraph tag around elements, even if no explicit or implicit paragraph breaks are found. The #:force? option is useful for when you want to guarantee that you always get a list of blocks.

Examples:
> (detect-paragraphs '("This" (span "will not be") "a paragraph"))

'("This" (span "will not be") "a paragraph")

> (detect-paragraphs '("But this" (span "will be") "a paragraph") #:force? #t)

'((p "But this" (span "will be") "a paragraph"))

procedure

(wrap-hanging-quotes tx 
  [#:single-preprend single-preprender 
  #:double-preprend double-preprender]) 
  txexpr?
  tx : txexpr?
  single-preprender : txexpr-tag? = 'squo
  double-preprender : txexpr-tag? = 'dquo
Find single or double quote marks at the beginning of tx and wrap them in an X-expression with the tag single-preprender or double-preprender, respectively. The default values are 'squo and 'dquo.

Examples:
> (wrap-hanging-quotes '(p "No quote to hang."))

'(p "No quote to hang.")

> (wrap-hanging-quotes '(p "“What? We need to hang quotes?”"))

'(p (dquo "“" "What? We need to hang quotes?”"))

In pro typography, quotation marks at the beginning of a line or paragraph are often shifted into the margin slightly to make them appear more optically aligned with the left edge of the text. With a reflowable layout model like HTML, you don’t know where your line breaks will be.

This function will simply insert the 'squo and 'dquo tags, which provide hooks that let you do the actual hanging via CSS, like so (actual measurement can be refined to taste):

squo {margin-left: -0.25em;}

dquo {margin-left: -0.50em;}

Be warned: there are many edge cases this function does not handle well.

Examples:
; Argh: this edge case is not handled properly
> (wrap-hanging-quotes '(p "“" (em "What?") "We need to hang quotes?”"))

'(p "“" (em "What?") "We need to hang quotes?”")

 
\ No newline at end of file diff --git a/pollen/scribblings/formats.html b/pollen/scribblings/formats.html deleted file mode 100644 index f643e42..0000000 --- a/pollen/scribblings/formats.html +++ /dev/null @@ -1,2 +0,0 @@ - -File formats
File formats
1 Source formats
1.1 Command syntax using ◊
1.2 Any command is valid
1.3 Standard exports
1.4 Custom exports
1.5 The "pollen.rkt" file
1.6 Preprocessor (.pp extension)
1.7 Markdown (.pmd extension)
1.8 Markup (.pm extension)
1.9 Pagetree (.ptree extension)
2 Utility formats
2.1 Scribble (.scrbl extension)
2.2 Null (.p extension)
3 Escaping output-file extensions within source-file names
6.2.900.15

File formats

1 Source formats

 #lang pollen/pre package: pollen
 #lang pollen/markdown
 #lang pollen/markup
 #lang pollen/ptree

The Pollen language is divided into variants, or dialects, that are tailored to suit each of the core source formats.

These dialects can be invoked one of two ways: either by invoking a specific dialect in the first line of the file (also known as the #lang line), or by using the generic #lang pollen as the first line, and then the correct dialect will be automatically selected based on the source file extension.

If the #lang line specifies a dialect different from the one specified by the file extension, the #lang line will take precedence.

For ease of use, the behavior of the Pollen language departs from the standard Racket language in several ways. The differences are noted below.

1.1 Command syntax using ◊

Commands must start with the special lozenge character . Other material is interpreted as plain text. See (part "pollen-command-syntax") for more.

How is this different from Racket? In Racket, everything is a command, and plain text must be quoted.

1.2 Any command is valid

There are no undefined commands in Pollen. If a command has not already been defined, it’s treated as a tag function. See (part "pollen-command-syntax") for more.

How is this different from Racket? In Racket, if you try to treat an identifier as a function before defining it with define, you’ll get an error.

1.3 Standard exports

By default, every Pollen source file exports two identifiers, which you can access by using the source file with require:

The main export, doc, contains the output of the file. The type of output depends on the source format (documented below).

The second export, metas, is a hashtable of key–value pairs with extra information that is extracted from the source. These metas will always contain the key 'here-path, which returns a string representation of the full path to the source file. Beyond that, the only metas are the ones that are specified within the source file (see the source formats below for more detail on how to specify metas).

Pollen source files also make the metas hashtable available through a submodule, also called metas. So rather than importing a source file with (require "source.html.pm"), you would (require (submod "source.html.pm" metas)). Accessing the metas this way avoids fully compiling the source file, and thus will usually be faster.

The names doc and metas can be changed for a project by overriding world:main-export and world:meta-export.

The Pollen rendering system relies on these two identifiers, but otherwise doesn’t care how they’re generated. Meaning, the code inside your Pollen source file could be #lang racket or #lang whatever. As long as you manually provide those two identifiers and follow the usual file-naming convention, your source file will be usable.

How is this different from Racket? In Racket, you must explicitly define and then provide any values you want to export.

1.4 Custom exports

Any value or function that is defined within the source file using define is automatically exported.

How is this different from Racket? In Racket, you must explicitly provide any values you want to export. Unlike Racket, every Pollen source file impliedly uses (provide (all-defined-out)).

1.5 The "pollen.rkt" file

If a file called "pollen.rkt" exists in the same directory with a source file, or in a parent directory of that source file, it’s automatically imported when the source file is compiled.

How is this different from Racket? In Racket, you must explicitly import files using require.

1.6 Preprocessor (.pp extension)

Invoke the preprocessor dialect by using #lang pollen/pre as the first line of your source file, or by using #lang pollen with a file extension of .pp. These forms are equivalent:

"sample.css.pp"

#lang pollen
...source...

"sample.css"

#lang pollen/pre
...source...

When no dialect is explicitly specified by either the #lang line or the file extension, Pollen will default to using the preprocessor dialect. For instance, this file will be treated as preprocessor source:

"test.yyz"

#lang pollen
...source...

Of course, you’re better off specifying the preprocessor dialect explicitly rather than relying on this default behavior.

The output of the preprocessor dialect, provided by 'doc, is plain text.

1.7 Markdown (.pmd extension)

Invoke the Markdown dialect by using #lang pollen/markdown as the first line of your source file, or by using #lang pollen with a file extension of .pmd. These forms are equivalent:

"sample.txt.pmd"

#lang pollen
...source...

"sample.txt"

#lang pollen/markdown
...source...

The output of the Markdown dialect, provided by doc, is a tagged X-expression.

1.8 Markup (.pm extension)

Invoke the Pollen markup dialect by using #lang pollen/markup as the first line of your source file, or by using #lang pollen with a file extension of .pm. These forms are equivalent:

"about.html.pm"

#lang pollen
...source...

"about.html"

#lang pollen/markup
...source...

The output of the Pollen markup dialect, provided by doc, is a tagged X-expression.

1.9 Pagetree (.ptree extension)

Invoke the pagetree dialect by using #lang pollen/ptree as the first line of your source file, or by using #lang pollen with a file extension of .ptree. These forms are equivalent:

"main.ptree"

#lang pollen
...source...

"main.rkt"

#lang pollen/ptree
...source...

The output of the pagetree dialect, provided by doc, is a pagetree? that is checked for correctness using validate-pagetree.

2 Utility formats

These aren’t source formats because they don’t contain a #lang pollen line. But for convenience, they get special handling by the Pollen project server.

2.1 Scribble (.scrbl extension)

Scribble files are recognized by the project server and can be compiled and previewed in single-page mode.

2.2 Null (.p extension)

Files with the null extension are simply rendered as a copy of the file without the extension, so "index.html.p" becomes "index.html".

This can be useful you’re managing your project with git. Most likely you’ll want to ignore "*.html" and other file types that are frequently regenerated by the project server. But if you have isolated static files — for instance, a "index.html" that doesn’t have source associated with it — they’ll be ignored too. You can cure this problem by appending the null extension to these static files, so they’ll be tracked in your source system without actually being source files.

3 Escaping output-file extensions within source-file names

Pollen relies extensively on the convention of naming source files by adding a source extension to an output-file name. So the Pollen markup source for "index.html" would be "index.html.pm".

This convention occasionally flummoxes other programs that assume a file can only have one extension. If you run into such a situation, you can escape the output-file extension using the world:extension-escape-char, which defaults to the underscore _.

So instead of "index.html.pm", your source-file name would be "index_html.pm". When this source file is rendered, it will automatically be converted into "index.html" (meaning, the escaped extension will be converted into a normal file extension).

This alternative-naming scheme is automatically enabled in every project. You can also set the escape character on a per-project basis (see world:current-extension-escape-char). Pollen will let you choose any character, but of course it would be unwise to pick one with special meaning in your filesystem (for instance, /).

 
\ No newline at end of file diff --git a/pollen/scribblings/module-reference.scrbl b/pollen/scribblings/module-reference.scrbl index fe15374..dac78b6 100644 --- a/pollen/scribblings/module-reference.scrbl +++ b/pollen/scribblings/module-reference.scrbl @@ -5,6 +5,7 @@ @local-table-of-contents[] @include-section["cache.scrbl"] +@include-section["core.scrbl"] @include-section["decode.scrbl"] @include-section["file.scrbl"] @include-section["pagetree.scrbl"] diff --git a/pollen/scribblings/render.scrbl b/pollen/scribblings/render.scrbl index 9551014..3e19cec 100644 --- a/pollen/scribblings/render.scrbl +++ b/pollen/scribblings/render.scrbl @@ -1,6 +1,6 @@ #lang scribble/manual -@(require scribble/eval pollen/render pollen/world (for-label racket (except-in pollen #%module-begin) pollen/world web-server/templates pollen/file sugar pollen/render)) +@(require scribble/eval pollen/render pollen/world (for-label racket (except-in pollen #%module-begin) pollen/world pollen/core web-server/templates pollen/file sugar pollen/render)) @(define my-eval (make-base-eval)) @(my-eval `(require pollen)) diff --git a/pollen/scribblings/tag.html b/pollen/scribblings/tag.html deleted file mode 100644 index 1ffef46..0000000 --- a/pollen/scribblings/tag.html +++ /dev/null @@ -1,2 +0,0 @@ - -Tag
Tag
Tag
make-default-tag-function
define-tag-function
6.3.0.14

Tag

 (require pollen/tag) package: pollen

Convenience functions for working with tags.

procedure

(make-default-tag-function id    
  kw-attr-name    
  kw-attr-value ...    
  ...)  (-> txexpr?)
  id : txexpr-tag?
  kw-attr-name : keyword?
  kw-attr-value : string?
Make a default tag function for id. The new tag function takes an optional set of X-expression attributes (txexpr-attrs?) followed by X-expression elements (txexpr-elements?). From these, the tag function creates a tagged X-expression using id as the tag.

Examples:
> (require pollen/tag)
> (define beaucoup (make-default-tag-function 'em))
> (beaucoup "Bonjour")

'(em "Bonjour")

> (beaucoup '((id "greeting")) "Bonjour")

'(em ((id "greeting")) "Bonjour")

Entering attributes this way can be cumbersome. So for convenience, the new tag function provides an alternative: any keyword arguments and their values will be interpreted as attributes.

Examples:
> (require pollen/tag)
> (define beaucoup (make-default-tag-function 'em))
> (beaucoup #:id "greeting" #:class "large" "Bonjour")

'(em ((class "large") (id "greeting")) "Bonjour")

You can also provide keyword arguments to make-default-tag-function itself, and they will become default attributes for every use of the tag function.

Examples:
> (require pollen/tag)
> (define beaucoup-small (make-default-tag-function 'em #:class "small"))
> (beaucoup-small #:id "greeting" "Bonjour")

'(em ((class "small") (id "greeting")) "Bonjour")

Pollen also uses this function to provide the default behavior for undefined tags. See #%top.

Note that while default tag functions are typically used to generate tagged X-expressions, they don’t enforce any restrictions on input, so they also do not guarantee that you will in fact get a valid tagged X-expression as output. This is intentional — default tag functions are a coding convenience, and their output is likely to be processed by other tag functions, so raising the error here would be premature.

Examples:
> (require pollen/tag)
> (define strange (make-default-tag-function 'div #:class "bizarre"))
; Invalid data types for elements
> (strange + *)

'(div ((class "bizarre")) #<procedure:+> #<procedure:*>)

; Double "class" attribute
> (strange #:class "spooky")

'(div ((class "bizarre") (class "spooky")))

syntax

(define-tag-function
(tag-id attr-id elem-id) body ...)
Helper function for making custom tag functions. Handles parsing chores, including conversion of keyword arguments into attributes (described in make-default-tag-function), and parses other attributes and elements normally.

Examples:
> (require pollen/tag)
> (define-tag-function (tag-name attrs elems)
    `(new-name ,(cons '(zim "zam") attrs) ,@elems))
> (tag-name "Hello world")

'(new-name ((zim "zam")) "Hello world")

> (tag-name '((key "value")) "Hello world")

'(new-name ((zim "zam") (key "value")) "Hello world")

> (tag-name #:key "value" "Hello world")

'(new-name ((zim "zam") (key "value")) "Hello world")

 
\ No newline at end of file diff --git a/pollen/scribblings/template.scrbl b/pollen/scribblings/template.scrbl index c9ad3f1..de4007a 100644 --- a/pollen/scribblings/template.scrbl +++ b/pollen/scribblings/template.scrbl @@ -11,117 +11,6 @@ Convenience functions for templates. These are automatically imported into the @racket[eval] environment when rendering with a template (see @racket[render]). -This module also re-exports everything from @racketmodname[pollen/template/html]. - - -@defproc[ -(get-doc -[doc-source (or/c pagenode? pathish?)]) -(or/c txexpr? string?)] -Retrieve the @racket[doc] export from @racket[_doc-source], which can be either a path, path string, or pagenode that can be resolved into a source path. If @racket[_doc-source] cannot be resolved, raise an error. - -If @racket[_doc-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). - -If @racket[world:current-main-export] has been overridden with a project-specific value, then that is retrieved instead. - - -@defproc[ -(get-metas -[meta-source (or/c pagenode? pathish?)]) -hash?] -Retrieve the @racket[metas] export from @racket[_meta-source], which can be either a path, path string, or pagenode that can be resolved into a source path. If @racket[_meta-source] cannot be resolved, raise an error. - -If @racket[_meta-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). - -If @racket[world:current-meta-export] has been overridden with a project-specific value, then that is retrieved instead. - - -@deftogether[( - -@defproc[ -(select -[key symbolish?] -[value-source (or/c hash? txexpr? pagenode? pathish?)]) -(or/c #f xexpr?)] - -@defproc[ -(select* -[key symbolish?] -[value-source (or/c hash? txexpr? pagenode? pathish?)]) -(or/c #f (listof xexpr?))] - -)] -Find matches for @racket[_key] in @racket[_value-source]. The @racket[_value-source] can be 1) a hashtable of @racket[metas], 2) a tagged X-expression representing a @racket[doc], or 3) a pagenode or path that identifies a source file that provides @racket[metas] and @racket[doc]. In that case, first look for @racket[_key] in @code{metas} (using @racket[select-from-metas]) and then in @code{doc} (using @racket[select-from-doc]). - -With @racket[select], you get the first result; with @racket[select*], you get them all. - -In both cases, you get @racket[#f] if there are no matches. - -Note that if @racket[_value-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). - -@examples[#:eval my-eval -(module nut-butters pollen/markup -'(div (question "Flavor?") - (answer "Cashew") (answer "Almond"))) -(code:comment @#,t{Import doc from 'nut-butters submodule}) -(require 'nut-butters) -(select 'question doc) -(select 'answer doc) -(select* 'answer doc) -(select 'nonexistent-key doc) -(select* 'nonexistent-key doc) -] - - -@defproc[ -(select-from-doc -[key symbolish?] -[doc-source (or/c txexpr? pagenodeish? pathish?)]) -(or/c #f (listof xexpr?))] -Look up the value of @racket[_key] in @racket[_doc-source]. The @racket[_doc-source] argument can be either 1) a tagged X-expression representing a @racket[doc] or 2) a pagenode or source path that identifies a source file that provides @racket[doc]. If no value exists for @racket[_key], you get @racket[#f]. - -Note that if @racket[_doc-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). - -@examples[#:eval my-eval -(module gelato pollen/markup -'(div (question "Flavor?") - (answer "Nocciola") (answer "Pistachio"))) -(code:comment @#,t{Import doc from 'gelato submodule}) -(require 'gelato) -(select-from-doc 'question doc) -('answer . select-from-doc . doc) -(select-from-doc 'nonexistent-key doc) -] - - - -@defproc[ -(select-from-metas -[key symbolish?] -[meta-source (or/c hash? pagenodeish? pathish?)]) -(or/c #f xexpr?)] -Look up the value of @racket[_key] in @racket[_meta-source]. The @racket[_meta-source] argument can be either 1) a hashtable representing @racket[metas] or 2) a pagenode or source path that identifies a source file that provides @racket[metas]. If no value exists for @racket[_key], you get @racket[#f]. - -Note that if @racket[_meta-source] is a relative path or pagenode, it is treated as being relative to @racket[world:current-project-root]. If that's not what you want, you'll need to convert it explicitly to a complete-path (e.g., with @racket[path->complete-path] or @racket[->complete-path]). - -@examples[#:eval my-eval -(define metas (hash 'template "sub.xml.pp" 'target "print")) -(select-from-metas 'template metas) -('target . select-from-metas . metas) -(select-from-metas 'nonexistent-key metas) -] - - -@defform[(when/splice condition pollen-args)] -If @racket[_condition] is true, put the @racket[_pollen-args] into the document. Within a template file, usually invoked like so: - -@verbatim{◊when/splice[@racketvarfont{condition}]{The text to insert.}} - -The inserted text can contain its own nested Pollen commands. - -@racket[when/splice] can be more convenient than @racket[when], because @racket[when] will only use the last argument between the curly braces. @racket[when/splice], by contrast, treats everything between the curly braces as a block. - - @section{HTML} diff --git a/pollen/scribblings/third-tutorial-files/template.html b/pollen/scribblings/third-tutorial-files/template.html deleted file mode 100644 index b05fb02..0000000 --- a/pollen/scribblings/third-tutorial-files/template.html +++ /dev/null @@ -1,15 +0,0 @@ - - - -◊select['h1 doc] by T. S. Eliot - - -◊->html[doc] -◊(define prev-page (previous here)) -◊when/splice[prev-page]{ -} -◊(define next-page (next here)) -◊when/splice[next-page]{ -} - - \ No newline at end of file diff --git a/pollen/scribblings/tutorial-fourth.scrbl b/pollen/scribblings/tutorial-fourth.scrbl index a47bdc7..4582e51 100644 --- a/pollen/scribblings/tutorial-fourth.scrbl +++ b/pollen/scribblings/tutorial-fourth.scrbl @@ -1,6 +1,6 @@ #lang scribble/manual -@(require scribble/eval racket/date (for-label racket/file racket/system pollen/decode plot pollen/world pollen/tag racket/base pollen/template txexpr racket/list racket/string pollen/render)) +@(require scribble/eval racket/date (for-label pollen/core racket/file racket/system pollen/decode plot pollen/world pollen/tag racket/base pollen/template txexpr racket/list racket/string pollen/render)) @(require "mb-tools.rkt") @(define my-eval (make-base-eval)) diff --git a/pollen/template.rkt b/pollen/template.rkt index 6c1a469..2ba689a 100644 --- a/pollen/template.rkt +++ b/pollen/template.rkt @@ -3,11 +3,4 @@ "template/base.rkt" "template/html.rkt") (provide (all-from-out "template/base.rkt" - "template/html.rkt")) - -(module-test-external - (check-equal? (select* 'key '#hash((key . "value"))) '("value")) - (check-equal? (select 'key '#hash((key . "value"))) "value") - - (define tx '(root (p "hello"))) - (check-equal? (->html tx) "

hello

")) \ No newline at end of file + "template/html.rkt")) \ No newline at end of file diff --git a/pollen/template/base.rkt b/pollen/template/base.rkt index a167cb0..d3ef171 100644 --- a/pollen/template/base.rkt +++ b/pollen/template/base.rkt @@ -1,115 +1,2 @@ #lang racket/base -(require (for-syntax racket/base "../world.rkt")) -(require racket/string xml xml/path sugar/define sugar/container sugar/coerce sugar/test racket/list) -(require "../file.rkt" txexpr "../world.rkt" "../cache.rkt" "../pagetree.rkt" "../private/debug.rkt") - -(define is-meta-value? hash?) -(define is-doc-value? txexpr?) -(define not-false? (λ(x) x)) - - -(define+provide/contract (select* key value-source) - (coerce/symbol? (or/c is-meta-value? is-doc-value? pagenode? pathish?) . -> . (or/c #f txexpr-elements?)) - (define metas-result (and (not (is-doc-value? value-source)) (select-from-metas key value-source))) - (define doc-result (and (not (is-meta-value? value-source)) (select-from-doc key value-source))) - (define result (filter not-false? (apply append (map ->list (list metas-result doc-result))))) - (and (pair? result) result)) - - -(define+provide/contract (select key value-source) - (coerce/symbol? (or/c is-meta-value? is-doc-value? pagenode? pathish?) . -> . (or/c #f txexpr-element?)) - (define result (select* key value-source)) - (and (pair? result) (car result))) - - -(module-test-external - (check-equal? (select* 'key '#hash((key . "value"))) '("value")) - (check-equal? (select 'key '#hash((key . "value"))) "value") - (check-false (select* 'absent-key '#hash((key . "value")))) - (check-false (select 'absent-key '#hash((key . "value")))) - (check-equal? (select* 'key '(root (key "value"))) '("value")) - (check-equal? (select 'key '(root (key "value"))) "value") - (check-false (select* 'absent-key '(root (key "value")))) - (check-false (select 'absent-key '(root (key "value")))) - (let ([metas '#hash((key . "value"))]) - (check-equal? (select* 'key metas) '("value")) - (check-equal? (select 'key metas) "value") - (check-false (select* 'absent-key metas)) - (check-false (select 'absent-key metas))) - (let ([doc '(root (key "value"))]) - (check-equal? (select* 'key doc) '("value")) - (check-equal? (select 'key doc) "value") - (check-false (select* 'absent-key doc)) - (check-false (select 'absent-key doc)))) - - -(define+provide/contract (select-from-metas key metas-source) - ;; output contract is a single txexpr-element - ;; because metas is a hash, and a hash has only one value for a key. - (coerce/symbol? (or/c is-meta-value? pagenode? pathish?) . -> . (or/c #f txexpr-element?)) - (define metas (if (is-meta-value? metas-source) - metas-source - (get-metas metas-source))) - (and (hash-has-key? metas key) (hash-ref metas key))) - -(module-test-external - (let ([metas '#hash((key . "value"))]) - (check-equal? (select-from-metas 'key metas) "value") - (check-false (select-from-metas 'absent-key metas)))) - - -(define+provide/contract (select-from-doc key doc-source) - ;; output contract is a list of elements - ;; because doc is a txexpr, and a txexpr can have multiple values for a key - (coerce/symbol? (or/c is-doc-value? pagenode? pathish?) . -> . (or/c #f txexpr-elements?)) - (define doc (if (is-doc-value? doc-source) - doc-source - (get-doc doc-source))) - (define result (se-path*/list (list key) doc)) - (and (pair? result) result)) - -(module-test-external - (check-equal? (select-from-doc 'key '(root (key "value"))) '("value")) - (check-false (select-from-doc 'absent-key '(root (key "value")))) - (let ([doc '(root (key "value"))]) - (check-equal? (select-from-doc 'key doc) '("value")) - (check-false (select-from-doc 'absent-key doc)))) - - -(define (convert+validate-path pagenode-or-path caller) - (let ([path (get-source (if (pagenode? pagenode-or-path) - (build-path (world:current-project-root) (symbol->string pagenode-or-path)) - pagenode-or-path))]) - (unless path - (error (format "~a no source found for '~a' in directory ~a" caller path (current-directory)))) - path)) - - -(define+provide/contract (get-metas pagenode-or-path) - ((or/c pagenode? pathish?) . -> . is-meta-value?) - (cached-metas (convert+validate-path pagenode-or-path 'get-metas))) - - -(define+provide/contract (get-doc pagenode-or-path) - ((or/c pagenode? pathish?) . -> . (or/c is-doc-value? string?)) - (cached-doc (convert+validate-path pagenode-or-path 'get-doc))) - -(provide when/splice) -(define-syntax (when/splice stx) - (syntax-case stx () - [(_ COND BODY ...) - (with-syntax ([SPLICING-TAG (datum->syntax stx (world:current-splicing-tag))]) - #'(if COND - (with-handlers ([exn:fail? (λ(exn) (error (format "within when/block, ~a" (exn-message exn))))]) - (list 'SPLICING-TAG BODY ...)) - ""))])) - -(provide when/block) ; bw compat -(define-syntax (when/block stx) - (syntax-case stx () - [(_ condition body ...) - #'(if condition (string-append* - (with-handlers ([exn:fail? (λ(exn) (error (format "within when/block, ~a" (exn-message exn))))]) - (map ->string (list body ...)))) - "")])) - +;; empty for now \ No newline at end of file diff --git a/pollen/test/test-lang-core.rkt b/pollen/test/test-lang-core.rkt new file mode 100644 index 0000000..5387202 --- /dev/null +++ b/pollen/test/test-lang-core.rkt @@ -0,0 +1,22 @@ +#lang racket/base +(require rackunit) + +;; check that automatic imports of pollen/core are present. + +(module markup pollen/markup + (define-meta zing "bam") + (select 'zing metas)) +(require (prefix-in markup: 'markup)) +(check-equal? markup:doc '(root "bam")) + +(module pre pollen/pre + (define-meta zing "bam") + (select 'zing metas)) +(require (prefix-in pre: 'pre)) +(check-equal? pre:doc "bam") + +(module markdown pollen/markdown + (define-meta zing "bam") + (select 'zing metas)) +(require (prefix-in markdown: 'markdown)) +(check-equal? markdown:doc '(root (p () "bam"))) diff --git a/pollen/test/test-lang-splice.rkt b/pollen/test/test-lang-splice.rkt index 7599a30..4966ea4 100644 --- a/pollen/test/test-lang-splice.rkt +++ b/pollen/test/test-lang-splice.rkt @@ -2,19 +2,16 @@ (require rackunit) (module markup pollen/markup - (require pollen/template) - "Hello" (when #t (@ "Splice")) (when/splice #t "Splice") "World") + "Hello" (when #t (@ "Splice")) "" (when/splice #t "Splice") "World") (require (prefix-in markup: 'markup)) (check-equal? markup:doc '(root "Hello" "Splice" "Splice" "World")) (module pre pollen/pre - (require pollen/template) - "Hello" (when #t (@ "Splice")) (when/splice #t "Splice") "World") + "Hello" (when #t (@ "Splice")) "" (when/splice #t "Splice") "World") (require (prefix-in pre: 'pre)) (check-equal? pre:doc "HelloSpliceSpliceWorld") (module markdown pollen/markdown - (require pollen/template) - "Hello" (when #t (@ "Splice")) (when/splice #t "Splice") "World") + "Hello" (when #t (@ "Splice")) "" (when/splice #t "Splice") "World") (require (prefix-in markdown: 'markdown)) (check-equal? markdown:doc '(root (p () "HelloSpliceSpliceWorld")))