-
Notifications
You must be signed in to change notification settings - Fork 4
/
parser.cljc
598 lines (492 loc) · 22.1 KB
/
parser.cljc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
;; # 🧩 Parsing
;;
;; Deals with transforming a sequence of tokens obtained by [markdown-it] into a nested AST composed of nested _nodes_.
;;
;; A _node_ is a clojure map and has no closed specification at the moment. We do follow a few conventions for its keys:
;;
;; - `:type` a keyword (:heading, :paragraph, :text, :code etc.) present on all nodes.
;;
;; When a node contains other child nodes, then it will have a
;;
;; - `:content` a collection of nodes representing nested content
;;
;; when a node is a textual leaf (as in a `:text` or `:formula` nodes) it carries a
;; - `:text` key with a string value
;;
;; Other keys might include e.g.
;;
;; - `:info` specific of fenced code blocks
;; - `:heading-level` specific of `:heading` nodes
;; - `:attrs` attributes as passed by markdown-it tokens (e.g `{:style "some style info"}`)
(ns nextjournal.markdown.parser
(:require [clojure.string :as str]
[nextjournal.markdown.transform :as md.transform]
[nextjournal.markdown.parser.emoji :as emoji]
#?@(:cljs [[applied-science.js-interop :as j]
[cljs.reader :as reader]])))
;; clj common accessors
(def get-in* #?(:clj get-in :cljs j/get-in))
(def update* #?(:clj update :cljs j/update!))
#?(:clj (defn re-groups* [m] (let [g (re-groups m)] (cond-> g (not (vector? g)) vector))))
(defn re-idx-seq
"Takes a regex and a string, returns a seq of triplets comprised of match groups followed by indices delimiting each match."
[re text]
#?(:clj (let [m (re-matcher re text)]
(take-while some? (repeatedly #(when (.find m) [(re-groups* m) (.start m) (.end m)]))))
:cljs (let [rex (js/RegExp. (.-source re) "g")]
(take-while some? (repeatedly #(when-some [m (.exec rex text)] [(vec m) (.-index m) (.-lastIndex rex)]))))))
(comment (re-idx-seq #"\{\{([^{]+)\}\}" "foo {{hello}} bar"))
(comment (re-idx-seq #"\{\{[^{]+\}\}" "foo {{hello}} bar"))
;; region node operations
;; helpers
(defn inc-last [path] (update path (dec (count path)) inc))
(defn hlevel [{:as _token hn :tag}] (when (string? hn) (some-> (re-matches #"h([\d])" hn) second #?(:clj Integer/parseInt :cljs js/parseInt))))
(defn split-by-emoji [s]
(let [[match start end] (first (re-idx-seq emoji/regex s))]
(if match
[(subs s start end) (str/trim (subs s end))]
[nil s])))
#_(split-by-emoji " Stop")
#_(split-by-emoji "🤚🏽 Stop")
#_(split-by-emoji "🤚🏽🤚 Stop")
#_(split-by-emoji "🤚🏽Stop")
#_(split-by-emoji "🤚🏽 Stop")
#_(split-by-emoji "😀 Stop")
#_(split-by-emoji "⚛️ Stop")
#_(split-by-emoji "⚛ Stop")
#_(split-by-emoji "⬇ Stop")
#_(split-by-emoji "Should not 🙁️ Split")
(defn text->id+emoji [text]
(when (string? text)
(let [[emoji text'] (split-by-emoji (str/trim text))]
(cond-> {:id (apply str (map (comp str/lower-case (fn [c] (case c (\space \_) \- c))) text'))}
emoji (assoc :emoji emoji)))))
#_(text->id+emoji "Hello There")
#_(text->id+emoji "Hello_There")
#_(text->id+emoji "👩🔬 Quantum Physics")
;; `parse-fence-info` ingests nextjournal, GFM, Pandoc and RMarkdown fenced code block info (any text following the leading 3 backticks) and returns a map
;;
;; _nextjournal_ / _GFM_
;;
;; ```python id=2e3541da-0735-4b7f-a12f-4fb1bfcb6138
;; python code
;; ```
;;
;; _Pandoc_
;;
;; ```{#pandoc-id .languge .extra-class key=Val}
;; code in language
;; ```
;;
;; _Rmd_
;;
;; ```{r cars, echo=FALSE}
;; R code
;; ```
;;
;; See also:
;; - https://github.github.com/gfm/#info-string
;; - https://pandoc.org/MANUAL.html#fenced-code-blocks
;; - https://rstudio.com/wp-content/uploads/2016/03/rmarkdown-cheatsheet-2.0.pdf"
(defn parse-fence-info [info-str]
(try
(when (string? info-str)
(let [tokens (-> info-str
str/trim
(str/replace #"[\{\}\,]" "") ;; remove Pandoc/Rmarkdown brackets and commas
(str/replace "." "") ;; remove dots
(str/split #" "))] ;; split by spaces
(reduce
(fn [{:as info-map :keys [language]} token]
(let [[_ k v] (re-matches #"^([^=]+)=([^=]+)$" token)]
(cond
(str/starts-with? token "#") (assoc info-map :id (str/replace token #"^#" "")) ;; pandoc #id
(and k v) (assoc info-map (keyword k) v)
(not language) (assoc info-map :language token) ;; language is the first simple token which is not a pandoc's id
:else (assoc info-map (keyword token) true))))
{}
tokens)))
(catch #?(:clj Throwable :cljs :default) _ {})))
(comment
(parse-fence-info "python runtime-id=5f77e475-6178-47a3-8437-45c9c34d57ff")
(parse-fence-info "{#some-id .lang foo=nex}")
(parse-fence-info "#id clojure")
(parse-fence-info "clojure #id")
(parse-fence-info "clojure")
(parse-fence-info "{r cars, echo=FALSE}"))
;; leaf nodes
(defn text-node [text] {:type :text :text text})
(defn tag-node [text] {:type :hashtag :text text})
(defn formula [text] {:type :formula :text text})
(defn block-formula [text] {:type :block-formula :text text})
(defn sidenote-ref [ref] {:type :sidenote-ref :content [(text-node (str (inc ref)))]})
;; node constructors
(defn node
[type content attrs top-level]
(cond-> {:type type :content content}
(seq attrs) (assoc :attrs attrs)
(seq top-level) (merge top-level)))
(defn empty-text-node? [{text :text t :type}] (and (= :text t) (empty? text)))
(defn push-node [{:as doc ::keys [path]} node]
(try
(cond-> doc
;; ⬇ mdit produces empty text tokens at mark boundaries, see edge cases below
(not (empty-text-node? node))
(-> #_doc
(update ::path inc-last)
(update-in (pop path) conj node)))
(catch #?(:clj Exception :cljs js/Error) e
(throw (ex-info (str "nextjournal.markdown cannot add node: " node " at path: " path)
{:doc doc :node node} e)))))
(def push-nodes (partial reduce push-node))
(defn open-node
([doc type] (open-node doc type {}))
([doc type attrs] (open-node doc type attrs {}))
([doc type attrs top-level]
(-> doc
(push-node (node type [] attrs top-level))
(update ::path into [:content -1]))))
;; after closing a node, document ::path will point at it
(def ppop (comp pop pop))
(defn close-node [doc] (update doc ::path ppop))
(defn update-current [{:as doc path ::path} fn & args] (apply update-in doc path fn args))
(defn assign-node-id+emoji [{:as doc ::keys [id->index path] :keys [text->id+emoji-fn]}]
(let [{:keys [id emoji]} (when (ifn? text->id+emoji-fn) (-> doc (get-in path) text->id+emoji-fn))
id-count (when id (get id->index id))]
(cond-> doc
id
(update-in [::id->index id] (fnil inc 0))
(or id emoji)
(update-in path (fn [node]
(cond-> node
id (assoc-in [:attrs :id] (cond-> id id-count (str "-" (inc id-count))))
emoji (assoc :emoji emoji)))))))
(comment ;; path after call
(-> empty-doc ;; [:content -1]
(open-node :heading) ;; [:content 0 :content -1]
(push-node {:node/type :text :text "foo"}) ;; [:content 0 :content 0]
(push-node {:node/type :text :text "foo"}) ;; [:content 0 :content 1]
close-node ;; [:content 1]
(open-node :paragraph) ;; [:content 1 :content]
(push-node {:node/type :text :text "hello"})
close-node
(open-node :bullet-list)
;;
))
;; endregion
;; region TOC builder:
;; toc nodes are heading nodes but with `:type` `:toc` and an extra branching along
;; the key `:children` representing the sub-sections of the node
(defn into-toc [toc {:as toc-item :keys [heading-level]}]
(loop [toc toc l heading-level toc-path [:children]]
;; `toc-path` is `[:children i₁ :children i₂ ... :children]`
(let [type-path (assoc toc-path (dec (count toc-path)) :type)]
(cond
;; insert intermediate default empty :content collections for the final update-in (which defaults to maps otherwise)
(not (get-in toc toc-path))
(recur (assoc-in toc toc-path []) l toc-path)
;; fill in toc types for non-contiguous jumps like h1 -> h3
(not (get-in toc type-path))
(recur (assoc-in toc type-path :toc) l toc-path)
(= 1 l)
(update-in toc toc-path (fnil conj []) toc-item)
:else
(recur toc
(dec l)
(conj toc-path
(max 0 (dec (count (get-in toc toc-path)))) ;; select last child at level if it exists
:children))))))
(defn add-to-toc [doc {:as h :keys [heading-level]}]
(cond-> doc (pos-int? heading-level) (update :toc into-toc (assoc h :type :toc))))
(defn set-title-when-missing [{:as doc :keys [title]} heading]
(cond-> doc (nil? title) (assoc :title (md.transform/->text heading))))
(defn add-title+toc
"Computes and adds a :title and a :toc to the document-like structure `doc` which might have not been constructed by means of `parse`."
[{:as doc :keys [content]}]
(let [rf (fn [doc heading] (-> doc (add-to-toc heading) (set-title-when-missing heading)))
xf (filter (comp #{:heading} :type))]
(reduce (xf rf) (assoc doc :toc {:type :toc}) content)))
(comment
(-> {:type :toc}
;;(into-toc {:heading-level 3 :title "Foo"})
;;(into-toc {:heading-level 2 :title "Section 1"})
(into-toc {:heading-level 1 :title "Title" :type :toc})
(into-toc {:heading-level 4 :title "Section 2" :type :toc})
;;(into-toc {:heading-level 4 :title "Section 2.1"})
;;(into-toc {:heading-level 2 :title "Section 3"})
)
(-> "# Top _Title_
par
### Three
## Two
par
- and a nested
- ### Heading not included
foo
## Two Again
par
# One Again
[[TOC]]
#### Four
end"
nextjournal.markdown/parse
:toc
))
;; endregion
;; region token handlers
(declare apply-tokens)
(defmulti apply-token (fn [_doc token] (:type token)))
(defmethod apply-token :default [doc token]
(prn :apply-token/unknown-type {:token token})
doc)
;; blocks
(defmethod apply-token "heading_open" [doc token] (open-node doc :heading {} {:heading-level (hlevel token)}))
(defmethod apply-token "heading_close" [doc {doc-level :level}]
(let [{:as doc ::keys [path]} (close-node doc)
doc' (assign-node-id+emoji doc)
heading (-> doc' (get-in path) (assoc :path path))]
(cond-> doc'
(zero? doc-level)
(-> (add-to-toc heading)
(set-title-when-missing heading)))))
;; for building the TOC we just care about headings at document top level (not e.g. nested under lists) ⬆
(defmethod apply-token "paragraph_open" [doc {:as _token :keys [hidden]}] (open-node doc (if hidden :plain :paragraph)))
(defmethod apply-token "paragraph_close" [doc _token] (close-node doc))
(defmethod apply-token "bullet_list_open" [doc {{:as attrs :keys [has-todos]} :attrs}] (open-node doc (if has-todos :todo-list :bullet-list) attrs))
(defmethod apply-token "bullet_list_close" [doc _token] (close-node doc))
(defmethod apply-token "ordered_list_open" [doc {:keys [attrs]}] (open-node doc :numbered-list attrs))
(defmethod apply-token "ordered_list_close" [doc _token] (close-node doc))
(defmethod apply-token "list_item_open" [doc {{:as attrs :keys [todo]} :attrs}] (open-node doc (if todo :todo-item :list-item) attrs))
(defmethod apply-token "list_item_close" [doc _token] (close-node doc))
(defmethod apply-token "math_block" [doc {text :content}] (push-node doc (block-formula text)))
(defmethod apply-token "math_block_end" [doc _token] doc)
(defmethod apply-token "hr" [doc _token] (push-node doc {:type :ruler}))
(defmethod apply-token "blockquote_open" [doc _token] (open-node doc :blockquote))
(defmethod apply-token "blockquote_close" [doc _token] (close-node doc))
(defmethod apply-token "tocOpen" [doc _token] (open-node doc :toc))
(defmethod apply-token "tocBody" [doc _token] doc) ;; ignore body
(defmethod apply-token "tocClose" [doc _token] (-> doc close-node (update-current dissoc :content)))
(defmethod apply-token "code_block" [doc {:as _token c :content}]
(-> doc
(open-node :code)
(push-node (text-node c))
close-node))
(defmethod apply-token "fence" [doc {:as _token i :info c :content}]
(-> doc
(open-node :code {} (assoc (parse-fence-info i) :info i))
(push-node (text-node c))
close-node))
;; footnotes
(defmethod apply-token "sidenote_ref" [doc token] (push-node doc (sidenote-ref (get-in* token [:meta :id]))))
(defmethod apply-token "sidenote_anchor" [doc token] doc)
(defmethod apply-token "sidenote_open" [doc token] (-> doc (assoc :sidenotes? true) (open-node :sidenote {:ref (get-in* token [:meta :id])})))
(defmethod apply-token "sidenote_close" [doc token] (close-node doc))
(defmethod apply-token "sidenote_block_open" [doc token] (-> doc (assoc :sidenotes? true) (open-node :sidenote {:ref (get-in* token [:meta :id])})))
(defmethod apply-token "sidenote_block_close" [doc token] (close-node doc))
;; tables
;; table data tokens might have {:style "text-align:right|left"} attrs, maybe better nested node > :attrs > :style ?
(defmethod apply-token "table_open" [doc _token] (open-node doc :table))
(defmethod apply-token "table_close" [doc _token] (close-node doc))
(defmethod apply-token "thead_open" [doc _token] (open-node doc :table-head))
(defmethod apply-token "thead_close" [doc _token] (close-node doc))
(defmethod apply-token "tr_open" [doc _token] (open-node doc :table-row))
(defmethod apply-token "tr_close" [doc _token] (close-node doc))
(defmethod apply-token "th_open" [doc token] (open-node doc :table-header (:attrs token)))
(defmethod apply-token "th_close" [doc _token] (close-node doc))
(defmethod apply-token "tbody_open" [doc _token] (open-node doc :table-body))
(defmethod apply-token "tbody_close" [doc _token] (close-node doc))
(defmethod apply-token "td_open" [doc token] (open-node doc :table-data (:attrs token)))
(defmethod apply-token "td_close" [doc _token] (close-node doc))
(comment
(->
"
| Syntax | JVM | JavaScript |
|--------|:------------------------:|--------------------------------:|
| foo | Loca _lDate_ ahoiii | goog.date.Date |
| bar | java.time.LocalTime | some [kinky](link/to/something) |
| bag | java.time.LocalDateTime | $\\phi$ |
"
nextjournal.markdown/parse
nextjournal.markdown.transform/->hiccup
))
;; ## Handling of Text Tokens
;;
;; normalize-tokenizer :: {:regex, :doc-handler} | {:tokenizer-fn, :handler} -> Tokenizer
;; Tokenizer :: {:tokenizer-fn :: TokenizerFn, :doc-handler :: DocHandler}
;;
;; Match :: Any
;; Handler :: Match -> Node
;; IndexedMatch :: (Match, Int, Int)
;; TokenizerFn :: String -> [IndexedMatch]
;; DocHandler :: Doc -> {:match :: Match} -> Doc
(def text-tokenizers
[{:regex #"(^|\B)#[\w-]+"
:handler (fn [match] {:type :hashtag :text (subs (match 0) 1)})}
{:regex #"\[\[([^\]]+)\]\]"
:handler (fn [match] {:type :internal-link :text (match 1)})}])
(defn normalize-tokenizer
"Normalizes a map of regex and handler into a Tokenizer"
[{:as tokenizer :keys [doc-handler handler regex tokenizer-fn]}]
(assert (and (or doc-handler handler) (or regex tokenizer-fn)))
(cond-> tokenizer
(not doc-handler) (assoc :doc-handler (fn [doc {:keys [match]}] (push-node doc (handler match))))
(not tokenizer-fn) (assoc :tokenizer-fn (partial re-idx-seq regex))))
(defn tokenize-text-node [{:as tkz :keys [tokenizer-fn doc-handler]} {:as node :keys [text]}]
;; TokenizerFn -> HNode -> [HNode]
(assert (and (fn? tokenizer-fn) (fn? doc-handler) (string? text))
{:text text :tokenizer tkz})
(let [idx-seq (tokenizer-fn text)]
(if (seq idx-seq)
(let [text-hnode (fn [s] (assoc (text-node s) :doc-handler push-node))
{:keys [nodes remaining-text]}
(reduce (fn [{:as acc :keys [remaining-text]} [match start end]]
(-> acc
(update :remaining-text subs 0 start)
(cond->
(< end (count remaining-text))
(update :nodes conj (text-hnode (subs remaining-text end))))
(update :nodes conj {:doc-handler doc-handler
:match match :text text
:start start :end end})))
{:remaining-text text :nodes ()}
(reverse idx-seq))]
(cond-> nodes
(seq remaining-text)
(conj (text-hnode remaining-text))))
[node])))
(defmethod apply-token "text" [{:as doc :keys [text-tokenizers]} {:keys [content]}]
(reduce (fn [doc {:as node :keys [doc-handler]}] (doc-handler doc (dissoc node :doc-handler)))
doc
(reduce (fn [nodes tokenizer]
(mapcat (fn [{:as node :keys [type]}]
(if (= :text type) (tokenize-text-node tokenizer node) [node]))
nodes))
[{:type :text :text content :doc-handler push-node}]
text-tokenizers)))
(comment
(def mustache (normalize-tokenizer {:regex #"\{\{([^\{]+)\}\}" :handler (fn [m] {:type :eval :text (m 1)})}))
(tokenize-text-node mustache {:text "{{what}} the {{hellow}}"})
(apply-token (assoc empty-doc :text-tokenizers [mustache])
{:type "text" :content "foo [[bar]] dang #hashy taggy [[what]] #dangy foo [[great]] and {{eval}} me"})
(nextjournal.markdown/parse "foo [[bar]] dang #hashy taggy [[what]] #dangy foo [[great]]" )
(parse (assoc empty-doc
:text-tokenizers
[(normalize-tokenizer {:regex #"\{\{([^\{]+)\}\}"
:doc-handler (fn [doc {[_ meta] :match}]
(update-in doc (pop (pop (::path ddoc))) assoc :meta meta))})])
(nextjournal.markdown/tokenize "# Title {{id=heading}}
* one
* two")))
;; inlines
(defmethod apply-token "inline" [doc {:as _token ts :children}] (apply-tokens doc ts))
(defmethod apply-token "math_inline" [doc {text :content}] (push-node doc (formula text)))
(defmethod apply-token "math_inline_double" [doc {text :content}] (push-node doc (formula text)))
(defmethod apply-token "softbreak" [doc _token] (push-node doc {:type :softbreak}))
;; images
(defmethod apply-token "image" [doc {:keys [attrs children]}] (-> doc (open-node :image attrs) (apply-tokens children) close-node))
;; marks
(defmethod apply-token "em_open" [doc _token] (open-node doc :em))
(defmethod apply-token "em_close" [doc _token] (close-node doc))
(defmethod apply-token "strong_open" [doc _token] (open-node doc :strong))
(defmethod apply-token "strong_close" [doc _token] (close-node doc))
(defmethod apply-token "s_open" [doc _token] (open-node doc :strikethrough))
(defmethod apply-token "s_close" [doc _token] (close-node doc))
(defmethod apply-token "link_open" [doc token] (open-node doc :link (:attrs token)))
(defmethod apply-token "link_close" [doc _token] (close-node doc))
(defmethod apply-token "code_inline" [doc {text :content}] (-> doc (open-node :monospace) (push-node (text-node text)) close-node))
;; html (ignored)
(defmethod apply-token "html_inline" [doc _] doc)
(defmethod apply-token "html_block" [doc _] doc)
;; endregion
;; region data builder api
(defn pairs->kmap [pairs] (into {} (map (juxt (comp keyword first) second)) pairs))
(defn apply-tokens [doc tokens]
(let [mapify-attrs-xf (map (fn [x] (update* x :attrs pairs->kmap)))]
(reduce (mapify-attrs-xf apply-token) doc tokens)))
(def empty-doc {:type :doc
:content []
;; Id -> Nat, to disambiguate ids for nodes with the same textual content
::id->index {}
;; Node -> {id : String, emoji String}, dissoc from context to opt-out of ids
:text->id+emoji-fn (comp text->id+emoji md.transform/->text)
:toc {:type :toc}
::path [:content -1] ;; private
:text-tokenizers text-tokenizers})
(defn parse
"Takes a doc and a collection of markdown-it tokens, applies tokens to doc. Uses an emtpy doc in arity 1."
([tokens] (parse empty-doc tokens))
([doc tokens] (-> doc
(update :text-tokenizers (partial map normalize-tokenizer))
(apply-tokens tokens)
(dissoc ::path :text-tokenizers))))
(comment
(-> "# 🎱 Markdown Data
some _emphatic_ **strong** [link](https://foo.com)
---
> some ~~nice~~ quote
> for fun
## Formulas
[[TOC]]
$$\\Pi^2$$
- [ ] and
- [x] some $\\Phi_{\\alpha}$ latext
- [ ] bullets
## Sidenotes
here [^mynote] to somewhere
## Fences
```py id=\"aaa-bbb-ccc\"
1
print(\"this is some python\")
2
3
```
![Image Text](https://img.icons8.com/officel/16/000000/public.png)
Hline Section
-------------
### but also [[indented code]]
import os
os.listdir('/')
or monospace mark [`real`](/foo/bar) fun.
[^mynote]: Here you _can_ `explain` at lenght
"
nextjournal.markdown/tokenize
parse
;;seq
;;(->> (take 10))
;;(->> (take-last 4))
))
;; endregion
;; region zoom-in at section
(defn section-at [{:as doc :keys [content]} [_ pos :as path]]
;; TODO: generalize over path (zoom-in at)
;; supports only top-level headings atm (as found in TOC)
(let [{:as h section-level :heading-level} (get-in doc path)
in-section? (fn [{l :heading-level}] (or (not l) (< section-level l)))]
(when section-level
{:type :doc
:content (cons h
(->> content
(drop (inc pos))
(take-while in-section?)))})))
(comment
(some-> "# Title
## Section 1
foo
- # What is this? (no!)
- maybe
### Section 1.2
## Section 2
some par
### Section 2.1
some other par
### Section 2.2
#### Section 2.2.1
two two one
#### Section 2.2.2
two two two
## Section 3
some final par"
nextjournal.markdown/parse
(section-at [:content 9]) ;; ⬅ paths are stored in TOC sections
nextjournal.markdown.transform/->hiccup))
;; endregion