Skip to content

Commit d40156b

Browse files
committed
Add support for footnotes as an optional extension
1 parent 0a17796 commit d40156b

File tree

16 files changed

+870
-149
lines changed

16 files changed

+870
-149
lines changed

commonmark-bench/info.rkt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#lang info
22

3-
(define version "1.0")
3+
(define version "1.1")
44

55
(define collection 'multi)
66

commonmark-doc/info.rkt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#lang info
22

3-
(define version "1.0")
3+
(define version "1.1")
44

55
(define collection 'multi)
66

commonmark-doc/scribblings/commonmark.scrbl

Lines changed: 162 additions & 11 deletions
Large diffs are not rendered by default.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.CmMdNotes {
2+
font-size: small;
3+
padding-left: 1em;
4+
}
5+
.CmMdNotes p {
6+
margin-bottom: 0.25em;
7+
}
8+
9+
.CmExample {
10+
column-gap: 0.5em;
11+
display: flex;
12+
margin-bottom: 1em;
13+
margin-top: 1em;
14+
}
15+
.CmExample > * {
16+
flex: 1;
17+
}
18+
19+
.CmExampleTitleLeft, .CmExampleTitleRight {
20+
background-color: #eee;
21+
}
22+
.CmExampleTitleRight {
23+
text-align: right;
24+
}
25+
.CmExampleTitleText {
26+
padding: 0 0.5em;
27+
}
28+
29+
.CmExampleContent {
30+
margin: 0 0.5em;
31+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#lang racket/base
2+
3+
;; This module implements a simple Markdown-to-Scribble renderer for the
4+
;; purposes of rendering examples in the commonmark library documentation.
5+
;;
6+
;; It is *not* a general-purpose solution for converting Markdown to Scribble,
7+
;; primarily because it explicitly avoids converting Markdown headers to
8+
;; Scribble `part-start` declarations, which would pollute the structure of the
9+
;; manual itself and would make it impossible to display rendered headings
10+
;; inside a nested flow.
11+
12+
(require commonmark/parse
13+
commonmark/private/render
14+
racket/class
15+
racket/contract
16+
racket/format
17+
racket/list
18+
racket/match
19+
racket/path
20+
racket/runtime-path
21+
setup/main-collects
22+
scribble/core
23+
scribble/html-properties
24+
scribble/manual
25+
threading
26+
(only-in xml cdata))
27+
28+
(provide (contract-out
29+
[markdown-block (-> string? ... block?)]
30+
[markdown-example (-> string? ... block?)]))
31+
32+
;; -----------------------------------------------------------------------------
33+
34+
(define-runtime-path commonmark.css "commonmark.css")
35+
(define commonmark-css-addition (~> (simple-form-path commonmark.css)
36+
path->main-collects-relative
37+
css-addition))
38+
39+
(define horizontal-rule-style (style #f (list (alt-tag "hr"))))
40+
(define (horizontal-rule)
41+
(paragraph horizontal-rule-style '()))
42+
43+
(define inset-style (style 'inset '()))
44+
(define ordered-style (style 'ordered '()))
45+
(define div-flow-style (style #f (list (alt-tag "div"))))
46+
(define footnotes-style (style "CmMdNotes" (list commonmark-css-addition (alt-tag "section"))))
47+
(define markdown-doc-style (style "CmMdDoc" (list commonmark-css-addition (alt-tag "div"))))
48+
(define example-style (style "CmExample" (list commonmark-css-addition (alt-tag "div"))))
49+
(define example-content-style (style "CmExampleContent" (list commonmark-css-addition (alt-tag "div"))))
50+
(define example-title-left-style (style "CmExampleTitleLeft" (list commonmark-css-addition)))
51+
(define example-title-right-style (style "CmExampleTitleRight" (list commonmark-css-addition)))
52+
(define example-title-text-style (style "CmExampleTitleText" (list commonmark-css-addition)))
53+
54+
(define (markdown-block . strs)
55+
(nested #:style 'code-inset (document->scribble (string->document (apply string-append strs)))))
56+
57+
(define (markdown-example . strs)
58+
(define str (apply string-append strs))
59+
(nested-flow
60+
example-style
61+
(list (nested-flow
62+
div-flow-style
63+
(list (paragraph example-title-left-style (list (element example-title-text-style (tt "markdown"))))
64+
(nested-flow example-content-style (list (verbatim str)))))
65+
(nested-flow
66+
div-flow-style
67+
(list (paragraph example-title-right-style (list (element example-title-text-style (tt "rendered"))))
68+
(nested-flow example-content-style (list (document->scribble (string->document str)))))))))
69+
70+
(define (document->scribble doc #:who [who 'document->scribble])
71+
(send (new scribble-render% [doc doc] [who who]) render-document))
72+
73+
(define scribble-render%
74+
(class abstract-render%
75+
(define/override (render-document)
76+
(define-values [body footnotes] (super render-document))
77+
(nested-flow
78+
markdown-doc-style
79+
(if (empty? footnotes)
80+
body
81+
(append body
82+
(list (nested-flow footnotes-style (list (itemization ordered-style footnotes))))))))
83+
84+
(define/override (render-thematic-break)
85+
(horizontal-rule))
86+
(define/override (render-heading content depth)
87+
(define tag (vector-ref #("h1" "h2" "h3" "h4" "h5" "h6") (sub1 depth)))
88+
(paragraph (style #f (list (alt-tag tag))) content))
89+
(define/override (render-code-block content language)
90+
(match language
91+
["racket" (typeset-code #:keep-lang-line? #f "#lang racket\n" content)]
92+
[_ (nested #:style 'code-inset (verbatim content))]))
93+
(define/override (render-html-block content)
94+
(paragraph (style #f '(div omitable)) (render-html content)))
95+
(define/override (render-paragraph content)
96+
(paragraph plain content))
97+
(define/override (render-blockquote blocks)
98+
(nested inset-style blocks))
99+
(define/override (render-itemization blockss list-style start-num)
100+
(itemization
101+
(style (cond
102+
[start-num 'ordered]
103+
[(eq? list-style 'tight) 'compact]
104+
[else #f])
105+
(match start-num
106+
[(or #f 1) '()]
107+
[_ (list (attributes (list (cons 'start (number->string start-num)))))]))
108+
blockss))
109+
110+
(define/override (render-line-break)
111+
(linebreak))
112+
(define/override (render-bold content)
113+
(element 'bold content))
114+
(define/override (render-italic content)
115+
(element 'italic content))
116+
(define/override (render-code content)
117+
(element 'tt content))
118+
(define/override (render-link content dest title)
119+
(element (style #f (list (make-target-url dest))) content))
120+
(define/override (render-image description source title)
121+
(image-element #f description source '() 1))
122+
(define/override (render-html content)
123+
(element (style #f (list (xexpr-property (cdata #f #f content) ""))) '()))
124+
(define/override (render-footnote-reference label defn-num ref-num)
125+
(~>> (~a defn-num)
126+
(link-element #f _ (footnote-definition-tag label))
127+
(target-element #f _ (footnote-reference-tag label ref-num))
128+
(element 'superscript)))
129+
130+
(define/override (render-footnote-definition blocks label ref-count)
131+
(define multiple-refs? (> ref-count 1))
132+
(define backrefs (~> (for/list ([i (in-range ref-count)])
133+
(define ref-num (add1 i))
134+
(link-element
135+
#f
136+
(if multiple-refs?
137+
(list "" (element 'superscript (~a ref-num)))
138+
"")
139+
(footnote-reference-tag label ref-num)))
140+
(add-between " ")
141+
(cons " " _)))
142+
143+
(define target (target-element #f '() (footnote-definition-tag label)))
144+
(define targeted-blocks
145+
(match blocks
146+
[(cons (paragraph para-style para-content) blocks)
147+
(cons (paragraph para-style (list target para-content)) blocks)]
148+
[_
149+
(cons (paragraph plain target) blocks)]))
150+
151+
(match targeted-blocks
152+
[(list blocks ... (paragraph para-style para-content))
153+
(append blocks (list (paragraph para-style (cons para-content backrefs))))]
154+
[_
155+
(append targeted-blocks (list (paragraph plain backrefs)))]))
156+
157+
(define local-tags (make-hash))
158+
(define/public (footnote-definition-tag label)
159+
(list 'footnote (hash-ref! local-tags label generated-tag)))
160+
(define/public (footnote-reference-tag label ref-num)
161+
(list 'footnote-ref (hash-ref! local-tags (cons label ref-num) generated-tag)))
162+
163+
(super-new)))

commonmark-lib/commonmark/parse.rkt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66

77
(provide (contract-out
88
[read-document (-> input-port? document?)]
9-
[string->document (-> string? document?)]))
9+
[string->document (-> string? document?)]
10+
11+
[current-parse-footnotes? (parameter/c any/c boolean?)]))

commonmark-lib/commonmark/private/parse/block.rkt

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"common.rkt"
1414
"inline.rkt")
1515

16-
(provide read-document string->document)
16+
(provide read-document
17+
string->document
18+
current-parse-footnotes?)
1719

1820
;; -----------------------------------------------------------------------------
1921

@@ -47,6 +49,8 @@
4749

4850
;; -----------------------------------------------------------------------------
4951

52+
(define current-parse-footnotes? (make-parameter #f (λ (x) (and x #t))))
53+
5054
(struct o:blockquote (blocks) #:transparent)
5155
(struct o:list ; see Note [Lists overview]
5256
(blocks ; blocks of the current open list item, or #f if no list item
@@ -61,6 +65,7 @@
6165
style ; (or/c 'tight 'loose)
6266
start-num) ; (or/c exact-nonnegative-integer? #f)
6367
#:transparent)
68+
(struct o:footnote-definition (blocks label) #:transparent)
6469

6570
; see Note [Open lists without an open item]
6671
(define (accumulate-list-blockss open-list)
@@ -82,8 +87,12 @@
8287
(read-document (open-input-string str)))
8388

8489
(define (read-document in)
90+
(define footnotes? (current-parse-footnotes?))
91+
8592
(define root-blocks (make-gvector))
8693
(define link-reference-defns (make-hash))
94+
(define footnote-defns (make-gvector))
95+
(define footnote-defn-labels (make-hash))
8796

8897
;; ---------------------------------------------------------------------------
8998
;; open blocks
@@ -121,7 +130,9 @@
121130
; see Note [Open list tightness]
122131
[style (if (o:list-end-blank? oc)
123132
'loose
124-
(o:list-style oc))])]))
133+
(o:list-style oc))])]
134+
[(o:footnote-definition blocks label)
135+
(o:footnote-definition (cons block blocks) label)]))
125136
(gvector-set! open-containers (sub1 ocs) oc*)]))
126137

127138
(define (open-leaf! new-open-leaf)
@@ -167,26 +178,30 @@
167178
; If there’s an open leaf, we need to take care to close it /before/ we
168179
; remove the open container.
169180
(close-leaf!)
170-
(add-block!
171-
#:close-unentered? #f
172-
(match (gvector-remove-last! open-containers)
173-
[(o:blockquote blocks)
174-
(blockquote (reverse blocks))]
175-
[(? o:list? open-list)
176-
; see Note [Transfer `end-blank?` to parent lists]
177-
(when (and (o:list-end-blank? open-list)
178-
(not (zero? (gvector-count open-containers))))
179-
(define last-idx (sub1 (gvector-count open-containers)))
180-
(define parent-container (gvector-ref open-containers last-idx))
181-
(when (o:list? parent-container)
182-
(gvector-set! open-containers
183-
last-idx
184-
(struct-copy o:list parent-container
185-
[end-blank? #t]))))
186-
187-
(itemization (reverse (accumulate-list-blockss open-list))
188-
(o:list-style open-list)
189-
(o:list-start-num open-list))])))
181+
(define new-block
182+
(match (gvector-remove-last! open-containers)
183+
[(o:blockquote blocks)
184+
(blockquote (reverse blocks))]
185+
[(? o:list? open-list)
186+
; see Note [Transfer `end-blank?` to parent lists]
187+
(when (and (o:list-end-blank? open-list)
188+
(not (zero? (gvector-count open-containers))))
189+
(define last-idx (sub1 (gvector-count open-containers)))
190+
(define parent-container (gvector-ref open-containers last-idx))
191+
(when (o:list? parent-container)
192+
(gvector-set! open-containers
193+
last-idx
194+
(struct-copy o:list parent-container
195+
[end-blank? #t]))))
196+
197+
(itemization (reverse (accumulate-list-blockss open-list))
198+
(o:list-style open-list)
199+
(o:list-start-num open-list))]
200+
[(o:footnote-definition blocks label)
201+
(gvector-add! footnote-defns (footnote-definition (reverse blocks) label))
202+
#f]))
203+
(when new-block
204+
(add-block! new-block #:close-unentered? #f)))
190205

191206
(define (unentered-containers?)
192207
(< entered-containers (gvector-count open-containers)))
@@ -410,7 +425,10 @@
410425
(close-leaf!)
411426
(close-unentered-containers!)
412427
(document (for/list ([block (in-gvector root-blocks)])
413-
(finish-block-content block)))]
428+
(finish-block-content block))
429+
(for/list ([footnote-defn (in-gvector footnote-defns)])
430+
(match-define (footnote-definition blocks label) footnote-defn)
431+
(footnote-definition (map finish-block-content blocks) label)))]
414432
[else
415433
(mode:enter-containers)]))
416434

@@ -451,7 +469,9 @@
451469
(open-container! (o:list '() '() indent marker-char #f #f 'tight start-num)
452470
#:enter? #f)])
453471
0])]
454-
[else #f])]))
472+
[else #f])]
473+
[(? o:footnote-definition?)
474+
(try-enter-indent 4)]))
455475

456476
(define (mode:enter-containers)
457477
(cond
@@ -675,6 +695,27 @@
675695
(open-container! (o:list '() '() indent marker-char #f #f 'tight start-num))
676696
(mode:content-start)])]
677697

698+
;; Extension: Footnote definitions
699+
[(and footnotes?
700+
(regexp-try/opt-indent
701+
; This is the regexp used by _scan_footnote_definition in cmark-gfm:
702+
; <https://github.com/github/cmark-gfm/blob/766f161ef6d61019acf3a69f5099489e7d14cd49/src/scanners.re#L323>
703+
; Note that it consumes all whitespace after the label, so it is
704+
; impossible for a footnote to start with an indented code block on the
705+
; same line as the label (but one can begin on the following line).
706+
(px "^"
707+
"\\[\\^"
708+
(:group "[^]" :space: "]+")
709+
"\\]:[ \t]*")))
710+
=> (match-lambda
711+
[(list _ _ label-bytes)
712+
(define label (bytes->string/utf-8 label-bytes))
713+
(define normalized-label (normalize-link-label label))
714+
(unless (hash-has-key? footnote-defn-labels normalized-label)
715+
(hash-set! footnote-defn-labels normalized-label label))
716+
(open-container! (o:footnote-definition '() label))
717+
(mode:content-start)])]
718+
678719
;; § 4.8 Paragraphs
679720
[else
680721
(define line (~> (read-line in 'any)
@@ -717,15 +758,20 @@
717758
(define (finish-block-content block)
718759
(match block
719760
[(heading content depth)
720-
(heading (string->inline content link-reference-defns) depth)]
761+
(heading (parse-inline content) depth)]
721762
[(paragraph content)
722-
(paragraph (string->inline content link-reference-defns))]
763+
(paragraph (parse-inline content))]
723764
[(blockquote blocks)
724765
(blockquote (map finish-block-content blocks))]
725766
[(itemization blockss style start-num)
726767
(itemization (map (λ~>> (map finish-block-content)) blockss) style start-num)]
727768
[_ block]))
728769

770+
(define (parse-inline content)
771+
(string->inline content
772+
#:link-defns link-reference-defns
773+
#:footnote-defns footnote-defn-labels))
774+
729775
(mode:line-start))
730776

731777
; see Note [Tabs and tab stops]

0 commit comments

Comments
 (0)