├── .gitignore ├── LICENSE.md ├── README.md ├── punct-doc ├── info.rkt ├── makefile ├── punct.scrbl └── tools.rkt ├── punct-lib ├── core.rkt ├── doc.rkt ├── element.rkt ├── fetch.rkt ├── info.rkt ├── main.rkt ├── parse.rkt ├── private │ ├── configure-runtime.rkt │ ├── constants.rkt │ ├── doclang-raw.rkt │ ├── main.rkt │ ├── pack.rkt │ ├── quasi-txpr.rkt │ └── reader-utils.rkt └── render │ ├── base.rkt │ ├── html.rkt │ └── plaintext.rkt ├── punct-tests ├── info.rkt ├── test-md-basic.page.rkt ├── test-metas-multibyte.page.rkt ├── test-metas.page.rkt └── test.rkt └── punct └── info.rkt /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#* 3 | .\#* 4 | .DS_Store 5 | *.swp 6 | compiled/ 7 | *.html 8 | *.js 9 | *.css 10 | doc/ 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Blue Oak Model License 2 | 3 | Version 1.0.0 4 | 5 | ## Purpose 6 | 7 | This license gives everyone as much permission to work with this software as possible, while 8 | protecting contributors from liability. 9 | 10 | ## Acceptance 11 | 12 | In order to receive this license, you must agree to its rules. The rules of this license are both 13 | obligations under that agreement and conditions to your license. You must not do anything with this 14 | software that triggers a rule that you cannot or will not follow. 15 | 16 | ## Copyright 17 | 18 | Each contributor licenses you to do everything with this software that would otherwise infringe that 19 | contributor's copyright in it. 20 | 21 | ## Notices 22 | 23 | You must ensure that everyone who gets a copy of any part of this software from you, with or without 24 | changes, also gets the text of this license or a link to . 25 | 26 | ## Excuse 27 | 28 | If anyone notifies you in writing that you have not complied with [Notices](#notices), you can keep 29 | your license by taking all practical steps to comply within 30 days after the notice. If you do not 30 | do so, your license ends immediately. 31 | 32 | ## Patent 33 | 34 | Each contributor licenses you to do everything with this software that would otherwise infringe any 35 | patent claims they can license or become able to license. 36 | 37 | ## Reliability 38 | 39 | No contributor can revoke this license. 40 | 41 | ## No Liability 42 | 43 | ***As far as the law allows, this software comes as is, without any warranty or condition, and no 44 | contributor will be liable to anyone for any damages related to this software or this license, under 45 | any kind of legal claim.*** 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Punct 2 | 3 | Punct is a programming environment for publishing things, implemented in Racket. It uses inline 4 | Racket code to extend CommonMark-flavored Markdown, which is parsed into a format-independent AST 5 | that can be rendered in HTML (or any other target file type). 6 | 7 | **Documentation is at **. 8 | 9 | **If you decide to rely on Punct in “production”, you should monitor the [Announcements][a] area of 10 | this repository. Any significant or breaking changes will be announced there first.** 11 | 12 | [a]: https://github.com/otherjoel/punct/discussions/categories/announcements 13 | 14 | ## Installation 15 | 16 | Clone this repository, and from within the checkout’s root folder, do `raco pkg install --link 17 | punct-lib/ punct-doc/` (note the trailing slashes). 18 | 19 | Once this is done, try it out by following along with the [Quick Start][qs]. 20 | 21 | [qs]: https://joeldueck.com/what-about/punct/Quick_start.html 22 | 23 | -------------------------------------------------------------------------------- /punct-doc/info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection "punct") 4 | (define scribblings '(("punct.scrbl"))) 5 | 6 | (define deps '("scribble-lib" 7 | "base")) 8 | (define build-deps '("commonmark-doc" 9 | "commonmark-lib" 10 | "racket-doc" 11 | "scribble-doc" 12 | "punct-lib")) 13 | 14 | (define update-implies '("punct-lib")) 15 | 16 | (define pkg-desc "documentation part of \"punct\"") 17 | (define license 'BlueOak-1.0.0) 18 | -------------------------------------------------------------------------------- /punct-doc/makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | scribble: punct.scrbl 4 | scribble: ## Rebuild Scribble docs 5 | rm -rf punct/* 6 | scribble --htmls +m --redirect https://docs.racket-lang.org/local-redirect/ punct.scrbl 7 | 8 | publish: ## Sync Scribble HTML docs to web server (doesn’t rebuild anything) 9 | rsync -av --delete punct/ $(JDCOM_SRV)what-about/punct/ 10 | 11 | # Self-documenting makefile (http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html) 12 | help: ## Displays this help screen 13 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 14 | 15 | .PHONY: help publish 16 | 17 | .DEFAULT_GOAL := help 18 | -------------------------------------------------------------------------------- /punct-doc/punct.scrbl: -------------------------------------------------------------------------------- 1 | #lang scribble/manual 2 | 3 | @(require [for-label commonmark 4 | punct/core 5 | punct/doc 6 | punct/element 7 | punct/fetch 8 | punct/parse 9 | punct/render/base 10 | punct/render/html 11 | punct/render/plaintext 12 | racket/base 13 | racket/class 14 | racket/contract/base 15 | racket/match 16 | (only-in xml xexpr? xexpr->string)]) 17 | 18 | @(require scribble/examples "tools.rkt") 19 | @(define ev (sandbox)) 20 | 21 | @(ev '(require punct/doc punct/parse punct/element)) 22 | 23 | @title[#:style '(toc)]{Punct: CommonMark + Racket} 24 | @author[(author+email "Joel Dueck" "joel@jdueck.net")] 25 | 26 | @defmodulelang[punct] 27 | 28 | Punct is a programming environment for publishing things, implemented in Racket. Punct’s two basic 29 | ideas are: 30 | 31 | @itemlist[#:style 'ordered 32 | 33 | @item{Markdown documents (parsed by @racketmodname[commonmark]), extensible with Racket code.} 34 | 35 | @item{Multiple output formats. A Punct program/document produces a format-independent AST.}] 36 | 37 | The latest version of this documentation can be found at 38 | @link["https://joeldueck.com/what-about/punct/"]{@tt{joeldueck.com}}. The source and installation 39 | instructions are at the project’s @link["https://github.com/otherjoel/punct"]{GitHub repo}. 40 | 41 | @margin-note{If you decide to rely on Punct in any kind of “production” capacity, you should make 42 | sure to monitor the @link["https://github.com/otherjoel/punct/pulls"]{pull requests} and 43 | @link["https://github.com/otherjoel/punct/discussions/categories/announcements"]{Announcements} 44 | areas of the GitHub repository. Any significant changes will be announced there first.} 45 | 46 | @youtube-embed-element["https://www.youtube.com/embed/9zxna1tlvHU"] 47 | 48 | I have designed Punct for my own use and creative needs. If you would like Punct to work differently 49 | or support some new feature, I encourage you to fork it and customize it yourself. 50 | 51 | This documentation assumes you are familiar with Racket, and with @racketlink[xexpr?]{X-expressions} 52 | and associated terms (attributes, elements, etc). 53 | 54 | @local-table-of-contents[] 55 | 56 | @section{Quick start} 57 | 58 | Open DrRacket and start a new file like so: 59 | 60 | @filebox["Untitled 1"]{@codeblock{ 61 | #lang punct 62 | 63 | --- 64 | author: Me 65 | --- 66 | 67 | # My first Punct doc 68 | 69 | Simple. 70 | }} 71 | 72 | As you can see, this document uses 73 | @hyperlink["https://www.markdownguide.org/basic-syntax/"]{Markdown} formatting and has a little 74 | metadata block near the beginning. It’s essentially a normal Markdown file, very much like one you 75 | would use with most publishing systems. The only thing that makes it different is the addition of 76 | @racketmodfont{#lang punct} at the top. 77 | 78 | Now click the @onscreen{Run} button in the toolbar. Punct will parse the document’s Markdown content 79 | and metadata, and produce a @racket[document] struct containing the metadata and an Abstract Syntax 80 | Tree (AST): 81 | 82 | @racketblock[ 83 | '#s(document #hasheq((author . "Me") (here-path . "7-unsaved-editor")) 84 | ((heading ((level "1")) "My first Punct doc") (paragraph "Simple.")) 85 | ())] 86 | 87 | This value is automatically bound to @racketid[doc]. The metadata at the top is included in that 88 | value, but is also bound to @racketid[metas]. 89 | 90 | @(ev '(define doc 91 | '#s(document #hasheq((author . "Me") (here-path . "7-unsaved-editor")) 92 | ((heading ((level "1")) "My first Punct doc") (paragraph "Simple.")) 93 | ()))) 94 | 95 | @(ev '(define metas '#hasheq((author . "Me") (here-path . "7-unsaved-editor")))) 96 | 97 | @examples[#:eval ev 98 | #:label #f 99 | doc 100 | metas] 101 | 102 | Both of these bindings are also @racket[provide]d so you can access them from other modules with 103 | @racket[require] or @racket[dynamic-require]. 104 | 105 | You can render @racketid{doc} to HTML by passing it to @racket[doc->html]. In the REPL: 106 | 107 | @examples[#:eval ev 108 | #:label #f 109 | (require punct/render/html) 110 | (doc->html doc)] 111 | 112 | You can escape to Racket code using the @litchar{•} “bullet” character (@tt{U+2022}): 113 | 114 | @codeblock{ 115 | #lang punct 116 | 117 | Today we’re computing •(+ 1 2). 118 | 119 | •string-upcase{keep it down, buddy.} 120 | } 121 | 122 | @margin-note{Punct does not care what extension you use for your filenames. Using @filepath{.rkt} is 123 | the simplest thing to do if you are using DrRacket, but you can use whatever you want. I use 124 | @filepath{.page.rkt} in my projects.} 125 | 126 | Any simple values produced by Racket code are converted to strings at compile time. If these strings 127 | contain valid Markdown, they will be parsed along with the rest of the document. The code below will 128 | produce a bulleted list: 129 | 130 | @codeblock{ 131 | #lang punct 132 | 133 | Three things to remember: 134 | 135 | •(apply string-append 136 | (map (λ (s) (format "* ~a\n" (string-upcase s))) 137 | '("keep" "it" "down"))) 138 | } 139 | 140 | Results in: 141 | 142 | @racketblock[ 143 | '#s(document #hasheq((here-path . "7-unsaved-editor")) 144 | ((paragraph "Three things to remember:") 145 | (itemization ((style "tight") (start "#f")) 146 | (item "KEEP") 147 | (item "IT") 148 | (item "DOWN"))) 149 | ())] 150 | 151 | @section{Writing Punct} 152 | 153 | Start your Punct source file with @racketmodfont{#lang punct}. Then just write in 154 | CommonMark-flavored Markdown. 155 | 156 | Punct allows inline Racket code that follows @secref["reader" #:doc '(lib 157 | "scribblings/scribble/scribble.scrbl")] but with the @litchar{•} “bullet” character (@tt{U+2022}) as 158 | the control character instead of @litchar{@"@"}. On Mac OS, you can type this using @tt{ALT+8}. 159 | 160 | Punct source files automaticaly @racket[provide] two bindings: @racketidfont{doc} (a 161 | @racket[document]) and @racketidfont{metas} (a hash table). 162 | 163 | @subsection{Using @racket[require]} 164 | 165 | By default, Punct programs have access to the bindings in @racketmodname[racket/base] and 166 | @racketmodname[punct/core]. You can import bindings from other modules in two ways: 167 | 168 | @itemlist[#:style 'ordered 169 | 170 | @item{By using @racket[require] as you usually would, or} 171 | 172 | @item{By adding one or more @secref["module-paths" #:doc '(lib "scribblings/guide/guide.scrbl")] 173 | directly on the @hash-lang[] line.}] 174 | 175 | @codeblock{ 176 | #lang punct "my-module.rkt" racket/math 177 | 178 | •; All bindings in "my-module.rkt" and racket/math are now available 179 | •; You can also just use require normally 180 | •(require racket/string) 181 | } 182 | 183 | @subsection[#:tag "metas block"]{Metadata block} 184 | 185 | Sources can optionally add metadata using @racketvalfont{key: value} lines, delimited by lines 186 | consisting only of consecutive hyphens: 187 | 188 | @codeblock{ 189 | #lang punct 190 | --- 191 | title: Prepare to be amazed 192 | date: 2020-05-07 193 | draft?: '#t 194 | --- 195 | 196 | Regular content goes here 197 | } 198 | 199 | This is a syntactic convenience that comes with a few rules and limitations: 200 | 201 | @itemlist[#:style 'compact 202 | 203 | @item{The metadata block must be the first non-whitespace thing that follows the 204 | @racketmodfont{#lang} line.} 205 | 206 | @item{Each value will always be parsed as a flat string --- or, if prefixed with a single quote 207 | @litchar{'}, as a simple datum (using @racket[read]). If more than one datum appears after the 208 | @litchar{'}, the first will be used and the rest discarded.} 209 | 210 | ] 211 | 212 | Prefixing meta values with @litchar{'} allows you to store booleans and numbers, as well as complex 213 | values like lists, vectors, hash tables, or anything else that @racket[read] would count as a single 214 | datum (and which fits in one line) --- but note that code inside the value will not be evaluated. 215 | 216 | If you want to use the results of expressions in your metadata, you can use the @racket[set-meta] 217 | function anywhere in the document or in code contained in other modules. Within the document body 218 | you can also use the @racket[?] macro as shorthand for @racket[set-meta]. 219 | 220 | @history[#:changed "1.2" @elem{Added ability to use datums quoted with @litchar{'} in metadata.}] 221 | 222 | @subsection{Markdown and Racket} 223 | 224 | When evaluating a source file, Punct does things in this order: 225 | 226 | @itemlist[#:style 'ordered 227 | 228 | @item{The metadata block is parsed and its values added to @racket[current-metas].} 229 | 230 | @item{Any inline Racket expressions are evaluated and replaced with their results. Tagged 231 | X-expressions are preserved in the final document structure (see @secref{custom}). Any non-string 232 | value other than a list, and any list beginning with something other than a symbol, is coerced into 233 | a string.} 234 | 235 | @item{The entire document is run through the @racketmodname[commonmark] parser, producing a 236 | @racket[document] which is bound to @racketidfont{doc}.} 237 | 238 | ] 239 | 240 | @section{Document structure} 241 | 242 | Because it uses the @racketmodname[commonmark] parser as a starting point, Punct documents come with 243 | a default structure that is fairly opinionated. You can augment this structure if you understand how 244 | the pieces fit together. 245 | 246 | @defmodule[punct/doc] 247 | 248 | The bindings provided by this module are also provided by @racketmodname[punct/core]. 249 | 250 | @defstruct[document ([metas hash-eq?] [body (listof block-element?)] [footnotes (listof block-element?)]) #:prefab]{ 251 | 252 | A Punct source file evaluates to a @racket[document] struct that includes a @racket[_metas] hash 253 | table containing any metadata defined using the @secref{metas block}, @racket[?] or 254 | @racket[set-meta]; and @racket[_body] and @racket[_footnotes], both of which are lists of 255 | @tech{block elements}. 256 | 257 | Behind the scenes: the @racketmodname[commonmark] parser produces a body and footnote definitions in 258 | the form of nested structs. Punct converts both of these into lists of tagged X-expressions, to 259 | allow for greater flexibility in adding @secref{custom}. 260 | 261 | @history[#:changed "1.0" @elem{@racket[_body] and @racket[_footnotes] now guaranteed to be valid 262 | X-expressions and not simply lists.}] 263 | 264 | } 265 | 266 | @subsection{Blocks and Flows} 267 | 268 | @defproc[(block-element? (v any/c)) boolean?]{ 269 | 270 | A @deftech{block element} in Punct is a tagged X-expression which counts as a structural part of a 271 | document: it starts with one of @racket['heading], @racket['paragraph], @racket['itemization], 272 | @racket['item], @racket['blockquote], @racket['code-block], @racket['html-block], 273 | @racket['footnote-definition] or @racket['thematic-break]. At the highest level, a document is a 274 | sequence of these block elements. 275 | 276 | @examples[#:label #f #:eval ev 277 | (block-element? '(paragraph "Block party!")) 278 | (block-element? "simple string") 279 | ] 280 | 281 | A @deftech{flow} is a list of @tech{block elements}. The Markdown parser produces three block 282 | elements that may contain flows: @racketid[blockquote], @racketid[item], and 283 | @racketid[footnote-definition]. 284 | 285 | } 286 | 287 | @deftogether[( 288 | @defform[#:kind "txexpr" #:link-target? #f #:literals (level) 289 | (heading [[level lev-str]] content ...) 290 | #:contracts ([lev-str (or/c "1" "2" "3" "4" "5" "6")] 291 | [content xexpr?])] 292 | 293 | @defform[#:kind "txexpr" #:link-target? #f (paragraph content ...) 294 | #:contracts ([content xexpr?])] 295 | 296 | @defform[#:kind "txexpr" #:link-target? #f #:literals (style start) 297 | (itemization [[style style-str] [start maybe-start]] item ...) 298 | #:contracts ([style-str (or/c "loose" "tight")] 299 | [maybe-start (or/c "" string?)] 300 | [item xexpr?])] 301 | 302 | @defform[#:kind "txexpr" #:link-target? #f (item block ...) 303 | #:contracts ([block xexpr?])] 304 | 305 | @defform[#:kind "txexpr" #:link-target? #f (blockquote block ...) 306 | #:contracts ([block block-element?])] 307 | 308 | @defform[#:kind "txexpr" #:link-target? #f #:literals (info) 309 | (code-block [[info info-str]] content ...) 310 | #:contracts ([info-str string?] 311 | [content xexpr?])] 312 | 313 | @defform[#:kind "txexpr" #:link-target? #f (html-block content ...) 314 | #:contracts ([content xexpr?])] 315 | 316 | @defform[#:kind "txexpr" #:link-target? #f #:literals (label ref-count) 317 | (footnote-definition [[label lbl] [ref-count rcount]] content ...) 318 | #:contracts ([lbl string?] 319 | [rcount string?] 320 | [content block-element?])] 321 | 322 | @defform[#:kind "txexpr" #:link-target? #f (thematic-break)] 323 | 324 | )] 325 | 326 | @subsection{Inline elements} 327 | 328 | @defproc[(inline-element? [v any/c]) boolean?]{ 329 | 330 | An @deftech{inline element} in Punct is a string, or any tagged X-expression that is not counted as 331 | a @tech{block element}. 332 | 333 | @examples[#:label #f #:eval ev 334 | (inline-element? "simple string") 335 | (inline-element? '(italic "emphasis")) 336 | (inline-element? '(made-up-element "x")) 337 | (inline-element? '(paragraph "Block party!")) 338 | ] 339 | 340 | Inline elements that appear on a line by themselves (i.e., not marked up within block elements) are 341 | automatically wrapped in @racketid[paragraph] elements. 342 | 343 | } 344 | 345 | Below is a list of the inline elements that can be produced by the Markdown parser. 346 | 347 | @deftogether[( 348 | 349 | @defform[#:kind "txexpr" #:link-target? #f (italic content ...) 350 | #:contracts ([content inline-element?])] 351 | 352 | @defform[#:kind "txexpr" #:link-target? #f (bold content ...) 353 | #:contracts ([content inline-element?])] 354 | 355 | @defform[#:kind "txexpr" #:link-target? #f #:literals (dest title) 356 | (link [[dest href] [title title-str]] content ...) 357 | #:contracts ([href string?] 358 | [title-str string?] 359 | [content inline-element?])] 360 | 361 | @defform[#:kind "txexpr" #:link-target? #f (code content ...) 362 | #:contracts ([content inline-element?])] 363 | 364 | @defform[#:kind "txexpr" #:link-target? #f #:literals (src title desc) 365 | (image [[src source] [title title-str] [desc description]]) 366 | #:contracts ([source string?] 367 | [title-str string?] 368 | [description string?])] 369 | 370 | @defform[#:kind "txexpr" #:link-target? #f (html content ...) 371 | #:contracts ([content inline-element?])] 372 | 373 | @defform[#:kind "txexpr" #:link-target? #f #:literals (label defn-num ref-num) 374 | (footnote-reference [[label lbl] [defn-num dnum] [ref-num rnum]]) 375 | #:contracts ([lbl string?] 376 | [dnum string?] 377 | [rnum string?])] 378 | 379 | @defform[#:kind "txexpr" #:link-target? #f (line-break)] 380 | 381 | )] 382 | 383 | @subsection[#:tag "custom"]{Custom elements} 384 | 385 | You can use Racket code to introduce new elements to the document’s structure. 386 | 387 | A @deftech{custom element} is any list that begins with a symbol, and which was produced by inline 388 | Racket code rather than by parsed Markdown syntax. 389 | 390 | @margin-note{If you think “custom elements” sound like Pollen “tags”, you are correct. I use “custom 391 | elements” rather than “tags” or “X-expressions” to distinguish them from from Markdown-generated 392 | elements; also, unlike Pollen tags, custom elements may be treated differently depending on their 393 | @tech{block attributes}.} 394 | 395 | A custom element may optionally have a set of @deftech{attributes}, which is a list of key/value 396 | pairs that appears as the second item in the list. 397 | 398 | Here is an example of a function that produces a custom @tt{abbreviation} element with a @tt{term} 399 | attribute: 400 | 401 | @codeblock|{ 402 | #lang punct 403 | 404 | •(define (abbr term . elems) 405 | `(abbreviation [[term ,term]] ,@elems)) 406 | 407 | Writing documentation in Javascript? •abbr["Laugh out loud"]{LOL}. 408 | }| 409 | 410 | Produces: 411 | 412 | @racketblock[ 413 | '#s(document #hasheq((here-path . "7-unsaved-editor")) 414 | ((paragraph 415 | "Writing documentation in Javascript? " 416 | (abbreviation ((term "Laugh out loud")) "LOL") 417 | ".")) 418 | ()) 419 | ] 420 | 421 | By default, Punct will treat custom elements as @tech{inline elements}: they will be wrapped inside 422 | @tt{paragraph} elements if they occur on their own lines. 423 | 424 | You can set a custom element’s @deftech{block attribute} to force Punct to treat it as a block 425 | element (that is, to avoid having it auto-wrapped inside a @tt{paragraph} element): simply give it a 426 | @racket['block] attribute with a value of either @racket["root"] or @racket["single"]: 427 | 428 | @itemlist[ 429 | 430 | @item{@racket["root"] should be used for blocks that might contain other block elements. 431 | @bold{Limitations:} @racket["root"]-type blocks cannot be contained inside Markdown-created 432 | @tech{flows} (such as block quotations notated using @litchar{>}); if found inside such a flow, they 433 | will “escape” out to the root level of the document.} 434 | 435 | @item{@racket["single"] should be used for block elements that might need to be contained within 436 | Markdown-created @tech{flows}. @bold{Limitations:} @racket["single"]-type blocks must appear on 437 | their own line or lines in order to be counted as blocks.}] 438 | 439 | If that seems complicated, think of it this way: there are three kinds of @tech{flows} that you can 440 | notate with Markdown: block quotes, list items, and footnote definitions. If your custom block 441 | element might appear as a child of any of those three Markdown notations, you should probably start 442 | by giving it the @racket['(block "single")] attribute. 443 | 444 | You’ll never get an error for using the wrong @racket['block] type on your custom elements; you’ll 445 | just get unexpected results in the structure of your document. 446 | 447 | @bold{Under the hood:} The two types of blocks above correspond to two methods Punct uses to trick 448 | the CommonMark parser into treating custom elements as blocks. With @racket["root"]-type blocks, 449 | Punct inserts extra line breaks (which is what causes these blocks to “escape” out of Markdown 450 | blockquotes to the document’s root level, just as it would if you typed two linebreaks in your 451 | source). With @racket["single"]-type blocks, Punct allows CommonMark to wrap the element in a 452 | @tt{paragraph}, then looks for any paragraph that contains @emph{only} a @racket["single"]-type 453 | block and “shucks” them out of their containing paragraphs. The need for such tricks comes from a 454 | design decision to use the @racketmodname[commonmark] package exactly as published, without forking 455 | or customizing it in any way. 456 | 457 | @subsubsection[#:tag "custom-element-conveniences"]{Custom element conveniences} 458 | 459 | If you make much use of @tech{custom elements}, you will probably find yourself writing several 460 | functions that do nothing but lightly rearrange the arguments into an X-expression: 461 | 462 | @racketblock[ 463 | (define (abbr term . elems) 464 | `(abbrevation [[term ,term]] ,@elems)) 465 | ] 466 | 467 | To cut down on the repetition, you can use @racket[default-element-function] to create a function 468 | that automatically parses keyword arguments into attributes: 469 | 470 | @codeblock{ 471 | #lang punct 472 | •(define abbr (default-element-function 'abbreviation)) 473 | 474 | •abbr[#:term "Laugh Out Loud"]{LOL} •; -→ '(abbreviation ((term "Laugh Out Loud")) "LOL") 475 | } 476 | 477 | You can simplify this even further with @racket[define-element]: 478 | 479 | @codeblock{ 480 | #lang punct 481 | •(define-element abbr) 482 | 483 | •abbr[#:term "Laugh Out Loud"]{LOL} •; -→ '(abbr ((term "Laugh Out Loud")) "LOL") 484 | } 485 | 486 | Both @racket[default-element-function] and @racket[define-element] allow shorthand for setting 487 | default @tech{block attributes} and defaults for the @racket['class] attribute. See their entries in 488 | module reference for more details. 489 | 490 | @codeblock{ 491 | #lang punct 492 | •(define-element note box§.warning #:title "WATCH IT") 493 | 494 | •note{Wet floor!} 495 | •; -→ '(box ((block "root") (class "warning") (title "WATCH IT") "Wet floor!") 496 | } 497 | 498 | @section{Rendering output} 499 | 500 | A Punct document is format-independent; when you want to use it in an output file, it must be 501 | rendered into that output file’s format. 502 | 503 | Punct currently includes an HTML renderer and a plain-text renderer. Both are based on a “base” 504 | renderer. You can extend any Punct renderer or the base renderer to customize the process of 505 | converting @racket[document]s to your target output format(s). 506 | 507 | @subsection[#:tag "rendering-custom-elements"]{Rendering custom elements} 508 | 509 | When rendering your document to a specific output format (such as HTML) Punct has to decide how to 510 | render any @tech{custom elements} introduced by your code. By default it will use its own fallback 511 | function for the target output format. For example, when targeting HTML, Punct defaults to 512 | @racket[default-html-tag], which simply converts custom elements to strings of HTML. If you want 513 | more customized behavior, you’ll need to provide a fallback procedure to the renderer. 514 | 515 | Your fallback function will be given three arguments: the tag, a list of attributes, and a list of 516 | sub-elements found inside your element. The sub-elements will already have been fully processed by 517 | Punct. 518 | 519 | Here’s an example pair of functions for rendering documents containing the custom @tt{abbreviation} 520 | element (from the examples above) into HTML: 521 | 522 | @racketblock[ 523 | 524 | (define (custom-html tag attrs elems) 525 | (match (list tag attrs) 526 | [`(abbreviation [[term ,term]]) `(abbr [[title ,term]] ,@elems)])) 527 | 528 | (define (my-html-renderer source-path) 529 | (doc->html (get-doc source-path) custom-html)) 530 | 531 | ] 532 | 533 | @subsection{Rendering HTML} 534 | 535 | @defmodule[punct/render/html] 536 | 537 | @defproc[(doc->html [pdoc document?] 538 | [fallback (-> symbol? 539 | (listof (list/c symbol? string?)) 540 | (listof xexpr?) 541 | xexpr?) default-html-tag]) 542 | string?]{ 543 | 544 | Renders @racket[_pdoc] into a string containing HTML markup. Each @tech{custom element} is passed to 545 | @racket[_fallback], which must return an @racketlink[xexpr?]{X-expression}. 546 | 547 | This function uses @racket[xexpr->string] to generate the HTML string. This function will blindly 548 | escape characters inside @racketoutput{