(ns devtools.formatters.printing (:require-macros [devtools.oops :refer [safe-call]]) (:require [devtools.prefs :refer [pref]] [devtools.format :refer [IDevtoolsFormat]] [devtools.protocols :refer [ITemplate IGroup ISurrogate IFormat]] [devtools.formatters.state :refer [push-object-to-current-history! *current-state* get-current-state is-circular? get-managed-print-level set-managed-print-level update-current-state!]] [devtools.formatters.helpers :refer [cljs-value? expandable? abbreviated? directly-printable? should-render?]])) ; -- helpers ---------------------------------------------------------------------------------------------------------------- (defn markup? [value] (::markup (meta value))) (defn mark-as-markup [value] (with-meta value {::markup true})) (defn build-markup [markup-db fn-key & args] (let [f (get markup-db fn-key)] (assert f (str "missing markup method in markup-db: " fn-key)) (mark-as-markup (apply f args)))) (defn wrap-value-as-reference-if-needed [markup-db value] (if (or (directly-printable? value) (markup? value)) value (build-markup markup-db :reference-surrogate value))) ; -- TemplateWriter --------------------------------------------------------------------------------------------------------- (deftype TemplateWriter [^:mutable group markup-db] Object (merge [_ a] (set! group (concat group a))) (get-group [_] group) IWriter (-write [_ o] (set! group (concat group [(wrap-value-as-reference-if-needed markup-db o)]))) ; issue #21 (-flush [_] nil)) (defn make-template-writer [markup-db] (TemplateWriter. [] markup-db)) ; -- post-processing -------------------------------------------------------------------------------------------------------- (defn already-reference? [group] (if-let [tag (first (first group))] (= tag "reference"))) (defn wrap-group-in-reference-if-needed [group obj markup-db] (if (and (not (already-reference? group)) (or (expandable? obj) (abbreviated? group))) (let [expandable-markup (apply build-markup markup-db :expandable group) surrogate-markup (build-markup markup-db :raw-surrogate obj expandable-markup :target) reference-markup (build-markup markup-db :reference surrogate-markup)] [reference-markup]) group)) (defn wrap-group-in-circular-warning-if-needed [group markup-db circular?] (if circular? [(apply build-markup markup-db :circular-reference group)] group)) (defn wrap-group-in-meta-if-needed [group value markup-db] (if (should-render? :render-metas value #(some? (meta %))) [(apply (partial (:meta-wrapper markup-db) (meta value)) group)] group)) ; default printer implementation can do this: ; :else (write-all writer "#<" (str obj) ">") ; we want to wrap stringified obj in a reference for further inspection ; ; this behaviour changed in https://github.com/clojure/clojurescript/commit/34c3b8985ed8197d90f441c46d168c4024a20eb8 ; newly functions and :else branch print "#object [" ... "]" ; ; in some situations obj can still be a clojurescript value (e.g. deftypes) ; we have to implement a special flag to prevent infinite recursion ; see https://github.com/binaryage/cljs-devtools/issues/2 ; https://github.com/binaryage/cljs-devtools/issues/8 (defn detect-edge-case-and-patch-it [group obj markup-db] (cond (or (and (= (count group) 5) (= (nth group 0) "#object[") (= (nth group 4) "\"]")) ; function case (and (= (count group) 5) (= (nth group 0) "#object[") (= (nth group 4) "]")) ; :else -constructor case (and (= (count group) 3) (= (nth group 0) "#object[") (= (nth group 2) "]"))) ; :else -cljs$lang$ctorStr case [(build-markup markup-db :native-reference obj)] (and (= (count group) 3) (= (nth group 0) "#<") (= (str obj) (nth group 1)) (= (nth group 2) ">")) ; old code prior r1.7.28 [(nth group 0) (build-markup :native-reference obj) (nth group 2)] :else group)) (defn post-process-printed-output [output-group obj markup-db circular?] (-> output-group (detect-edge-case-and-patch-it obj markup-db) ; an ugly hack (wrap-group-in-reference-if-needed obj markup-db) (wrap-group-in-circular-warning-if-needed markup-db circular?) (wrap-group-in-meta-if-needed obj markup-db))) ; -- alternative printer ---------------------------------------------------------------------------------------------------- (defn alt-printer-job [obj writer opts] (let [{:keys [markup-db]} opts] (if (or (safe-call satisfies? false IDevtoolsFormat obj) (safe-call satisfies? false IFormat obj)) ; we have to wrap value in reference if detected IFormat (-write writer (build-markup markup-db :reference obj)) (if-let [atomic-markup (build-markup markup-db :atomic obj)] (-write writer atomic-markup) (let [default-impl (:fallback-impl opts) ; we want to limit print-level, at max-print-level level use maximal abbreviation e.g. [...] or {...} inner-opts (if (= *print-level* 1) (assoc opts :print-length 0) opts)] (default-impl obj writer inner-opts)))))) (defn alt-printer-impl [obj writer opts] (binding [*current-state* (get-current-state)] (let [{:keys [markup-db]} opts circular? (is-circular? obj) inner-writer (make-template-writer (:markup-db opts))] (push-object-to-current-history! obj) (alt-printer-job obj inner-writer opts) (.merge writer (post-process-printed-output (.get-group inner-writer) obj markup-db circular?))))) ; -- common code for managed printing --------------------------------------------------------------------------------------- (defn managed-print [tag markup-db printer] (let [writer (make-template-writer markup-db) opts {:alt-impl alt-printer-impl :markup-db markup-db :print-length (pref :max-header-elements) :more-marker (pref :more-marker)} job-fn #(printer writer opts)] (if-let [managed-print-level (get-managed-print-level)] (binding [*print-level* managed-print-level] (update-current-state! #(set-managed-print-level % nil)) ; reset managed-print-level so it does not propagate further down in expaded data (job-fn)) (job-fn)) (concat [(pref tag)] (.get-group writer)))) ; -- public printing API ---------------------------------------------------------------------------------------------------- (defn managed-print-via-writer [value tag markup-db] (managed-print tag markup-db (fn [writer opts] (pr-seq-writer [value] writer opts)))) ; note we use pr-seq-writer becasue pr-writer is private for some reason (defn managed-print-via-protocol [value tag markup-db] (managed-print tag markup-db (fn [writer opts] (-pr-writer value writer opts))))