|
1 | 1 | (ns quo2.components.code.snippet
|
2 | 2 | (:require [cljs-bean.core :as bean]
|
3 | 3 | [clojure.string :as string]
|
4 |
| - [oops.core :as oops] |
5 | 4 | [quo2.components.buttons.button :as button]
|
| 5 | + [quo2.components.code.code.style :as style] |
6 | 6 | [quo2.components.markdown.text :as text]
|
7 |
| - [quo2.foundations.colors :as colors] |
8 |
| - [quo2.theme :as theme] |
9 | 7 | [react-native.core :as rn]
|
10 | 8 | [react-native.linear-gradient :as linear-gradient]
|
11 |
| - [react-native.masked-view :as masked-view] |
12 | 9 | [react-native.syntax-highlighter :as highlighter]
|
13 | 10 | [reagent.core :as reagent]))
|
14 | 11 |
|
15 |
| -;; Example themes: |
16 |
| -;; https://github.com/react-syntax-highlighter/react-syntax-highlighter/tree/master/src/styles/hljs |
17 |
| -(def ^:private themes |
18 |
| - {:light {:hljs-comment {:color colors/neutral-40} |
19 |
| - :hljs-title {:color (colors/custom-color :blue 50)} |
20 |
| - :hljs-keyword {:color (colors/custom-color :green 50)} |
21 |
| - :hljs-string {:color (colors/custom-color :turquoise 50)} |
22 |
| - :hljs-literal {:color (colors/custom-color :turquoise 50)} |
23 |
| - :hljs-number {:color (colors/custom-color :turquoise 50)} |
24 |
| - :line-number {:color colors/neutral-40}} |
25 |
| - :dark {:hljs-comment {:color colors/neutral-60} |
26 |
| - :hljs-title {:color (colors/custom-color :blue 60)} |
27 |
| - :hljs-keyword {:color (colors/custom-color :green 60)} |
28 |
| - :hljs-string {:color (colors/custom-color :turquoise 60)} |
29 |
| - :hljs-literal {:color (colors/custom-color :turquoise 60)} |
30 |
| - :hljs-number {:color (colors/custom-color :turquoise 60)} |
31 |
| - :line-number {:color colors/neutral-40}}}) |
32 |
| - |
33 |
| -(defn- text-style |
34 |
| - [class-names] |
35 |
| - (->> class-names |
36 |
| - (map keyword) |
37 |
| - (reduce #(merge %1 (get-in themes [(theme/get-theme) %2])) |
38 |
| - {:flex-shrink 1 |
39 |
| - ;; Round to a nearest whole number to achieve consistent |
40 |
| - ;; spacing (also important for calculating `max-text-height`). |
41 |
| - ;; Line height seems to be inconsistent between text being |
42 |
| - ;; wrapped and text being rendered on a newline using Flexbox |
43 |
| - ;; layout. |
44 |
| - :line-height 18}))) |
45 |
| - |
46 | 12 | (defn- render-nodes
|
47 | 13 | [nodes]
|
48 |
| - (map (fn [{:keys [children value] :as node}] |
49 |
| - ;; Node can have :children or a :value. |
| 14 | + (map (fn [{:keys [children value last-line?] :as node}] |
50 | 15 | (if children
|
51 | 16 | (into [text/text
|
52 |
| - {:weight :code |
53 |
| - :size :paragraph-2 |
54 |
| - :style (text-style (get-in node [:properties :className]))}] |
| 17 | + (cond-> {:weight :code |
| 18 | + :size :paragraph-2 |
| 19 | + :style (style/text-style (get-in node [:properties :className]))} |
| 20 | + last-line? (assoc :number-of-lines 1))] |
55 | 21 | (render-nodes children))
|
56 | 22 | ;; Remove newlines as we already render each line separately.
|
57 |
| - (-> value string/trim-newline))) |
| 23 | + (string/trim-newline value))) |
58 | 24 | nodes))
|
59 | 25 |
|
| 26 | +(defn- line |
| 27 | + [{:keys [line-number line-number-width]} children] |
| 28 | + [rn/view {:style style/line} |
| 29 | + [rn/view {:style (style/line-number line-number-width)} |
| 30 | + [text/text |
| 31 | + {:style (style/text-style ["line-number"]) |
| 32 | + :weight :code |
| 33 | + :size :paragraph-2} |
| 34 | + line-number]] |
| 35 | + children]) |
| 36 | + |
60 | 37 | (defn- code-block
|
61 | 38 | [{:keys [rows line-number-width]}]
|
62 |
| - [into [:<>] |
| 39 | + [rn/view |
63 | 40 | (->> rows
|
64 | 41 | (render-nodes)
|
65 |
| - ;; Line numbers |
66 |
| - (map-indexed (fn [idx row] |
67 |
| - (conj [rn/view {:style {:flex-direction :row}} |
68 |
| - [rn/view |
69 |
| - {:style {:width line-number-width |
70 |
| - ;; 8+12 margin |
71 |
| - :margin-right 20}} |
72 |
| - [text/text |
73 |
| - {:weight :code |
74 |
| - :size :paragraph-2 |
75 |
| - :style (text-style ["line-number"])} |
76 |
| - (inc idx)]]] |
77 |
| - row))))]) |
| 42 | + (map-indexed (fn [idx row-content] |
| 43 | + [line |
| 44 | + {:line-number (inc idx) |
| 45 | + :line-number-width line-number-width} |
| 46 | + row-content])) |
| 47 | + (into [:<>]))]) |
78 | 48 |
|
79 |
| -(defn- native-renderer |
80 |
| - [] |
81 |
| - (let [text-height (reagent/atom nil)] |
82 |
| - (fn [{:keys [rows max-lines on-copy-press]}] |
83 |
| - (let [background-color (colors/theme-colors |
84 |
| - colors/white |
85 |
| - colors/neutral-80-opa-40) |
86 |
| - background-color-left (colors/theme-colors |
87 |
| - colors/neutral-5 |
88 |
| - colors/neutral-80) |
89 |
| - border-color (colors/theme-colors |
90 |
| - colors/neutral-20 |
91 |
| - colors/neutral-80) |
92 |
| - rows (bean/->clj rows) |
93 |
| - font-scale (:font-scale (rn/use-window-dimensions)) |
94 |
| - max-rows (or max-lines (count rows)) ;; Cut down on rows to process. |
95 |
| - max-line-digits (-> rows count (min max-rows) str count) |
96 |
| - ;; ~ 9 is char width, 18 is width used in Figma. |
97 |
| - line-number-width (* font-scale (max 18 (* 9 max-line-digits))) |
98 |
| - max-text-height (some-> max-lines |
99 |
| - (* font-scale 18)) ;; 18 is font's line height. |
100 |
| - truncated? (and max-text-height (< max-text-height @text-height)) |
101 |
| - maybe-mask-wrapper (if truncated? |
102 |
| - [masked-view/masked-view |
103 |
| - {:mask-element |
104 |
| - (reagent/as-element |
105 |
| - [linear-gradient/linear-gradient |
106 |
| - {:colors ["black" "transparent"] |
107 |
| - :locations [0.75 1] |
108 |
| - :style {:flex 1}}])}] |
109 |
| - [:<>])] |
110 |
| - |
111 |
| - [rn/view |
112 |
| - {:style {:overflow :hidden |
113 |
| - :padding 8 |
114 |
| - :background-color background-color |
115 |
| - :border-color border-color |
116 |
| - :border-width 1 |
117 |
| - :border-radius 8 |
118 |
| - ;; Hide on intial render to avoid flicker when mask-wrapper is shown. |
119 |
| - :opacity (if @text-height 1 0)}} |
120 |
| - ;; Line number container |
121 |
| - [rn/view |
122 |
| - {:style {:position :absolute |
123 |
| - :bottom 0 |
124 |
| - :top 0 |
125 |
| - :left 0 |
126 |
| - :width (+ line-number-width 8 8) |
127 |
| - :background-color background-color-left |
128 |
| - :border-right-color border-color |
129 |
| - :border-right-width 1}}] |
130 |
| - (conj maybe-mask-wrapper |
131 |
| - [rn/view {:max-height max-text-height} |
132 |
| - [rn/view |
133 |
| - {:on-layout (fn [evt] |
134 |
| - (let [height (oops/oget evt "nativeEvent.layout.height")] |
135 |
| - (reset! text-height height)))} |
136 |
| - [code-block |
137 |
| - {:rows (take max-rows rows) |
138 |
| - :line-number-width line-number-width}]]]) |
| 49 | +(defn- mask-view |
| 50 | + [{:keys [apply-mask?]} child] |
| 51 | + (if apply-mask? |
| 52 | + [:<> |
| 53 | + [rn/view {:style style/gradient-container} |
| 54 | + [linear-gradient/linear-gradient |
| 55 | + {:style style/gradient |
| 56 | + :colors [:transparent (style/gradient-color)]} |
| 57 | + [rn/view {:style style/gradient}]]] |
| 58 | + child] |
| 59 | + child)) |
139 | 60 |
|
140 |
| - ;; Copy button |
141 |
| - [rn/view |
142 |
| - {:style {:position :absolute |
143 |
| - :bottom 8 |
144 |
| - :right 8}} |
145 |
| - [button/button |
146 |
| - {:icon true |
147 |
| - :type :grey |
148 |
| - :size 24 |
149 |
| - :on-press on-copy-press} |
150 |
| - :main-icons/copy]]])))) |
| 61 | +(defn- calc-line-number-width |
| 62 | + [font-scale rows-to-show] |
| 63 | + (let [max-line-digits (-> rows-to-show str count)] |
| 64 | + (if (= 1 max-line-digits) |
| 65 | + 18 ;; ~ 9 is char width, 18 is width used in Figma. |
| 66 | + (* 9 max-line-digits font-scale)))) |
151 | 67 |
|
152 |
| -(defn- wrap-renderer-fn |
153 |
| - [f {:keys [max-lines on-copy-press]}] |
154 |
| - (fn [^js props] |
155 |
| - (reagent/as-element [:f> f |
156 |
| - {:rows (.-rows props) |
157 |
| - :max-lines max-lines |
158 |
| - :on-copy-press on-copy-press}]))) |
| 68 | +(defn- native-renderer |
| 69 | + [{:keys [rows max-lines on-copy-press] |
| 70 | + :or {max-lines ##Inf}}] |
| 71 | + (let [font-scale (:font-scale (rn/use-window-dimensions)) |
| 72 | + total-rows (count rows) |
| 73 | + number-rows-to-show (min (count rows) max-lines) |
| 74 | + line-number-width (calc-line-number-width font-scale number-rows-to-show) |
| 75 | + truncated? (< number-rows-to-show total-rows) |
| 76 | + rows-to-show-coll (if truncated? |
| 77 | + (as-> rows $ |
| 78 | + (update $ number-rows-to-show assoc :last-line? true) |
| 79 | + (take (inc number-rows-to-show) $)) |
| 80 | + rows)] |
| 81 | + [rn/view {:style (style/container)} |
| 82 | + [rn/view {:style (style/line-number-container line-number-width)}] |
| 83 | + [rn/view {:style (style/divider line-number-width)}] |
| 84 | + [mask-view {:apply-mask? truncated?} |
| 85 | + [code-block |
| 86 | + {:rows rows-to-show-coll |
| 87 | + :line-number-width line-number-width}]] |
| 88 | + [rn/view {:style style/copy-button} |
| 89 | + [button/button |
| 90 | + {:icon true |
| 91 | + :type :grey |
| 92 | + :size 24 |
| 93 | + :on-press on-copy-press |
| 94 | + :override-background-color (style/button-background-color)} |
| 95 | + :main-icons/copy]]])) |
159 | 96 |
|
160 | 97 | (defn snippet
|
161 | 98 | [{:keys [language max-lines on-copy-press]} children]
|
162 | 99 | [highlighter/highlighter
|
163 | 100 | {:language language
|
164 |
| - :renderer (wrap-renderer-fn |
165 |
| - native-renderer |
166 |
| - {:max-lines max-lines |
167 |
| - :on-copy-press #(when on-copy-press |
168 |
| - (on-copy-press children))}) |
169 |
| - ;; Default props to adapt Highlighter for react-native. |
170 |
| - ;;:CodeTag react-native/View |
171 |
| - ;;:PreTag react-native/View |
| 101 | + :renderer (fn [^js/Object props] |
| 102 | + (reagent/as-element |
| 103 | + [:f> native-renderer |
| 104 | + {:rows (-> props .-rows bean/->clj) |
| 105 | + :on-copy-press #(when on-copy-press (on-copy-press children)) |
| 106 | + :max-lines max-lines}])) |
172 | 107 | :show-line-numbers false
|
173 |
| - :style #js {} |
174 |
| - :custom-style #js {:backgroundColor nil}} |
| 108 | + :style {} |
| 109 | + :custom-style {:background-color nil}} |
175 | 110 | children])
|
0 commit comments