Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

22 May 2021

Racket的Scribble Text模板

学习Scribble as Preprocessor。

scribble/text以及scribble/html是预处理器型语言,用于生成text或者HTML。它们使用和scribble相容的@语句,但是处理的是抽象的可以转化为text或者HTML的文档。

1 Text Generation

scribble/text对顶层环境的一些修改:

  • 使用read-syntax-inside来读取模块的正文,此举和Scribble的文档阅读器一致。这意味着默认情况下,所有的文本是以字符串的形式读取的;以及@构造可以用来转义racket的函数以及表达式。
  • 表达式的量值是以自定义的output函数来打印的。此函数和display很相似,但是为文本输出作了一些特殊修改。

当scribble/text被求访而不是作为#lang使用时,不会改变量值的打印方式,并且不会导入racket/base的对照,incude是以include/text提供,begin是以begin/text提供。

1.1 Writing Text Files

感受一下例子:

#lang scribble/text
@(define (errors n)
   (list n
         " error"
         (and (not (= n 1)) "s")))

You have @errors[3] in your code,
I fixed @errors[1].

对应的输出:

You have 3 errors in your code,

I fixed 1 error.

例子2与上一个例子的输出雷同,但是写法不一样

#lang scribble/text
@(define (errors n)
   ;; note the use of `unless'
   @list{@n error@unless[(= n 1)]{s}})
You have @errors[3] in your code,
I fixed @errors[1].

为了方便写定义,定义之后的换行以及换行前面的缩进都会被忽略:

例子2也可以写成:

#lang scribble/text

@(define (plural n)
   (unless (= n 1) "s"))
 
@(define (errors n)
   @list{@n error@plural[n]})
 
You have @errors[3] in your code,
  @(define fixed 1)
I fixed @errors[fixed].

其他情况下的换行不会被忽略,如以下所示:

#lang scribble/text

@(define (count n str)

   (for/list ([i (in-range 1 (add1 n))])

     @list{@i @str,@"\n"}))

Start...
@count[3]{Mississippi}
... and I'm done

上述例子的输出结果是:

Start...
1 Mississippi,
2 Mississippi,
3 Mississippi,

... and I'm done

有其他几种方式帮你避免输出中的空行,比如让函数调用本身跨行:

Start...
@count[3]{Mississippi
}... and I'm done

另一种方法是使用@;来忽略从它之后到换行的所有内容(包括换行):

Start again...
@count[3]{Massachusetts}@;
... and I'm done again.

当然,也可以让函数来控制空行的生成:

#lang scribble/text
@(require racket/list)
@(define (counts n str)
   (add-between
    (for/list ([i (in-range 1 (+ n 1))])
      @list{@i @str,})
    "\n"))
Start...
@counts[3]{Mississippi}
... and I'm done.

上面的方法很常用,所以scribble/text提供了方便函数add-newlines:

#lang scribble/text
@(define (count n str)
   (add-newlines
    (for/list ([i (in-range 1 (+ n 1))])
      @list{@i @str,})))

也可以通过#:sep关键字设置分隔符:

#lang scribble/text
@(define (count n str)
   (add-newlines #:sep ",\n"
    (for/list ([i (in-range 1 (+ n 1))])
      @list{@i @str})))

1.2 Defining Functions and More

@的转义的用途有点多,看起来有点晕:

#lang scribble/text
@(define @bold[text] @list{*@|text|*})
An @bold{important} note.

上述定义了一个@构造,叫做@bold用于输出bold格式,代码运行结果是:

An *important* note.

也可以让函数直接接受文本参数:

#lang scribble/text
@(define (choose 1st 2nd)
   @list{Either @1st, or @|2nd|@"."})
@(define who "us")
@choose[@list{you're with @who}
        @list{against @who}]

可以使用化集来简化书写:

@(define-syntax-rule (compare (x ...) ...)
   (add-newlines
    (list (list "* " x ...) ...)))
Shopping list:
@compare[@{apples}
         @{oranges}
         @{@(* 2 3) bananas}]

@{...}@(...)的区别是前者将内容作为执行诀,后者将内容作为表达式,后者也可以写为@[...]

scribble/text还提供了一个split-lines函数,用来按换行切分字符串:

#lang scribble/text
@(require racket/list)
@(define (features . text)
   (add-between (split-lines text)
                ", "))
@features{red
          fast
          reliable}.

@构造是可以嵌套的:

#lang scribble/text
@(define ((choose . 1st) . 2nd)
   @list{Either you're @1st, or @|2nd|.})
@(define who "me")
@@choose{with @who}{against @who}

外层的@是单入参,所以要对choose进行柯里化。

1.3 Using Printouts

打印的内容会直接被整合进输出:

#lang scribble/text
First
@display{Second}
Third

对应的输出是:

First
Second
Third

所以,可以直接选择在直接打印函数的结果,而不用将其返回:

#lang scribble/text
@(define (count n)
   (for ([i (in-range 1 (+ n 1))])
     (printf "~a Mississippi,\n" i)))
Start...
@count[3]@; avoid an empty line
... and I'm done.

但是将打印和返回混合在一起可能有意料以外的效果:

#lang scribble/text
@list{1 @display{two} 3}

上述打印的是"two1 3”。

也可以构造一个无限输出:

#lang scribble/text
@(define (count n)
   (cons @list{@n Mississippi,@"\n"}
         (lambda ()
           (count (add1 n)))))
Start...
@count[1]
this line is never printed!

1.4 Indentation in Preprocessed output

可以使用修引构造来确定输出的具体内容,比如:

#lang scribble/text
@(format "~s" '@list{
                 a
                   b
                 c})

的输出是:

(list "a" "\n" "  " "b" "\n" "c")

Scribble读取器会自动忽略无关的缩进。这样代码块的位置不会影响输出。但是代码块内部的缩进会被保留。可以用block来指明代码块:

#lang scribble/text

foo @block{1
           2
           3}

连对的呈现默认也是按照代码块处理的,可以利用这个特点做一个小功能:

#lang scribble/text
@(define (code . text)
   @list{begin
           @text
         end})
@code{first
      second
      @code{
        third
        fourth}
      last}

输出是:

begin
  first
  second
  begin
    third
    fourth
  end
  last
end

splice则不主动处理缩进,以splice方式呈现的list也步处理缩进。

disable-prefix则可以用来消除缩进,其所在行的缩进也会被消除。disable-prefix之后的会按目标栏缩进。

add-prefix则可以按自己的需求来添加所需的前缀。混合使用disable-prefix和add-prefix的时候,可能需要flush缓存。

1.5 Using External Files

可以简单地求访外部文件。此外,可以使用#lang at-expr来开启对@-构造地支持,下面是一个例子。

独部定义文件:

; stuff.rkt
#lang at-exp racket/base
(require racket/list)
(provide (all-defined-out))
(define (itemize . items)
  (add-between (map (lambda (item)
                      @list{* @item})
                    items)
               "\n"))
(define summary
  @list{If that's not enough,
        I don't know what is.})

独部使用文件:

#lang scribble/text
@(require "stuff.rkt")
Todo:
@itemize[@list{Hack some}
         @list{Sleep some}
         @list{Hack some
               more}]
@summary

上述都是在scribble/text中嵌套racket定义和表达式,也可以反过来在racket中嵌套scribble/text,只需(require scribble/text)

#lang at-exp racket/base
(require scribble/text racket/list)
(define (itemize . items)
  (add-between (map (lambda (item)
                      @list{* @item})
                    items)
               "\n"))
(define summary
  @list{If that's not enough,
        I don't know what is.})
(output
 @list{
   Todo:
   @itemize[@list{Hack some}
            @list{Sleep some}
            @list{Hack some
                  more}]
   @summary
 })

只需使用output切换到scribble/text输出状态。

还有一种办法,就是直接使用@include来包含其他文件:

@include["template.html"]

在scribble/text中require其他text是无法工作地,因为返回发起求访地访问之前,被求访地文件就会被运算。

1.6 Text Generation Functions

  • outputable/c,适用于ouput地契约判定诀,目前可以接收任意值
  • (output v [port]),输出文本内容
  • (block v ...),以切块方式输出
  • (splice v ...),以拼接方式输出
  • (disable-prefix v ...)
  • (restore-prefix v ...)
  • (add-prefix pfx v ...)
  • (set-prefix pfx v ...)
  • flush,输出前缩进和当前前缀来
  • (with-writer writer v ...),调整当前使用地writer
  • (add-newlines items [#:sep sep]),和add-between类似,但是会忽略#f以及#元素
  • (split-lines items),将所列条目转化为一个连对的连对,相邻的非”\n"内容会被归置到一个嵌套的连对,"\n"内容会被丢弃。
  • (include/text maybe-char path-spec),将目标文件以scribble/text方式读取进来。可以选用@以外的命令字符
  • ((begin/text form ...)),类似于begin,但是表达式运算结果会以连对方式收集,并按begin/list构造返回

(未完待续)