-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathast.cljc
More file actions
115 lines (95 loc) · 4.06 KB
/
Copy pathast.cljc
File metadata and controls
115 lines (95 loc) · 4.06 KB
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
(ns parser.ast
"AST shape + walking helpers + source-rendering inverse.
Every node is a 3-element hiccup-style vector:
[:tag {attrs} content]
- Position 0: keyword tag (from `known-tags`)
- Position 1: attrs map (always present, possibly `{}`)
- Position 2: content — either a string (for `:text`, `:math-inline`,
`:math-block`) or a vector of child nodes (for everything else). The
shape is uniform so `(nth node 1)` is always attrs and `(nth node 2)`
always content — no branching on arity in the walker.
The `:marker` attribute on `:bold`, `:italic`, `:highlight`,
`:strikethrough` preserves the exact marker the user typed (`**` vs
`__`, `*` vs `_`) so clipboard round-trip keeps source stable. Never
canonicalize."
(:require [clojure.string :as str]
[parser.page-refs :as page-refs]))
(def known-tags
#{:text :bold :italic :highlight :strikethrough
:math-inline :math-block :link :page-ref :image :doc})
(defn tag [node] (nth node 0))
(defn attrs [node] (nth node 1))
(defn content [node] (nth node 2))
(defn text? [node] (= :text (tag node)))
(defn leaf?
"True for nodes whose `content` is a raw string or empty children vector —
i.e. nothing to recurse into."
[node]
(let [t (tag node)]
(or (= :text t)
(#{:math-inline :math-block :image} t))))
(defn children
"Return the vector of child nodes, or [] for leaves."
[node]
(if (leaf? node)
[]
(content node)))
(defn node?
"Shape predicate. Used in tests, not on the render hot path."
[x]
(and (vector? x)
(= 3 (count x))
(contains? known-tags (nth x 0))
(map? (nth x 1))
(or (string? (nth x 2))
(and (vector? (nth x 2))
(every? node? (nth x 2))))))
;; ── Source rendering (inverse of parse) ──────────────────────────────────────
(declare render-as-source)
(defn- render-children [nodes]
(apply str (map render-as-source nodes)))
(defn render-as-source
"Emit source markdown from an AST node (or a vector of nodes).
Inverse of `parser.parse/parse`: `(parse (render-as-source (parse s)))`
must equal `(parse s)` for every source `s` the parser produces. The
inverse holds on parser output, not on every arbitrary AST — e.g. an
italic node with `:marker \"*\"` wrapping text with leading whitespace
won't round-trip because the parser rejects `* foo *`. Tests stay on
parser outputs.
Accepts either a single node or a vector of sibling nodes."
[node-or-nodes]
(cond
;; Sibling vector (untagged — caller passed (:content doc) etc.)
(and (vector? node-or-nodes)
(every? #(and (vector? %)
(contains? known-tags (first %)))
node-or-nodes))
(render-children node-or-nodes)
;; Single node
(vector? node-or-nodes)
(let [[t a c] node-or-nodes]
(case t
:doc (render-children c)
:text (str c)
:bold (let [m (or (:marker a) "**")] (str m (render-children c) m))
:italic (let [m (or (:marker a) "_")] (str m (render-children c) m))
:highlight (str "==" (render-children c) "==")
:strikethrough (str "~~" (render-children c) "~~")
:math-inline (str "$" c "$")
:math-block (str "$$" c "$$")
:link (str "[" (render-children c) "](" (:target a) ")")
:page-ref (page-refs/format-ref (:name a))
:image (let [{:keys [alt path width]} a
base (str "")]
(if width (str base "{width=" width "}") base))))
:else
(str node-or-nodes)))
;; ── Construction helpers (internal convenience) ─────────────────────────────
(defn text-node
"Build a :text leaf. Null-safe."
[s]
[:text {} (or s "")])
(defn blank?
"True if a :text leaf has empty string content."
[node]
(and (text? node) (str/blank? (content node))))