From 65fcc0ec150f8c170ef4fd70515b71eaa2677899 Mon Sep 17 00:00:00 2001 From: Matthew Butterick Date: Sun, 21 Jun 2015 17:20:14 -0700 Subject: [PATCH] permit more flexible `meta` markup (closes #57) --- metas.rkt | 86 +++++++++++++++++++++++++++--------- scribblings/command.scrbl | 93 ++++++++++++++++++++++++++++----------- world.rkt | 1 + 3 files changed, 134 insertions(+), 46 deletions(-) diff --git a/metas.rkt b/metas.rkt index f0c9f94..2a677aa 100644 --- a/metas.rkt +++ b/metas.rkt @@ -1,35 +1,72 @@ #lang racket/base -(require racket/list pollen/top txexpr) ; pollen/top needed for metaroot +(require racket/list pollen/top txexpr pollen/world) ; pollen/top needed for metaroot (provide split-metas-to-hash) +(require sugar) + +(module+ test + (require rackunit)) + + +(define (possible-meta-element? x) + (and (txexpr? x) (equal? world:meta-tag-name (get-tag x)))) + + +(define (trivial-meta-element? x) + (and (possible-meta-element? x) (not (nontrivial-meta-element? x)))) + + +(define (has-meta-attrs x) + (let ([attrs (get-attrs x)]) + (and (not (empty? attrs)) (andmap valid-meta-attr? attrs)))) + + +(define (has-meta-elements x) + (not (empty? (filter txexpr? (get-elements x))))) + + +(define (nontrivial-meta-element? x) + (and (possible-meta-element? x) + (or (has-meta-attrs x) (has-meta-elements x)))) + + (define (meta-element? x) (or (trivial-meta-element? x) (nontrivial-meta-element? x))) -(define (trivial-meta-element? x) ; trivial meta has no attributes. - (and (possible-meta-element? x) (empty? (get-attrs x)))) -(define (nontrivial-meta-element? x) ; nontrivial meta has attributes that are valid. - (and (possible-meta-element? x) - (let ([attrs (get-attrs x)]) - (and (not (empty? attrs)) (andmap valid-meta-attr? attrs))))) +(module+ test + (check-true (nontrivial-meta-element? '(meta ((foo "bar"))))) + (check-true (nontrivial-meta-element? '(meta (foo "bar")))) + (check-true (trivial-meta-element? '(meta))) + (check-true (trivial-meta-element? '(meta "bar")))) + -(define (possible-meta-element? x) - (and (txexpr? x) (equal? 'meta (get-tag x)))) (define (valid-meta-attr? x) (or (and (list? x) (symbol? (first x)) (string? (second x))) (error 'is-meta-element? "error: meta must be a symbol / string pair, instead got: ~v" x))) + +(define (make-atomic-meta key . values) + `(meta (,key ,@values))) + + (define (explode-meta-element me) - ;; convert a meta with multiple key/value pairs into multiple metas with a single key/value pair + ;; convert a meta with multiple key/value pairs into multiple metas with a single txexpr element ;; only gets nontrivial metas to start. (let loop ([me me][acc empty]) (cond - [(not (trivial-meta-element? me)) ; meta might become trivial during loop (no attrs) - (define attrs (get-attrs me)) - (loop `(meta ,(cdr attrs)) (cons `(meta ,(list (car attrs))) acc))] + [(not (trivial-meta-element? me)) ; meta might become trivial during loop + (cond + [(has-meta-attrs me) ; might have txexpr elements, so preserve them + (define attrs (get-attrs me)) + (loop `(meta ,(cdr attrs) ,@(get-elements me)) (cons (apply make-atomic-meta (car attrs)) acc))] + [else ; has txexpr elements, but not meta-attrs + (define txexpr-elements (filter txexpr? (get-elements me))) + (loop `(meta () ,@(cdr txexpr-elements)) (cons (apply make-atomic-meta (car txexpr-elements)) acc))])] [else (reverse acc)]))) + (define (split-meta-elements x) ; pull metas out of doc and put them into meta-elements accumulator (when (not (txexpr? x)) (error 'split-meta-elements "Not a txexpr: ~v" x)) @@ -38,6 +75,7 @@ (define exploded-meta-elements (append-map explode-meta-element (filter nontrivial-meta-element? meta-elements))) (values thing-without-meta-elements exploded-meta-elements)) + (define (split-metas-to-hash x) (define-values (doc-without-metas meta-elements) (split-meta-elements x)) ;; 'metaroot is the hook for the meta decoder function. @@ -45,9 +83,12 @@ ;; because of `explode-meta-element`, meta-elements will be a list of metas with a single key/value pair ;; metaroot can rely on this (define metas-xexpr (apply metaroot meta-elements)) - (define (first-attribute x) (car (get-attrs x))) - (define (meta-key x) (first (first-attribute x))) - (define (meta-value x) (second (first-attribute x))) + (define (first-attribute x) (car (get-elements x))) + (define (meta-key x) (car (first-attribute x))) + (define (meta-value x) (let ([rest (cdr (first-attribute x))]) + (if (= (length rest) 1) + (car rest) + rest))) (define (meta-element->assoc me) (cons (meta-key me) (meta-value me))) (define metas (make-hash (map meta-element->assoc (cdr metas-xexpr)))) (values doc-without-metas metas)) @@ -55,7 +96,12 @@ (module+ test (require rackunit) - (define x '(root (meta ((foo "bar"))) "hello" (p (meta ((foo "zam"))) (meta) "there"))) - (define-values (doc-without-metas metahash) (split-metas-to-hash x)) - (check-equal? doc-without-metas '(root "hello" (p "there"))) - (check-equal? (hash-ref metahash 'foo) "zam")) \ No newline at end of file + (let ([x '(root (meta ((foo "bar"))) "hello" (p (meta ((foo "zam"))) (meta) "there"))]) + (define-values (doc-without-metas metahash) (split-metas-to-hash x)) + (check-equal? doc-without-metas '(root "hello" (p "there"))) + (check-equal? (hash-ref metahash 'foo) "zam")) + + (let ([x '(root (meta (foo "bar")) "hello" (p (meta (foo (zim "zam"))) (meta) "there"))]) + (define-values (doc-without-metas metahash) (split-metas-to-hash x)) + (check-equal? doc-without-metas '(root "hello" (p "there"))) + (check-equal? (hash-ref metahash 'foo) '(zim "zam")))) \ No newline at end of file diff --git a/scribblings/command.scrbl b/scribblings/command.scrbl index 9cefeaf..687c5cb 100644 --- a/scribblings/command.scrbl +++ b/scribblings/command.scrbl @@ -450,23 +450,22 @@ The value of edge is ◊|edge| pixels} @;-------------------------------------------------------------------- @subsubsection{Inserting metas} -@italic{Metas} are key–value pairs embedded in a source file that are not included in the main output when the source is run, and collected into a separate hash table. +@italic{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, @racket[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). -Metas are not a foundational abstraction. They're just a convenience — a place to store arbitrary pieces of information that you might want to use later. +@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].} -@margin-note{Pollen occasionally uses metas. 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 insert a meta, use the standard command syntax for inserting a tag with an attribute pair, but use the special @code{meta} name: +To make a meta, you create a tag with the special @code{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 -◊some-tag['key: "value"]{Normal tag} ◊meta['dog: "Roxy"] +◊some-tag['key: "value"]{Normal tag} +◊meta{◊cat{Chopper}} ◊some-tag['key: "value"]{Another normal tag} } -When you mark a meta like this, two things happen. First, when you run the file, the meta is removed from the result: +When you run a source file with metas in it, two things happen. First, the metas are removed from the output: @repl-output{ '(some-tag ((key "value")) "Normal tag") @@ -474,68 +473,110 @@ When you mark a meta like this, two things happen. First, when you run the file, '(some-tag ((key "value")) "Another normal tag") } -@margin-note{If your @code{meta} includes a text argument between curly braces, it will be ignored.} -Second, the meta is collected into a hash table that is exported with the name @code{metas}. To see this hash table, run the file above in DrRacket, then move to the interactions window and type @exec{metas} at the prompt: +Second, the metas are collected into a hash table that is exported with the name @code{metas}. To see this hash table, run the file above in DrRacket, then switch to the interactions window and type @exec{metas} at the prompt: @terminal{ > metas -'#hash((here-path . "unsaved-editor167056") (dog . "Roxy")) +'#hash((dog . "Roxy") (cat . "Chopper") (here-path . "unsaved-editor")) } -The only key that's automatically defined in every meta table is @code{here-path}, which is the absolute path to the source file. (Here, because the file hasn't been saved, you'll see the @code{unsaved-editor...} name instead.) +The only key that's automatically defined in every meta table is @code{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 @code{unsaved-editor} name instead.) Still, you can override this too: @codeblock{ #lang pollen -◊some-tag['key: "value"]{Normal tag} ◊meta['dog: "Roxy"] +◊some-tag['key: "value"]{Normal tag} +◊meta{◊cat{Chopper}} ◊some-tag['key: "value"]{Another normal tag} -◊meta['here-path: "nowhere"] +◊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: @terminal{ > metas -'#hash((dog . "Roxy") (here-path . "nowhere")) +'#hash((dog . "Roxy") (cat . "Chopper") (here-path . "tesseract")) } -It doesn't matter how many metas you put in a source file, or where you put them. They'll all be extracted into the @code{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: + +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 @code{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: @codeblock{ #lang pollen - -◊some-tag['key: "value"]{Normal tag} ◊meta['dog: "Roxy"] -◊some-tag['key: "value"]{Another normal tag} -◊meta['dog: "Lex"] +◊meta{◊dog{Lex}} } -Though there are two metas named @racket['dog], only the second one persists: +In this case, though there are two metas named @racket[dog] (and they use different forms) only the second one persists: @terminal{ > metas -'#hash((dog . "Lex") (here-path . "unsaved-editor167056")) +'#hash((dog . "Lex") (here-path . "unsaved-editor")) } -You're allowed to put multiple keys and values within a single @code{meta} tag. As above, later keys supersede earlier ones. +You can put multiple keys and values within a single @code{meta} tag, and you can mix them between attributes and elements. As above, later keys supersede earlier ones. @codeblock{ #lang pollen +◊meta['dog: "Roxy" 'lion: "P22"]{◊dog{Lex}} +} -◊some-tag['key: "value"]{Normal tag} -◊meta['dog: "Roxy" 'lion: "P22" 'dog: "Lex"] -◊some-tag['key: "value"]{Another normal tag} +@terminal{ +> metas +'#hash((dog . "Lex") (here-path . "unsaved-editor") (lion . "P22")) +} + +Should you store your metas as attributes or elements? That's up to you, but elements are more flexible. When your key-value pair is stored as an attribute, your value has to be a string (because that's the only datatype an attribute can hold). Whereas when your key-value pair is stored as an element, you have two extra possiblilites. + +First, the value can be any X-expression. For instance, this code uses an @racket[img] tag as the meta value: + +@codeblock{ +#lang pollen +◊meta['dog: "Roxy"] +◊meta{◊dog{◊img['src: "lex.gif"]}} } @terminal{ > metas -'#hash((dog . "Lex") (here-path . "unsaved-editor167056") (lion . "P22")) +'#hash((dog . (img ((src "roxy.gif")))) (here-path . "unsaved-editor")) } +Second, if you use an element, the value can be either a single value or a list or values: + +@codeblock{ +#lang pollen +◊meta['dog: "Roxy"] +◊meta{◊categories['brindles 'boxers 'working-dogs]} +} + +@terminal{ +> metas +'#hash((dog . "Roxy") (here-path . "unsaved-editor") (categories . (brindles boxers working-dogs))) +} + +Be aware that if you put things inside a @racket[meta] tag that don't qualify as key-value pairs, Pollen will just discard them. So don't be surprised when this: + +@codeblock{ +#lang pollen +◊meta['dog: "Roxy"]{This text will be ignored} +◊meta[◊cat{Chopper}]{As will this text} +} + +Gets treated as if you wrote it this way: + +@codeblock{ +#lang pollen +◊meta['dog: "Roxy"] +◊meta[◊cat{Chopper}] +} + + + + @;-------------------------------------------------------------------- @subsubsection{Inserting a comment} diff --git a/world.rkt b/world.rkt index 37f07b4..fc438f8 100644 --- a/world.rkt +++ b/world.rkt @@ -36,6 +36,7 @@ (define main-pollen-export 'doc) ; don't forget to change fallback template too (define meta-pollen-export 'metas) +(define meta-tag-name 'meta) (define directory-require "directory-require.rkt")