3325 lines
125 KiB
Clojure
3325 lines
125 KiB
Clojure
; Copyright (c) Rich Hickey. All rights reserved.
|
|
; The use and distribution terms for this software are covered by the
|
|
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
|
|
; which can be found in the file epl-v10.html at the root of this distribution.
|
|
; By using this software in any fashion, you are agreeing to be bound by
|
|
; the terms of this license.
|
|
; You must not remove this notice, or any other, from this software.
|
|
|
|
(ns cljs.pprint
|
|
(:refer-clojure :exclude [deftype print println pr prn float?])
|
|
(:require-macros
|
|
[cljs.pprint :as m :refer [with-pretty-writer getf setf deftype
|
|
pprint-logical-block print-length-loop
|
|
defdirectives formatter-out]])
|
|
(:require
|
|
[cljs.core :refer [IWriter IDeref]]
|
|
[clojure.string :as string]
|
|
[goog.string :as gstring])
|
|
(:import [goog.string StringBuffer]))
|
|
|
|
;;======================================================================
|
|
;; override print fns to use *out*
|
|
;;======================================================================
|
|
|
|
(defn- print [& more]
|
|
(-write *out* (apply print-str more)))
|
|
|
|
(defn- println [& more]
|
|
(apply print more)
|
|
(-write *out* \newline))
|
|
|
|
(defn- print-char [c]
|
|
(-write *out* (condp = c
|
|
\backspace "\\backspace"
|
|
\tab "\\tab"
|
|
\newline "\\newline"
|
|
\formfeed "\\formfeed"
|
|
\return "\\return"
|
|
\" "\\\""
|
|
\\ "\\\\"
|
|
(str "\\" c))))
|
|
|
|
(defn- ^:dynamic pr [& more]
|
|
(-write *out* (apply pr-str more)))
|
|
|
|
(defn- prn [& more]
|
|
(apply pr more)
|
|
(-write *out* \newline))
|
|
|
|
;;======================================================================
|
|
;; cljs specific utils
|
|
;;======================================================================
|
|
|
|
(defn float?
|
|
"Returns true if n is an float."
|
|
[n]
|
|
(and (number? n)
|
|
(not ^boolean (js/isNaN n))
|
|
(not (identical? n js/Infinity))
|
|
(not (== (js/parseFloat n) (js/parseInt n 10)))))
|
|
|
|
(defn char-code
|
|
"Convert char to int"
|
|
[c]
|
|
(cond
|
|
(number? c) c
|
|
(and (string? c) (== (.-length c) 1)) (.charCodeAt c 0)
|
|
:else (throw (js/Error. "Argument to char must be a character or number"))))
|
|
|
|
;;======================================================================
|
|
;; Utilities
|
|
;;======================================================================
|
|
|
|
(defn- map-passing-context [func initial-context lis]
|
|
(loop [context initial-context
|
|
lis lis
|
|
acc []]
|
|
(if (empty? lis)
|
|
[acc context]
|
|
(let [this (first lis)
|
|
remainder (next lis)
|
|
[result new-context] (apply func [this context])]
|
|
(recur new-context remainder (conj acc result))))))
|
|
|
|
(defn- consume [func initial-context]
|
|
(loop [context initial-context
|
|
acc []]
|
|
(let [[result new-context] (apply func [context])]
|
|
(if (not result)
|
|
[acc new-context]
|
|
(recur new-context (conj acc result))))))
|
|
|
|
(defn- consume-while [func initial-context]
|
|
(loop [context initial-context
|
|
acc []]
|
|
(let [[result continue new-context] (apply func [context])]
|
|
(if (not continue)
|
|
[acc context]
|
|
(recur new-context (conj acc result))))))
|
|
|
|
(defn- unzip-map [m]
|
|
"Take a map that has pairs in the value slots and produce a pair of maps,
|
|
the first having all the first elements of the pairs and the second all
|
|
the second elements of the pairs"
|
|
[(into {} (for [[k [v1 v2]] m] [k v1]))
|
|
(into {} (for [[k [v1 v2]] m] [k v2]))])
|
|
|
|
(defn- tuple-map [m v1]
|
|
"For all the values, v, in the map, replace them with [v v1]"
|
|
(into {} (for [[k v] m] [k [v v1]])))
|
|
|
|
(defn- rtrim [s c]
|
|
"Trim all instances of c from the end of sequence s"
|
|
(let [len (count s)]
|
|
(if (and (pos? len) (= (nth s (dec (count s))) c))
|
|
(loop [n (dec len)]
|
|
(cond
|
|
(neg? n) ""
|
|
(not (= (nth s n) c)) (subs s 0 (inc n))
|
|
true (recur (dec n))))
|
|
s)))
|
|
|
|
(defn- ltrim [s c]
|
|
"Trim all instances of c from the beginning of sequence s"
|
|
(let [len (count s)]
|
|
(if (and (pos? len) (= (nth s 0) c))
|
|
(loop [n 0]
|
|
(if (or (= n len) (not (= (nth s n) c)))
|
|
(subs s n)
|
|
(recur (inc n))))
|
|
s)))
|
|
|
|
(defn- prefix-count [aseq val]
|
|
"Return the number of times that val occurs at the start of sequence aseq,
|
|
if val is a seq itself, count the number of times any element of val occurs at the
|
|
beginning of aseq"
|
|
(let [test (if (coll? val) (set val) #{val})]
|
|
(loop [pos 0]
|
|
(if (or (= pos (count aseq)) (not (test (nth aseq pos))))
|
|
pos
|
|
(recur (inc pos))))))
|
|
|
|
;; Flush the pretty-print buffer without flushing the underlying stream
|
|
(defprotocol IPrettyFlush
|
|
(-ppflush [pp]))
|
|
|
|
;;======================================================================
|
|
;; column_writer.clj
|
|
;;======================================================================
|
|
|
|
(def ^:dynamic ^{:private true} *default-page-width* 72)
|
|
|
|
(defn- get-field [this sym]
|
|
(sym @@this))
|
|
|
|
(defn- set-field [this sym new-val]
|
|
(swap! @this assoc sym new-val))
|
|
|
|
(defn- get-column [this]
|
|
(get-field this :cur))
|
|
|
|
(defn- get-line [this]
|
|
(get-field this :line))
|
|
|
|
(defn- get-max-column [this]
|
|
(get-field this :max))
|
|
|
|
(defn- set-max-column [this new-max]
|
|
(set-field this :max new-max)
|
|
nil)
|
|
|
|
(defn- get-writer [this]
|
|
(get-field this :base))
|
|
|
|
;; Why is the c argument an integer?
|
|
(defn- c-write-char [this c]
|
|
(if (= c \newline)
|
|
(do
|
|
(set-field this :cur 0)
|
|
(set-field this :line (inc (get-field this :line))))
|
|
(set-field this :cur (inc (get-field this :cur))))
|
|
(-write (get-field this :base) c))
|
|
|
|
(defn- column-writer
|
|
([writer] (column-writer writer *default-page-width*))
|
|
([writer max-columns]
|
|
(let [fields (atom {:max max-columns, :cur 0, :line 0 :base writer})]
|
|
(reify
|
|
|
|
IDeref
|
|
(-deref [_] fields)
|
|
|
|
IWriter
|
|
(-flush [_]
|
|
(-flush writer))
|
|
(-write
|
|
;;-write isn't multi-arity, so need different way to do this
|
|
#_([this ^chars cbuf ^Number off ^Number len]
|
|
(let [writer (get-field this :base)]
|
|
(-write writer cbuf off len)))
|
|
[this x]
|
|
(condp = (type x)
|
|
js/String
|
|
(let [s x
|
|
nl (.lastIndexOf s \newline)]
|
|
(if (neg? nl)
|
|
(set-field this :cur (+ (get-field this :cur) (count s)))
|
|
(do
|
|
(set-field this :cur (- (count s) nl 1))
|
|
(set-field this :line (+ (get-field this :line)
|
|
(count (filter #(= % \newline) s))))))
|
|
(-write (get-field this :base) s))
|
|
js/Number
|
|
(c-write-char this x)))))))
|
|
|
|
;;======================================================================
|
|
;; pretty_writer.clj
|
|
;;======================================================================
|
|
|
|
;;======================================================================
|
|
;; Forward declarations
|
|
;;======================================================================
|
|
|
|
(declare ^{:arglists '([this])} get-miser-width)
|
|
|
|
;;======================================================================
|
|
;; The data structures used by pretty-writer
|
|
;;======================================================================
|
|
|
|
(defrecord ^{:private true} logical-block
|
|
[parent section start-col indent
|
|
done-nl intra-block-nl
|
|
prefix per-line-prefix suffix
|
|
logical-block-callback])
|
|
|
|
(defn- ancestor? [parent child]
|
|
(loop [child (:parent child)]
|
|
(cond
|
|
(nil? child) false
|
|
(identical? parent child) true
|
|
:else (recur (:parent child)))))
|
|
|
|
(defn- buffer-length [l]
|
|
(let [l (seq l)]
|
|
(if l
|
|
(- (:end-pos (last l)) (:start-pos (first l)))
|
|
0)))
|
|
|
|
;; A blob of characters (aka a string)
|
|
(deftype buffer-blob :data :trailing-white-space :start-pos :end-pos)
|
|
|
|
;; A newline
|
|
(deftype nl-t :type :logical-block :start-pos :end-pos)
|
|
|
|
(deftype start-block-t :logical-block :start-pos :end-pos)
|
|
|
|
(deftype end-block-t :logical-block :start-pos :end-pos)
|
|
|
|
(deftype indent-t :logical-block :relative-to :offset :start-pos :end-pos)
|
|
|
|
(def ^:private pp-newline (fn [] "\n"))
|
|
|
|
(declare emit-nl)
|
|
|
|
(defmulti ^{:private true} write-token #(:type-tag %2))
|
|
|
|
(defmethod write-token :start-block-t [this token]
|
|
(when-let [cb (getf :logical-block-callback)] (cb :start))
|
|
(let [lb (:logical-block token)]
|
|
(when-let [prefix (:prefix lb)]
|
|
(-write (getf :base) prefix))
|
|
(let [col (get-column (getf :base))]
|
|
(reset! (:start-col lb) col)
|
|
(reset! (:indent lb) col))))
|
|
|
|
(defmethod write-token :end-block-t [this token]
|
|
(when-let [cb (getf :logical-block-callback)] (cb :end))
|
|
(when-let [suffix (:suffix (:logical-block token))]
|
|
(-write (getf :base) suffix)))
|
|
|
|
(defmethod write-token :indent-t [this token]
|
|
(let [lb (:logical-block token)]
|
|
(reset! (:indent lb)
|
|
(+ (:offset token)
|
|
(condp = (:relative-to token)
|
|
:block @(:start-col lb)
|
|
:current (get-column (getf :base)))))))
|
|
|
|
(defmethod write-token :buffer-blob [this token]
|
|
(-write (getf :base) (:data token)))
|
|
|
|
(defmethod write-token :nl-t [this token]
|
|
(if (or (= (:type token) :mandatory)
|
|
(and (not (= (:type token) :fill))
|
|
@(:done-nl (:logical-block token))))
|
|
(emit-nl this token)
|
|
(if-let [tws (getf :trailing-white-space)]
|
|
(-write (getf :base) tws)))
|
|
(setf :trailing-white-space nil))
|
|
|
|
(defn- write-tokens [this tokens force-trailing-whitespace]
|
|
(doseq [token tokens]
|
|
(if-not (= (:type-tag token) :nl-t)
|
|
(if-let [tws (getf :trailing-white-space)]
|
|
(-write (getf :base) tws)))
|
|
(write-token this token)
|
|
(setf :trailing-white-space (:trailing-white-space token))
|
|
(let [tws (getf :trailing-white-space)]
|
|
(when (and force-trailing-whitespace tws)
|
|
(-write (getf :base) tws)
|
|
(setf :trailing-white-space nil)))))
|
|
|
|
;;======================================================================
|
|
;; emit-nl? method defs for each type of new line. This makes
|
|
;; the decision about whether to print this type of new line.
|
|
;;======================================================================
|
|
|
|
(defn- tokens-fit? [this tokens]
|
|
(let [maxcol (get-max-column (getf :base))]
|
|
(or
|
|
(nil? maxcol)
|
|
(< (+ (get-column (getf :base)) (buffer-length tokens)) maxcol))))
|
|
|
|
(defn- linear-nl? [this lb section]
|
|
(or @(:done-nl lb)
|
|
(not (tokens-fit? this section))))
|
|
|
|
(defn- miser-nl? [this lb section]
|
|
(let [miser-width (get-miser-width this)
|
|
maxcol (get-max-column (getf :base))]
|
|
(and miser-width maxcol
|
|
(>= @(:start-col lb) (- maxcol miser-width))
|
|
(linear-nl? this lb section))))
|
|
|
|
(defmulti ^{:private true} emit-nl? (fn [t _ _ _] (:type t)))
|
|
|
|
(defmethod emit-nl? :linear [newl this section _]
|
|
(let [lb (:logical-block newl)]
|
|
(linear-nl? this lb section)))
|
|
|
|
(defmethod emit-nl? :miser [newl this section _]
|
|
(let [lb (:logical-block newl)]
|
|
(miser-nl? this lb section)))
|
|
|
|
(defmethod emit-nl? :fill [newl this section subsection]
|
|
(let [lb (:logical-block newl)]
|
|
(or @(:intra-block-nl lb)
|
|
(not (tokens-fit? this subsection))
|
|
(miser-nl? this lb section))))
|
|
|
|
(defmethod emit-nl? :mandatory [_ _ _ _]
|
|
true)
|
|
|
|
;;======================================================================
|
|
;; Various support functions
|
|
;;======================================================================
|
|
|
|
(defn- get-section [buffer]
|
|
(let [nl (first buffer)
|
|
lb (:logical-block nl)
|
|
section (seq (take-while #(not (and (nl-t? %) (ancestor? (:logical-block %) lb)))
|
|
(next buffer)))]
|
|
[section (seq (drop (inc (count section)) buffer))]))
|
|
|
|
(defn- get-sub-section [buffer]
|
|
(let [nl (first buffer)
|
|
lb (:logical-block nl)
|
|
section (seq (take-while #(let [nl-lb (:logical-block %)]
|
|
(not (and (nl-t? %) (or (= nl-lb lb) (ancestor? nl-lb lb)))))
|
|
(next buffer)))]
|
|
section))
|
|
|
|
(defn- update-nl-state [lb]
|
|
(reset! (:intra-block-nl lb) true)
|
|
(reset! (:done-nl lb) true)
|
|
(loop [lb (:parent lb)]
|
|
(if lb
|
|
(do (reset! (:done-nl lb) true)
|
|
(reset! (:intra-block-nl lb) true)
|
|
(recur (:parent lb))))))
|
|
|
|
(defn- emit-nl [this nl]
|
|
(-write (getf :base) (pp-newline))
|
|
(setf :trailing-white-space nil)
|
|
(let [lb (:logical-block nl)
|
|
prefix (:per-line-prefix lb)]
|
|
(if prefix
|
|
(-write (getf :base) prefix))
|
|
(let [istr (apply str (repeat (- @(:indent lb) (count prefix)) \space))]
|
|
(-write (getf :base) istr))
|
|
(update-nl-state lb)))
|
|
|
|
(defn- split-at-newline [tokens]
|
|
(let [pre (seq (take-while #(not (nl-t? %)) tokens))]
|
|
[pre (seq (drop (count pre) tokens))]))
|
|
|
|
;; write-token-string is called when the set of tokens in the buffer
|
|
;; is long than the available space on the line
|
|
(defn- write-token-string [this tokens]
|
|
(let [[a b] (split-at-newline tokens)]
|
|
(if a (write-tokens this a false))
|
|
(if b
|
|
(let [[section remainder] (get-section b)
|
|
newl (first b)]
|
|
(let [do-nl (emit-nl? newl this section (get-sub-section b))
|
|
result (if do-nl
|
|
(do
|
|
(emit-nl this newl)
|
|
(next b))
|
|
b)
|
|
long-section (not (tokens-fit? this result))
|
|
result (if long-section
|
|
(let [rem2 (write-token-string this section)]
|
|
(if (= rem2 section)
|
|
(do ; If that didn't produce any output, it has no nls
|
|
; so we'll force it
|
|
(write-tokens this section false)
|
|
remainder)
|
|
(into [] (concat rem2 remainder))))
|
|
result)]
|
|
result)))))
|
|
|
|
(defn- write-line [this]
|
|
(loop [buffer (getf :buffer)]
|
|
(setf :buffer (into [] buffer))
|
|
(if (not (tokens-fit? this buffer))
|
|
(let [new-buffer (write-token-string this buffer)]
|
|
(if-not (identical? buffer new-buffer)
|
|
(recur new-buffer))))))
|
|
|
|
;; Add a buffer token to the buffer and see if it's time to start
|
|
;; writing
|
|
(defn- add-to-buffer [this token]
|
|
(setf :buffer (conj (getf :buffer) token))
|
|
(if (not (tokens-fit? this (getf :buffer)))
|
|
(write-line this)))
|
|
|
|
;; Write all the tokens that have been buffered
|
|
(defn- write-buffered-output [this]
|
|
(write-line this)
|
|
(if-let [buf (getf :buffer)]
|
|
(do
|
|
(write-tokens this buf true)
|
|
(setf :buffer []))))
|
|
|
|
(defn- write-white-space [this]
|
|
(when-let [tws (getf :trailing-white-space)]
|
|
(-write (getf :base) tws)
|
|
(setf :trailing-white-space nil)))
|
|
|
|
;;; If there are newlines in the string, print the lines up until the last newline,
|
|
;;; making the appropriate adjustments. Return the remainder of the string
|
|
(defn- write-initial-lines
|
|
[^Writer this ^String s]
|
|
(let [lines (string/split s "\n" -1)]
|
|
(if (= (count lines) 1)
|
|
s
|
|
(let [^String prefix (:per-line-prefix (first (getf :logical-blocks)))
|
|
^String l (first lines)]
|
|
(if (= :buffering (getf :mode))
|
|
(let [oldpos (getf :pos)
|
|
newpos (+ oldpos (count l))]
|
|
(setf :pos newpos)
|
|
(add-to-buffer this (make-buffer-blob l nil oldpos newpos))
|
|
(write-buffered-output this))
|
|
(do
|
|
(write-white-space this)
|
|
(-write (getf :base) l)))
|
|
(-write (getf :base) \newline)
|
|
(doseq [^String l (next (butlast lines))]
|
|
(-write (getf :base) l)
|
|
(-write (getf :base) (pp-newline))
|
|
(if prefix
|
|
(-write (getf :base) prefix)))
|
|
(setf :buffering :writing)
|
|
(last lines)))))
|
|
|
|
(defn- p-write-char [this c]
|
|
(if (= (getf :mode) :writing)
|
|
(do
|
|
(write-white-space this)
|
|
(-write (getf :base) c))
|
|
(if (= c \newline)
|
|
(write-initial-lines this \newline)
|
|
(let [oldpos (getf :pos)
|
|
newpos (inc oldpos)]
|
|
(setf :pos newpos)
|
|
(add-to-buffer this (make-buffer-blob (char c) nil oldpos newpos))))))
|
|
|
|
;;======================================================================
|
|
;; Initialize the pretty-writer instance
|
|
;;======================================================================
|
|
|
|
(defn- pretty-writer [writer max-columns miser-width]
|
|
(let [lb (logical-block. nil nil (atom 0) (atom 0) (atom false) (atom false)
|
|
nil nil nil nil)
|
|
; NOTE: may want to just `specify!` #js { ... fields ... } with the protocols
|
|
fields (atom {:pretty-writer true
|
|
:base (column-writer writer max-columns)
|
|
:logical-blocks lb
|
|
:sections nil
|
|
:mode :writing
|
|
:buffer []
|
|
:buffer-block lb
|
|
:buffer-level 1
|
|
:miser-width miser-width
|
|
:trailing-white-space nil
|
|
:pos 0})]
|
|
(reify
|
|
|
|
IDeref
|
|
(-deref [_] fields)
|
|
|
|
IWriter
|
|
(-write [this x]
|
|
(condp = (type x)
|
|
js/String
|
|
(let [s0 (write-initial-lines this x)
|
|
s (string/replace-first s0 #"\s+$" "")
|
|
white-space (subs s0 (count s))
|
|
mode (getf :mode)]
|
|
(if (= mode :writing)
|
|
(do
|
|
(write-white-space this)
|
|
(-write (getf :base) s)
|
|
(setf :trailing-white-space white-space))
|
|
(let [oldpos (getf :pos)
|
|
newpos (+ oldpos (count s0))]
|
|
(setf :pos newpos)
|
|
(add-to-buffer this (make-buffer-blob s white-space oldpos newpos)))))
|
|
js/Number
|
|
(p-write-char this x)))
|
|
(-flush [this]
|
|
(-ppflush this)
|
|
(-flush (getf :base)))
|
|
|
|
IPrettyFlush
|
|
(-ppflush [this]
|
|
(if (= (getf :mode) :buffering)
|
|
(do
|
|
(write-tokens this (getf :buffer) true)
|
|
(setf :buffer []))
|
|
(write-white-space this)))
|
|
|
|
)))
|
|
|
|
;;======================================================================
|
|
;; Methods for pretty-writer
|
|
;;======================================================================
|
|
|
|
(defn- start-block
|
|
[this prefix per-line-prefix suffix]
|
|
(let [lb (logical-block. (getf :logical-blocks) nil (atom 0) (atom 0)
|
|
(atom false) (atom false)
|
|
prefix per-line-prefix suffix nil)]
|
|
(setf :logical-blocks lb)
|
|
(if (= (getf :mode) :writing)
|
|
(do
|
|
(write-white-space this)
|
|
(when-let [cb (getf :logical-block-callback)] (cb :start))
|
|
(if prefix
|
|
(-write (getf :base) prefix))
|
|
(let [col (get-column (getf :base))]
|
|
(reset! (:start-col lb) col)
|
|
(reset! (:indent lb) col)))
|
|
(let [oldpos (getf :pos)
|
|
newpos (+ oldpos (if prefix (count prefix) 0))]
|
|
(setf :pos newpos)
|
|
(add-to-buffer this (make-start-block-t lb oldpos newpos))))))
|
|
|
|
(defn- end-block [this]
|
|
(let [lb (getf :logical-blocks)
|
|
suffix (:suffix lb)]
|
|
(if (= (getf :mode) :writing)
|
|
(do
|
|
(write-white-space this)
|
|
(if suffix
|
|
(-write (getf :base) suffix))
|
|
(when-let [cb (getf :logical-block-callback)] (cb :end)))
|
|
(let [oldpos (getf :pos)
|
|
newpos (+ oldpos (if suffix (count suffix) 0))]
|
|
(setf :pos newpos)
|
|
(add-to-buffer this (make-end-block-t lb oldpos newpos))))
|
|
(setf :logical-blocks (:parent lb))))
|
|
|
|
(defn- nl [this type]
|
|
(setf :mode :buffering)
|
|
(let [pos (getf :pos)]
|
|
(add-to-buffer this (make-nl-t type (getf :logical-blocks) pos pos))))
|
|
|
|
(defn- indent [this relative-to offset]
|
|
(let [lb (getf :logical-blocks)]
|
|
(if (= (getf :mode) :writing)
|
|
(do
|
|
(write-white-space this)
|
|
(reset! (:indent lb)
|
|
(+ offset (condp = relative-to
|
|
:block @(:start-col lb)
|
|
:current (get-column (getf :base))))))
|
|
(let [pos (getf :pos)]
|
|
(add-to-buffer this (make-indent-t lb relative-to offset pos pos))))))
|
|
|
|
(defn- get-miser-width [this]
|
|
(getf :miser-width))
|
|
|
|
;;======================================================================
|
|
;; pprint_base.clj
|
|
;;======================================================================
|
|
|
|
;;======================================================================
|
|
;; Variables that control the pretty printer
|
|
;;======================================================================
|
|
|
|
;; *print-length*, *print-level*, *print-namespace-maps* and *print-dup* are defined in cljs.core
|
|
(def ^:dynamic
|
|
^{:doc "Bind to true if you want write to use pretty printing"}
|
|
*print-pretty* true)
|
|
|
|
(defonce ^:dynamic
|
|
^{:doc "The pretty print dispatch function. Use with-pprint-dispatch or
|
|
set-pprint-dispatch to modify."
|
|
:added "1.2"}
|
|
*print-pprint-dispatch* nil)
|
|
|
|
(def ^:dynamic
|
|
^{:doc "Pretty printing will try to avoid anything going beyond this column.
|
|
Set it to nil to have pprint let the line be arbitrarily long. This will ignore all
|
|
non-mandatory newlines.",
|
|
:added "1.2"}
|
|
*print-right-margin* 72)
|
|
|
|
(def ^:dynamic
|
|
^{:doc "The column at which to enter miser style. Depending on the dispatch table,
|
|
miser style add newlines in more places to try to keep lines short allowing for further
|
|
levels of nesting.",
|
|
:added "1.2"}
|
|
*print-miser-width* 40)
|
|
|
|
;;; TODO implement output limiting
|
|
(def ^:dynamic
|
|
^{:private true,
|
|
:doc "Maximum number of lines to print in a pretty print instance (N.B. This is not yet used)"}
|
|
*print-lines* nil)
|
|
|
|
;;; TODO: implement circle and shared
|
|
(def ^:dynamic
|
|
^{:private true,
|
|
:doc "Mark circular structures (N.B. This is not yet used)"}
|
|
*print-circle* nil)
|
|
|
|
;;; TODO: should we just use *print-dup* here?
|
|
(def ^:dynamic
|
|
^{:private true,
|
|
:doc "Mark repeated structures rather than repeat them (N.B. This is not yet used)"}
|
|
*print-shared* nil)
|
|
|
|
(def ^:dynamic
|
|
^{:doc "Don't print namespaces with symbols. This is particularly useful when
|
|
pretty printing the results of macro expansions"
|
|
:added "1.2"}
|
|
*print-suppress-namespaces* nil)
|
|
|
|
;;; TODO: support print-base and print-radix in cl-format
|
|
;;; TODO: support print-base and print-radix in rationals
|
|
(def ^:dynamic
|
|
^{:doc "Print a radix specifier in front of integers and rationals. If *print-base* is 2, 8,
|
|
or 16, then the radix specifier used is #b, #o, or #x, respectively. Otherwise the
|
|
radix specifier is in the form #XXr where XX is the decimal value of *print-base* "
|
|
:added "1.2"}
|
|
*print-radix* nil)
|
|
|
|
(def ^:dynamic
|
|
^{:doc "The base to use for printing integers and rationals."
|
|
:added "1.2"}
|
|
*print-base* 10)
|
|
|
|
;;======================================================================
|
|
;; Internal variables that keep track of where we are in the
|
|
;; structure
|
|
;;======================================================================
|
|
|
|
(def ^:dynamic ^{:private true} *current-level* 0)
|
|
|
|
(def ^:dynamic ^{:private true} *current-length* nil)
|
|
|
|
;;======================================================================
|
|
;; Support for the write function
|
|
;;======================================================================
|
|
|
|
(declare ^{:arglists '([n])} format-simple-number)
|
|
|
|
;; This map causes var metadata to be included in the compiled output, even
|
|
;; in advanced compilation. See CLJS-1853 - António Monteiro
|
|
;; (def ^{:private true} write-option-table
|
|
;; {;:array *print-array*
|
|
;; :base #'cljs.pprint/*print-base*,
|
|
;; ;;:case *print-case*,
|
|
;; :circle #'cljs.pprint/*print-circle*,
|
|
;; ;;:escape *print-escape*,
|
|
;; ;;:gensym *print-gensym*,
|
|
;; :length #'cljs.core/*print-length*,
|
|
;; :level #'cljs.core/*print-level*,
|
|
;; :lines #'cljs.pprint/*print-lines*,
|
|
;; :miser-width #'cljs.pprint/*print-miser-width*,
|
|
;; :dispatch #'cljs.pprint/*print-pprint-dispatch*,
|
|
;; :pretty #'cljs.pprint/*print-pretty*,
|
|
;; :radix #'cljs.pprint/*print-radix*,
|
|
;; :readably #'cljs.core/*print-readably*,
|
|
;; :right-margin #'cljs.pprint/*print-right-margin*,
|
|
;; :suppress-namespaces #'cljs.pprint/*print-suppress-namespaces*})
|
|
|
|
(defn- table-ize [t m]
|
|
(apply hash-map (mapcat
|
|
#(when-let [v (get t (key %))] [v (val %)])
|
|
m)))
|
|
|
|
(defn- pretty-writer?
|
|
"Return true iff x is a PrettyWriter"
|
|
[x] (and (satisfies? IDeref x) (:pretty-writer @@x)))
|
|
|
|
(defn- make-pretty-writer
|
|
"Wrap base-writer in a PrettyWriter with the specified right-margin and miser-width"
|
|
[base-writer right-margin miser-width]
|
|
(pretty-writer base-writer right-margin miser-width))
|
|
|
|
(defn write-out
|
|
"Write an object to *out* subject to the current bindings of the printer control
|
|
variables. Use the kw-args argument to override individual variables for this call (and
|
|
any recursive calls).
|
|
|
|
*out* must be a PrettyWriter if pretty printing is enabled. This is the responsibility
|
|
of the caller.
|
|
|
|
This method is primarily intended for use by pretty print dispatch functions that
|
|
already know that the pretty printer will have set up their environment appropriately.
|
|
Normal library clients should use the standard \"write\" interface. "
|
|
[object]
|
|
(let [length-reached (and *current-length*
|
|
*print-length*
|
|
(>= *current-length* *print-length*))]
|
|
(if-not *print-pretty*
|
|
(pr object)
|
|
(if length-reached
|
|
(-write *out* "...") ;;TODO could this (incorrectly) print ... on the next line?
|
|
(do
|
|
(if *current-length* (set! *current-length* (inc *current-length*)))
|
|
(*print-pprint-dispatch* object))))
|
|
length-reached))
|
|
|
|
(defn write
|
|
"Write an object subject to the current bindings of the printer control variables.
|
|
Use the kw-args argument to override individual variables for this call (and any
|
|
recursive calls). Returns the string result if :stream is nil or nil otherwise.
|
|
|
|
The following keyword arguments can be passed with values:
|
|
Keyword Meaning Default value
|
|
:stream Writer for output or nil true (indicates *out*)
|
|
:base Base to use for writing rationals Current value of *print-base*
|
|
:circle* If true, mark circular structures Current value of *print-circle*
|
|
:length Maximum elements to show in sublists Current value of *print-length*
|
|
:level Maximum depth Current value of *print-level*
|
|
:lines* Maximum lines of output Current value of *print-lines*
|
|
:miser-width Width to enter miser mode Current value of *print-miser-width*
|
|
:dispatch The pretty print dispatch function Current value of *print-pprint-dispatch*
|
|
:pretty If true, do pretty printing Current value of *print-pretty*
|
|
:radix If true, prepend a radix specifier Current value of *print-radix*
|
|
:readably* If true, print readably Current value of *print-readably*
|
|
:right-margin The column for the right margin Current value of *print-right-margin*
|
|
:suppress-namespaces If true, no namespaces in symbols Current value of *print-suppress-namespaces*
|
|
|
|
* = not yet supported
|
|
"
|
|
[object & kw-args]
|
|
(let [options (merge {:stream true} (apply hash-map kw-args))]
|
|
;;TODO rewrite this as a macro
|
|
(binding [cljs.pprint/*print-base* (:base options cljs.pprint/*print-base*)
|
|
;;:case *print-case*,
|
|
cljs.pprint/*print-circle* (:circle options cljs.pprint/*print-circle*)
|
|
;;:escape *print-escape*
|
|
;;:gensym *print-gensym*
|
|
cljs.core/*print-length* (:length options cljs.core/*print-length*)
|
|
cljs.core/*print-level* (:level options cljs.core/*print-level*)
|
|
cljs.pprint/*print-lines* (:lines options cljs.pprint/*print-lines*)
|
|
cljs.pprint/*print-miser-width* (:miser-width options cljs.pprint/*print-miser-width*)
|
|
cljs.pprint/*print-pprint-dispatch* (:dispatch options cljs.pprint/*print-pprint-dispatch*)
|
|
cljs.pprint/*print-pretty* (:pretty options cljs.pprint/*print-pretty*)
|
|
cljs.pprint/*print-radix* (:radix options cljs.pprint/*print-radix*)
|
|
cljs.core/*print-readably* (:readably options cljs.core/*print-readably*)
|
|
cljs.pprint/*print-right-margin* (:right-margin options cljs.pprint/*print-right-margin*)
|
|
cljs.pprint/*print-suppress-namespaces* (:suppress-namespaces options cljs.pprint/*print-suppress-namespaces*)]
|
|
;;TODO enable printing base
|
|
#_[bindings (if (or (not (= *print-base* 10)) *print-radix*)
|
|
{#'pr pr-with-base}
|
|
{})]
|
|
(binding []
|
|
(let [sb (StringBuffer.)
|
|
optval (if (contains? options :stream)
|
|
(:stream options)
|
|
true)
|
|
base-writer (if (or (true? optval) (nil? optval))
|
|
(StringBufferWriter. sb)
|
|
optval)]
|
|
(if *print-pretty*
|
|
(with-pretty-writer base-writer
|
|
(write-out object))
|
|
(binding [*out* base-writer]
|
|
(pr object)))
|
|
(if (true? optval)
|
|
(string-print (str sb)))
|
|
(if (nil? optval)
|
|
(str sb)))))))
|
|
|
|
(defn pprint
|
|
([object]
|
|
(let [sb (StringBuffer.)]
|
|
(binding [*out* (StringBufferWriter. sb)]
|
|
(pprint object *out*)
|
|
(string-print (str sb)))))
|
|
([object writer]
|
|
(with-pretty-writer writer
|
|
(binding [*print-pretty* true]
|
|
(write-out object))
|
|
(if (not (= 0 (get-column *out*)))
|
|
(-write *out* \newline)))))
|
|
|
|
(defn set-pprint-dispatch
|
|
[function]
|
|
(set! *print-pprint-dispatch* function)
|
|
nil)
|
|
|
|
;;======================================================================
|
|
;; Support for the functional interface to the pretty printer
|
|
;;======================================================================
|
|
|
|
(defn- check-enumerated-arg [arg choices]
|
|
(if-not (choices arg)
|
|
;; TODO clean up choices string
|
|
(throw (js/Error. (str "Bad argument: " arg ". It must be one of " choices)))))
|
|
|
|
(defn- level-exceeded []
|
|
(and *print-level* (>= *current-level* *print-level*)))
|
|
|
|
(defn pprint-newline
|
|
"Print a conditional newline to a pretty printing stream. kind specifies if the
|
|
newline is :linear, :miser, :fill, or :mandatory.
|
|
|
|
This function is intended for use when writing custom dispatch functions.
|
|
|
|
Output is sent to *out* which must be a pretty printing writer."
|
|
[kind]
|
|
(check-enumerated-arg kind #{:linear :miser :fill :mandatory})
|
|
(nl *out* kind))
|
|
|
|
(defn pprint-indent
|
|
"Create an indent at this point in the pretty printing stream. This defines how
|
|
following lines are indented. relative-to can be either :block or :current depending
|
|
whether the indent should be computed relative to the start of the logical block or
|
|
the current column position. n is an offset.
|
|
|
|
This function is intended for use when writing custom dispatch functions.
|
|
|
|
Output is sent to *out* which must be a pretty printing writer."
|
|
[relative-to n]
|
|
(check-enumerated-arg relative-to #{:block :current})
|
|
(indent *out* relative-to n))
|
|
|
|
;; TODO a real implementation for pprint-tab
|
|
(defn pprint-tab
|
|
"Tab at this point in the pretty printing stream. kind specifies whether the tab
|
|
is :line, :section, :line-relative, or :section-relative.
|
|
|
|
Colnum and colinc specify the target column and the increment to move the target
|
|
forward if the output is already past the original target.
|
|
|
|
This function is intended for use when writing custom dispatch functions.
|
|
|
|
Output is sent to *out* which must be a pretty printing writer.
|
|
|
|
THIS FUNCTION IS NOT YET IMPLEMENTED."
|
|
{:added "1.2"}
|
|
[kind colnum colinc]
|
|
(check-enumerated-arg kind #{:line :section :line-relative :section-relative})
|
|
(throw (js/Error. "pprint-tab is not yet implemented")))
|
|
|
|
;;======================================================================
|
|
;; cl_format.clj
|
|
;;======================================================================
|
|
|
|
;; Forward references
|
|
(declare ^{:arglists '([format-str])} compile-format)
|
|
(declare ^{:arglists '([stream format args] [format args])} execute-format)
|
|
(declare ^{:arglists '([s])} init-navigator)
|
|
;; End forward references
|
|
|
|
(defn cl-format
|
|
"An implementation of a Common Lisp compatible format function. cl-format formats its
|
|
arguments to an output stream or string based on the format control string given. It
|
|
supports sophisticated formatting of structured data.
|
|
|
|
Writer satisfies IWriter, true to output via *print-fn* or nil to output
|
|
to a string, format-in is the format control string and the remaining arguments
|
|
are the data to be formatted.
|
|
|
|
The format control string is a string to be output with embedded 'format directives'
|
|
describing how to format the various arguments passed in.
|
|
|
|
If writer is nil, cl-format returns the formatted result string. Otherwise, cl-format
|
|
returns nil.
|
|
|
|
For example:
|
|
(let [results [46 38 22]]
|
|
(cl-format true \"There ~[are~;is~:;are~]~:* ~d result~:p: ~{~d~^, ~}~%\"
|
|
(count results) results))
|
|
|
|
Prints via *print-fn*:
|
|
There are 3 results: 46, 38, 22
|
|
|
|
Detailed documentation on format control strings is available in the \"Common Lisp the
|
|
Language, 2nd edition\", Chapter 22 (available online at:
|
|
http://www.cs.cmu.edu/afs/cs.cmu.edu/project/ai-repository/ai/html/cltl/clm/node200.html#SECTION002633000000000000000)
|
|
and in the Common Lisp HyperSpec at
|
|
http://www.lispworks.com/documentation/HyperSpec/Body/22_c.htm"
|
|
{:see-also [["http://www.cs.cmu.edu/afs/cs.cmu.edu/project/ai-repository/ai/html/cltl/clm/node200.html#SECTION002633000000000000000"
|
|
"Common Lisp the Language"]
|
|
["http://www.lispworks.com/documentation/HyperSpec/Body/22_c.htm"
|
|
"Common Lisp HyperSpec"]]}
|
|
[writer format-in & args]
|
|
(let [compiled-format (if (string? format-in) (compile-format format-in) format-in)
|
|
navigator (init-navigator args)]
|
|
(execute-format writer compiled-format navigator)))
|
|
|
|
(def ^:dynamic ^{:private true} *format-str* nil)
|
|
|
|
(defn- format-error [message offset]
|
|
(let [full-message (str message \newline *format-str* \newline
|
|
(apply str (repeat offset \space)) "^" \newline)]
|
|
(throw (js/Error full-message))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Argument navigators manage the argument list
|
|
;; as the format statement moves through the list
|
|
;; (possibly going forwards and backwards as it does so)
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defrecord ^{:private true}
|
|
arg-navigator [seq rest pos])
|
|
|
|
(defn- init-navigator
|
|
"Create a new arg-navigator from the sequence with the position set to 0"
|
|
{:skip-wiki true}
|
|
[s]
|
|
(let [s (seq s)]
|
|
(arg-navigator. s s 0)))
|
|
|
|
;; TODO call format-error with offset
|
|
(defn- next-arg [navigator]
|
|
(let [rst (:rest navigator)]
|
|
(if rst
|
|
[(first rst) (arg-navigator. (:seq navigator) (next rst) (inc (:pos navigator)))]
|
|
(throw (js/Error "Not enough arguments for format definition")))))
|
|
|
|
(defn- next-arg-or-nil [navigator]
|
|
(let [rst (:rest navigator)]
|
|
(if rst
|
|
[(first rst) (arg-navigator. (:seq navigator) (next rst) (inc (:pos navigator)))]
|
|
[nil navigator])))
|
|
|
|
;; Get an argument off the arg list and compile it if it's not already compiled
|
|
(defn- get-format-arg [navigator]
|
|
(let [[raw-format navigator] (next-arg navigator)
|
|
compiled-format (if (string? raw-format)
|
|
(compile-format raw-format)
|
|
raw-format)]
|
|
[compiled-format navigator]))
|
|
|
|
(declare relative-reposition)
|
|
|
|
(defn- absolute-reposition [navigator position]
|
|
(if (>= position (:pos navigator))
|
|
(relative-reposition navigator (- (:pos navigator) position))
|
|
(arg-navigator. (:seq navigator) (drop position (:seq navigator)) position)))
|
|
|
|
(defn- relative-reposition [navigator position]
|
|
(let [newpos (+ (:pos navigator) position)]
|
|
(if (neg? position)
|
|
(absolute-reposition navigator newpos)
|
|
(arg-navigator. (:seq navigator) (drop position (:rest navigator)) newpos))))
|
|
|
|
(defrecord ^{:private true}
|
|
compiled-directive [func def params offset])
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; When looking at the parameter list, we may need to manipulate
|
|
;; the argument list as well (for 'V' and '#' parameter types).
|
|
;; We hide all of this behind a function, but clients need to
|
|
;; manage changing arg navigator
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; TODO: validate parameters when they come from arg list
|
|
(defn- realize-parameter [[param [raw-val offset]] navigator]
|
|
(let [[real-param new-navigator]
|
|
(cond
|
|
(contains? #{:at :colon} param) ;pass flags through unchanged - this really isn't necessary
|
|
[raw-val navigator]
|
|
|
|
(= raw-val :parameter-from-args)
|
|
(next-arg navigator)
|
|
|
|
(= raw-val :remaining-arg-count)
|
|
[(count (:rest navigator)) navigator]
|
|
|
|
true
|
|
[raw-val navigator])]
|
|
[[param [real-param offset]] new-navigator]))
|
|
|
|
(defn- realize-parameter-list [parameter-map navigator]
|
|
(let [[pairs new-navigator]
|
|
(map-passing-context realize-parameter navigator parameter-map)]
|
|
[(into {} pairs) new-navigator]))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Functions that support individual directives
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Common handling code for ~A and ~S
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(declare ^{:arglists '([base val])} opt-base-str)
|
|
|
|
(def ^{:private true}
|
|
special-radix-markers {2 "#b" 8 "#o" 16 "#x"})
|
|
|
|
(defn- format-simple-number [n]
|
|
(cond
|
|
(integer? n) (if (= *print-base* 10)
|
|
(str n (if *print-radix* "."))
|
|
(str
|
|
(if *print-radix* (or (get special-radix-markers *print-base*) (str "#" *print-base* "r")))
|
|
(opt-base-str *print-base* n)))
|
|
;;(ratio? n) ;;no ratio support
|
|
:else nil))
|
|
|
|
(defn- format-ascii [print-func params arg-navigator offsets]
|
|
(let [[arg arg-navigator] (next-arg arg-navigator)
|
|
base-output (or (format-simple-number arg) (print-func arg))
|
|
base-width (.-length base-output)
|
|
min-width (+ base-width (:minpad params))
|
|
width (if (>= min-width (:mincol params))
|
|
min-width
|
|
(+ min-width
|
|
(* (+ (quot (- (:mincol params) min-width 1)
|
|
(:colinc params))
|
|
1)
|
|
(:colinc params))))
|
|
chars (apply str (repeat (- width base-width) (:padchar params)))]
|
|
(if (:at params)
|
|
(print (str chars base-output))
|
|
(print (str base-output chars)))
|
|
arg-navigator))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Support for the integer directives ~D, ~X, ~O, ~B and some
|
|
;; of ~R
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- integral?
|
|
"returns true if a number is actually an integer (that is, has no fractional part)"
|
|
[x]
|
|
(cond
|
|
(integer? x) true
|
|
;;(decimal? x) ;;no decimal support
|
|
(float? x) (= x (Math/floor x))
|
|
;;(ratio? x) ;;no ratio support
|
|
:else false))
|
|
|
|
(defn- remainders
|
|
"Return the list of remainders (essentially the 'digits') of val in the given base"
|
|
[base val]
|
|
(reverse
|
|
(first
|
|
(consume #(if (pos? %)
|
|
[(rem % base) (quot % base)]
|
|
[nil nil])
|
|
val))))
|
|
|
|
;; TODO: xlated-val does not seem to be used here.
|
|
;; NB
|
|
(defn- base-str
|
|
"Return val as a string in the given base"
|
|
[base val]
|
|
(if (zero? val)
|
|
"0"
|
|
(let [xlated-val (cond
|
|
;(float? val) (bigdec val) ;;No bigdec
|
|
;(ratio? val) nil ;;No ratio
|
|
:else val)]
|
|
(apply str
|
|
(map
|
|
#(if (< % 10) (char (+ (char-code \0) %)) (char (+ (char-code \a) (- % 10))))
|
|
(remainders base val))))))
|
|
|
|
;;Not sure if this is accurate or necessary
|
|
(def ^{:private true}
|
|
javascript-base-formats {8 "%o", 10 "%d", 16 "%x"})
|
|
|
|
(defn- opt-base-str
|
|
"Return val as a string in the given base. No cljs format, so no improved performance."
|
|
[base val]
|
|
(base-str base val))
|
|
|
|
(defn- group-by* [unit lis]
|
|
(reverse
|
|
(first
|
|
(consume (fn [x] [(seq (reverse (take unit x))) (seq (drop unit x))]) (reverse lis)))))
|
|
|
|
(defn- format-integer [base params arg-navigator offsets]
|
|
(let [[arg arg-navigator] (next-arg arg-navigator)]
|
|
(if (integral? arg)
|
|
(let [neg (neg? arg)
|
|
pos-arg (if neg (- arg) arg)
|
|
raw-str (opt-base-str base pos-arg)
|
|
group-str (if (:colon params)
|
|
(let [groups (map #(apply str %) (group-by* (:commainterval params) raw-str))
|
|
commas (repeat (count groups) (:commachar params))]
|
|
(apply str (next (interleave commas groups))))
|
|
raw-str)
|
|
signed-str (cond
|
|
neg (str "-" group-str)
|
|
(:at params) (str "+" group-str)
|
|
true group-str)
|
|
padded-str (if (< (.-length signed-str) (:mincol params))
|
|
(str (apply str (repeat (- (:mincol params) (.-length signed-str))
|
|
(:padchar params)))
|
|
signed-str)
|
|
signed-str)]
|
|
(print padded-str))
|
|
(format-ascii print-str {:mincol (:mincol params) :colinc 1 :minpad 0
|
|
:padchar (:padchar params) :at true}
|
|
(init-navigator [arg]) nil))
|
|
arg-navigator))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Support for english formats (~R and ~:R)
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^{:private true}
|
|
english-cardinal-units
|
|
["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"
|
|
"ten" "eleven" "twelve" "thirteen" "fourteen"
|
|
"fifteen" "sixteen" "seventeen" "eighteen" "nineteen"])
|
|
|
|
(def ^{:private true}
|
|
english-ordinal-units
|
|
["zeroth" "first" "second" "third" "fourth" "fifth" "sixth" "seventh" "eighth" "ninth"
|
|
"tenth" "eleventh" "twelfth" "thirteenth" "fourteenth"
|
|
"fifteenth" "sixteenth" "seventeenth" "eighteenth" "nineteenth"])
|
|
|
|
(def ^{:private true}
|
|
english-cardinal-tens
|
|
["" "" "twenty" "thirty" "forty" "fifty" "sixty" "seventy" "eighty" "ninety"])
|
|
|
|
(def ^{:private true}
|
|
english-ordinal-tens
|
|
["" "" "twentieth" "thirtieth" "fortieth" "fiftieth"
|
|
"sixtieth" "seventieth" "eightieth" "ninetieth"])
|
|
|
|
;; We use "short scale" for our units (see http://en.wikipedia.org/wiki/Long_and_short_scales)
|
|
;; Number names from http://www.jimloy.com/math/billion.htm
|
|
;; We follow the rules for writing numbers from the Blue Book
|
|
;; (http://www.grammarbook.com/numbers/numbers.asp)
|
|
(def ^{:private true}
|
|
english-scale-numbers
|
|
["" "thousand" "million" "billion" "trillion" "quadrillion" "quintillion"
|
|
"sextillion" "septillion" "octillion" "nonillion" "decillion"
|
|
"undecillion" "duodecillion" "tredecillion" "quattuordecillion"
|
|
"quindecillion" "sexdecillion" "septendecillion"
|
|
"octodecillion" "novemdecillion" "vigintillion"])
|
|
|
|
(defn- format-simple-cardinal
|
|
"Convert a number less than 1000 to a cardinal english string"
|
|
[num]
|
|
(let [hundreds (quot num 100)
|
|
tens (rem num 100)]
|
|
(str
|
|
(if (pos? hundreds) (str (nth english-cardinal-units hundreds) " hundred"))
|
|
(if (and (pos? hundreds) (pos? tens)) " ")
|
|
(if (pos? tens)
|
|
(if (< tens 20)
|
|
(nth english-cardinal-units tens)
|
|
(let [ten-digit (quot tens 10)
|
|
unit-digit (rem tens 10)]
|
|
(str
|
|
(if (pos? ten-digit) (nth english-cardinal-tens ten-digit))
|
|
(if (and (pos? ten-digit) (pos? unit-digit)) "-")
|
|
(if (pos? unit-digit) (nth english-cardinal-units unit-digit)))))))))
|
|
|
|
(defn- add-english-scales
|
|
"Take a sequence of parts, add scale numbers (e.g., million) and combine into a string
|
|
offset is a factor of 10^3 to multiply by"
|
|
[parts offset]
|
|
(let [cnt (count parts)]
|
|
(loop [acc []
|
|
pos (dec cnt)
|
|
this (first parts)
|
|
remainder (next parts)]
|
|
(if (nil? remainder)
|
|
(str (apply str (interpose ", " acc))
|
|
(if (and (not (empty? this)) (not (empty? acc))) ", ")
|
|
this
|
|
(if (and (not (empty? this)) (pos? (+ pos offset)))
|
|
(str " " (nth english-scale-numbers (+ pos offset)))))
|
|
(recur
|
|
(if (empty? this)
|
|
acc
|
|
(conj acc (str this " " (nth english-scale-numbers (+ pos offset)))))
|
|
(dec pos)
|
|
(first remainder)
|
|
(next remainder))))))
|
|
|
|
(defn- format-cardinal-english [params navigator offsets]
|
|
(let [[arg navigator] (next-arg navigator)]
|
|
(if (= 0 arg)
|
|
(print "zero")
|
|
(let [abs-arg (if (neg? arg) (- arg) arg) ; some numbers are too big for Math/abs (is this true?)
|
|
parts (remainders 1000 abs-arg)]
|
|
(if (<= (count parts) (count english-scale-numbers))
|
|
(let [parts-strs (map format-simple-cardinal parts)
|
|
full-str (add-english-scales parts-strs 0)]
|
|
(print (str (if (neg? arg) "minus ") full-str)))
|
|
(format-integer ;; for numbers > 10^63, we fall back on ~D
|
|
10
|
|
{:mincol 0, :padchar \space, :commachar \, :commainterval 3, :colon true}
|
|
(init-navigator [arg])
|
|
{:mincol 0, :padchar 0, :commachar 0 :commainterval 0}))))
|
|
navigator))
|
|
|
|
(defn- format-simple-ordinal
|
|
"Convert a number less than 1000 to a ordinal english string
|
|
Note this should only be used for the last one in the sequence"
|
|
[num]
|
|
(let [hundreds (quot num 100)
|
|
tens (rem num 100)]
|
|
(str
|
|
(if (pos? hundreds) (str (nth english-cardinal-units hundreds) " hundred"))
|
|
(if (and (pos? hundreds) (pos? tens)) " ")
|
|
(if (pos? tens)
|
|
(if (< tens 20)
|
|
(nth english-ordinal-units tens)
|
|
(let [ten-digit (quot tens 10)
|
|
unit-digit (rem tens 10)]
|
|
(if (and (pos? ten-digit) (not (pos? unit-digit)))
|
|
(nth english-ordinal-tens ten-digit)
|
|
(str
|
|
(if (pos? ten-digit) (nth english-cardinal-tens ten-digit))
|
|
(if (and (pos? ten-digit) (pos? unit-digit)) "-")
|
|
(if (pos? unit-digit) (nth english-ordinal-units unit-digit))))))
|
|
(if (pos? hundreds) "th")))))
|
|
|
|
(defn- format-ordinal-english [params navigator offsets]
|
|
(let [[arg navigator] (next-arg navigator)]
|
|
(if (= 0 arg)
|
|
(print "zeroth")
|
|
(let [abs-arg (if (neg? arg) (- arg) arg) ; some numbers are too big for Math/abs (is this true?)
|
|
parts (remainders 1000 abs-arg)]
|
|
(if (<= (count parts) (count english-scale-numbers))
|
|
(let [parts-strs (map format-simple-cardinal (drop-last parts))
|
|
head-str (add-english-scales parts-strs 1)
|
|
tail-str (format-simple-ordinal (last parts))]
|
|
(print (str (if (neg? arg) "minus ")
|
|
(cond
|
|
(and (not (empty? head-str)) (not (empty? tail-str)))
|
|
(str head-str ", " tail-str)
|
|
|
|
(not (empty? head-str)) (str head-str "th")
|
|
:else tail-str))))
|
|
(do (format-integer ;for numbers > 10^63, we fall back on ~D
|
|
10
|
|
{:mincol 0, :padchar \space, :commachar \, :commainterval 3, :colon true}
|
|
(init-navigator [arg])
|
|
{:mincol 0, :padchar 0, :commachar 0 :commainterval 0})
|
|
(let [low-two-digits (rem arg 100)
|
|
not-teens (or (< 11 low-two-digits) (> 19 low-two-digits))
|
|
low-digit (rem low-two-digits 10)]
|
|
(print (cond
|
|
(and (== low-digit 1) not-teens) "st"
|
|
(and (== low-digit 2) not-teens) "nd"
|
|
(and (== low-digit 3) not-teens) "rd"
|
|
:else "th")))))))
|
|
navigator))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Support for roman numeral formats (~@R and ~@:R)
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^{:private true}
|
|
old-roman-table
|
|
[[ "I" "II" "III" "IIII" "V" "VI" "VII" "VIII" "VIIII"]
|
|
[ "X" "XX" "XXX" "XXXX" "L" "LX" "LXX" "LXXX" "LXXXX"]
|
|
[ "C" "CC" "CCC" "CCCC" "D" "DC" "DCC" "DCCC" "DCCCC"]
|
|
[ "M" "MM" "MMM"]])
|
|
|
|
(def ^{:private true}
|
|
new-roman-table
|
|
[[ "I" "II" "III" "IV" "V" "VI" "VII" "VIII" "IX"]
|
|
[ "X" "XX" "XXX" "XL" "L" "LX" "LXX" "LXXX" "XC"]
|
|
[ "C" "CC" "CCC" "CD" "D" "DC" "DCC" "DCCC" "CM"]
|
|
[ "M" "MM" "MMM"]])
|
|
|
|
(defn- format-roman
|
|
"Format a roman numeral using the specified look-up table"
|
|
[table params navigator offsets]
|
|
(let [[arg navigator] (next-arg navigator)]
|
|
(if (and (number? arg) (> arg 0) (< arg 4000))
|
|
(let [digits (remainders 10 arg)]
|
|
(loop [acc []
|
|
pos (dec (count digits))
|
|
digits digits]
|
|
(if (empty? digits)
|
|
(print (apply str acc))
|
|
(let [digit (first digits)]
|
|
(recur (if (= 0 digit)
|
|
acc
|
|
(conj acc (nth (nth table pos) (dec digit))))
|
|
(dec pos)
|
|
(next digits))))))
|
|
(format-integer ; for anything <= 0 or > 3999, we fall back on ~D
|
|
10
|
|
{:mincol 0, :padchar \space, :commachar \, :commainterval 3, :colon true}
|
|
(init-navigator [arg])
|
|
{:mincol 0, :padchar 0, :commachar 0 :commainterval 0}))
|
|
navigator))
|
|
|
|
(defn- format-old-roman [params navigator offsets]
|
|
(format-roman old-roman-table params navigator offsets))
|
|
|
|
(defn- format-new-roman [params navigator offsets]
|
|
(format-roman new-roman-table params navigator offsets))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Support for character formats (~C)
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^{:private true}
|
|
special-chars {8 "Backspace", 9 "Tab", 10 "Newline", 13 "Return", 32 "Space"})
|
|
|
|
(defn- pretty-character [params navigator offsets]
|
|
(let [[c navigator] (next-arg navigator)
|
|
as-int (char-code c)
|
|
base-char (bit-and as-int 127)
|
|
meta (bit-and as-int 128)
|
|
special (get special-chars base-char)]
|
|
(if (> meta 0) (print "Meta-"))
|
|
(print (cond
|
|
special special
|
|
(< base-char 32) (str "Control-" (char (+ base-char 64)))
|
|
(= base-char 127) "Control-?"
|
|
:else (char base-char)))
|
|
navigator))
|
|
|
|
(defn- readable-character [params navigator offsets]
|
|
(let [[c navigator] (next-arg navigator)]
|
|
(condp = (:char-format params)
|
|
\o (cl-format true "\\o~3, '0o" (char-code c))
|
|
\u (cl-format true "\\u~4, '0x" (char-code c))
|
|
nil (print-char c))
|
|
navigator))
|
|
|
|
(defn- plain-character [params navigator offsets]
|
|
(let [[char navigator] (next-arg navigator)]
|
|
(print char)
|
|
navigator))
|
|
|
|
;; Check to see if a result is an abort (~^) construct
|
|
;; TODO: move these funcs somewhere more appropriate
|
|
(defn- abort? [context]
|
|
(let [token (first context)]
|
|
(or (= :up-arrow token) (= :colon-up-arrow token))))
|
|
|
|
;; Handle the execution of "sub-clauses" in bracket constructions
|
|
(defn- execute-sub-format [format args base-args]
|
|
(second
|
|
(map-passing-context
|
|
(fn [element context]
|
|
(if (abort? context)
|
|
[nil context] ; just keep passing it along
|
|
(let [[params args] (realize-parameter-list (:params element) context)
|
|
[params offsets] (unzip-map params)
|
|
params (assoc params :base-args base-args)]
|
|
[nil (apply (:func element) [params args offsets])])))
|
|
args
|
|
format)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Support for real number formats
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; TODO - return exponent as int to eliminate double conversion
|
|
(defn- float-parts-base
|
|
"Produce string parts for the mantissa (normalize 1-9) and exponent"
|
|
[f]
|
|
(let [s (string/lower-case (str f))
|
|
exploc (.indexOf s \e)
|
|
dotloc (.indexOf s \.)]
|
|
(if (neg? exploc)
|
|
(if (neg? dotloc)
|
|
[s (str (dec (count s)))]
|
|
[(str (subs s 0 dotloc) (subs s (inc dotloc))) (str (dec dotloc))])
|
|
(if (neg? dotloc)
|
|
[(subs s 0 exploc) (subs s (inc exploc))]
|
|
[(str (subs s 0 1) (subs s 2 exploc)) (subs s (inc exploc))]))))
|
|
|
|
(defn- float-parts
|
|
"Take care of leading and trailing zeros in decomposed floats"
|
|
[f]
|
|
(let [[m e] (float-parts-base f)
|
|
m1 (rtrim m \0)
|
|
m2 (ltrim m1 \0)
|
|
delta (- (count m1) (count m2))
|
|
e (if (and (pos? (count e)) (= (nth e 0) \+)) (subs e 1) e)]
|
|
(if (empty? m2)
|
|
["0" 0]
|
|
[m2 (- (js/parseInt e 10) delta)])))
|
|
|
|
(defn- inc-s
|
|
"Assumption: The input string consists of one or more decimal digits,
|
|
and no other characters. Return a string containing one or more
|
|
decimal digits containing a decimal number one larger than the input
|
|
string. The output string will always be the same length as the input
|
|
string, or one character longer."
|
|
[s]
|
|
(let [len-1 (dec (count s))]
|
|
(loop [i (int len-1)]
|
|
(cond
|
|
(neg? i) (apply str "1" (repeat (inc len-1) "0"))
|
|
(= \9 (.charAt s i)) (recur (dec i))
|
|
:else (apply str (subs s 0 i)
|
|
(char (inc (char-code (.charAt s i))))
|
|
(repeat (- len-1 i) "0"))))))
|
|
|
|
(defn- round-str [m e d w]
|
|
(if (or d w)
|
|
(let [len (count m)
|
|
;; Every formatted floating point number should include at
|
|
;; least one decimal digit and a decimal point.
|
|
w (if w (max 2 w)
|
|
;;NB: if w doesn't exist, it won't ever be used because d will
|
|
;; satisfy the cond below. cljs gives a compilation warning if
|
|
;; we don't provide a value here.
|
|
0)
|
|
round-pos (cond
|
|
;; If d was given, that forces the rounding
|
|
;; position, regardless of any width that may
|
|
;; have been specified.
|
|
d (+ e d 1)
|
|
;; Otherwise w was specified, so pick round-pos
|
|
;; based upon that.
|
|
;; If e>=0, then abs value of number is >= 1.0,
|
|
;; and e+1 is number of decimal digits before the
|
|
;; decimal point when the number is written
|
|
;; without scientific notation. Never round the
|
|
;; number before the decimal point.
|
|
(>= e 0) (max (inc e) (dec w))
|
|
;; e < 0, so number abs value < 1.0
|
|
:else (+ w e))
|
|
[m1 e1 round-pos len] (if (= round-pos 0)
|
|
[(str "0" m) (inc e) 1 (inc len)]
|
|
[m e round-pos len])]
|
|
(if round-pos
|
|
(if (neg? round-pos)
|
|
["0" 0 false]
|
|
(if (> len round-pos)
|
|
(let [round-char (nth m1 round-pos)
|
|
result (subs m1 0 round-pos)]
|
|
(if (>= (char-code round-char) (char-code \5))
|
|
(let [round-up-result (inc-s result)
|
|
expanded (> (count round-up-result) (count result))]
|
|
[(if expanded
|
|
(subs round-up-result 0 (dec (count round-up-result)))
|
|
round-up-result)
|
|
e1 expanded])
|
|
[result e1 false]))
|
|
[m e false]))
|
|
[m e false]))
|
|
[m e false]))
|
|
|
|
(defn- expand-fixed [m e d]
|
|
(let [[m1 e1] (if (neg? e)
|
|
[(str (apply str (repeat (dec (- e)) \0)) m) -1]
|
|
[m e])
|
|
len (count m1)
|
|
target-len (if d (+ e1 d 1) (inc e1))]
|
|
(if (< len target-len)
|
|
(str m1 (apply str (repeat (- target-len len) \0)))
|
|
m1)))
|
|
|
|
(defn- insert-decimal
|
|
"Insert the decimal point at the right spot in the number to match an exponent"
|
|
[m e]
|
|
(if (neg? e)
|
|
(str "." m)
|
|
(let [loc (inc e)]
|
|
(str (subs m 0 loc) "." (subs m loc)))))
|
|
|
|
(defn- get-fixed [m e d]
|
|
(insert-decimal (expand-fixed m e d) e))
|
|
|
|
(defn- insert-scaled-decimal
|
|
"Insert the decimal point at the right spot in the number to match an exponent"
|
|
[m k]
|
|
(if (neg? k)
|
|
(str "." m)
|
|
(str (subs m 0 k) "." (subs m k))))
|
|
|
|
;;TODO: No ratio, so not sure what to do here
|
|
(defn- convert-ratio [x]
|
|
x)
|
|
|
|
;; the function to render ~F directives
|
|
;; TODO: support rationals. Back off to ~D/~A in the appropriate cases
|
|
(defn- fixed-float [params navigator offsets]
|
|
(let [w (:w params)
|
|
d (:d params)
|
|
[arg navigator] (next-arg navigator)
|
|
[sign abs] (if (neg? arg) ["-" (- arg)] ["+" arg])
|
|
abs (convert-ratio abs)
|
|
[mantissa exp] (float-parts abs)
|
|
scaled-exp (+ exp (:k params))
|
|
add-sign (or (:at params) (neg? arg))
|
|
append-zero (and (not d) (<= (dec (count mantissa)) scaled-exp))
|
|
[rounded-mantissa scaled-exp expanded] (round-str mantissa scaled-exp
|
|
d (if w (- w (if add-sign 1 0))))
|
|
fixed-repr (get-fixed rounded-mantissa (if expanded (inc scaled-exp) scaled-exp) d)
|
|
fixed-repr (if (and w d
|
|
(>= d 1)
|
|
(= (.charAt fixed-repr 0) \0)
|
|
(= (.charAt fixed-repr 1) \.)
|
|
(> (count fixed-repr) (- w (if add-sign 1 0))))
|
|
(subs fixed-repr 1) ;chop off leading 0
|
|
fixed-repr)
|
|
prepend-zero (= (first fixed-repr) \.)]
|
|
(if w
|
|
(let [len (count fixed-repr)
|
|
signed-len (if add-sign (inc len) len)
|
|
prepend-zero (and prepend-zero (not (>= signed-len w)))
|
|
append-zero (and append-zero (not (>= signed-len w)))
|
|
full-len (if (or prepend-zero append-zero)
|
|
(inc signed-len)
|
|
signed-len)]
|
|
(if (and (> full-len w) (:overflowchar params))
|
|
(print (apply str (repeat w (:overflowchar params))))
|
|
(print (str
|
|
(apply str (repeat (- w full-len) (:padchar params)))
|
|
(if add-sign sign)
|
|
(if prepend-zero "0")
|
|
fixed-repr
|
|
(if append-zero "0")))))
|
|
(print (str
|
|
(if add-sign sign)
|
|
(if prepend-zero "0")
|
|
fixed-repr
|
|
(if append-zero "0"))))
|
|
navigator))
|
|
|
|
;; the function to render ~E directives
|
|
;; TODO: support rationals. Back off to ~D/~A in the appropriate cases
|
|
;; TODO: define ~E representation for Infinity
|
|
(defn- exponential-float [params navigator offset]
|
|
(let [[arg navigator] (next-arg navigator)
|
|
arg (convert-ratio arg)]
|
|
(loop [[mantissa exp] (float-parts (if (neg? arg) (- arg) arg))]
|
|
(let [w (:w params)
|
|
d (:d params)
|
|
e (:e params)
|
|
k (:k params)
|
|
expchar (or (:exponentchar params) \E)
|
|
add-sign (or (:at params) (neg? arg))
|
|
prepend-zero (<= k 0)
|
|
scaled-exp (- exp (dec k))
|
|
scaled-exp-str (str (Math/abs scaled-exp))
|
|
scaled-exp-str (str expchar (if (neg? scaled-exp) \- \+)
|
|
(if e (apply str
|
|
(repeat
|
|
(- e
|
|
(count scaled-exp-str))
|
|
\0)))
|
|
scaled-exp-str)
|
|
exp-width (count scaled-exp-str)
|
|
base-mantissa-width (count mantissa)
|
|
scaled-mantissa (str (apply str (repeat (- k) \0))
|
|
mantissa
|
|
(if d
|
|
(apply str
|
|
(repeat
|
|
(- d (dec base-mantissa-width)
|
|
(if (neg? k) (- k) 0)) \0))))
|
|
w-mantissa (if w (- w exp-width))
|
|
[rounded-mantissa _ incr-exp] (round-str
|
|
scaled-mantissa 0
|
|
(cond
|
|
(= k 0) (dec d)
|
|
(pos? k) d
|
|
(neg? k) (dec d))
|
|
(if w-mantissa
|
|
(- w-mantissa (if add-sign 1 0))))
|
|
full-mantissa (insert-scaled-decimal rounded-mantissa k)
|
|
append-zero (and (= k (count rounded-mantissa)) (nil? d))]
|
|
(if (not incr-exp)
|
|
(if w
|
|
(let [len (+ (count full-mantissa) exp-width)
|
|
signed-len (if add-sign (inc len) len)
|
|
prepend-zero (and prepend-zero (not (= signed-len w)))
|
|
full-len (if prepend-zero (inc signed-len) signed-len)
|
|
append-zero (and append-zero (< full-len w))]
|
|
(if (and (or (> full-len w) (and e (> (- exp-width 2) e)))
|
|
(:overflowchar params))
|
|
(print (apply str (repeat w (:overflowchar params))))
|
|
(print (str
|
|
(apply str
|
|
(repeat
|
|
(- w full-len (if append-zero 1 0))
|
|
(:padchar params)))
|
|
(if add-sign (if (neg? arg) \- \+))
|
|
(if prepend-zero "0")
|
|
full-mantissa
|
|
(if append-zero "0")
|
|
scaled-exp-str))))
|
|
(print (str
|
|
(if add-sign (if (neg? arg) \- \+))
|
|
(if prepend-zero "0")
|
|
full-mantissa
|
|
(if append-zero "0")
|
|
scaled-exp-str)))
|
|
(recur [rounded-mantissa (inc exp)]))))
|
|
navigator))
|
|
|
|
;; the function to render ~G directives
|
|
;; This just figures out whether to pass the request off to ~F or ~E based
|
|
;; on the algorithm in CLtL.
|
|
;; TODO: support rationals. Back off to ~D/~A in the appropriate cases
|
|
;; TODO: refactor so that float-parts isn't called twice
|
|
(defn- general-float [params navigator offsets]
|
|
(let [[arg _] (next-arg navigator)
|
|
arg (convert-ratio arg)
|
|
[mantissa exp] (float-parts (if (neg? arg) (- arg) arg))
|
|
w (:w params)
|
|
d (:d params)
|
|
e (:e params)
|
|
n (if (= arg 0.0) 0 (inc exp))
|
|
ee (if e (+ e 2) 4)
|
|
ww (if w (- w ee))
|
|
d (if d d (max (count mantissa) (min n 7)))
|
|
dd (- d n)]
|
|
(if (<= 0 dd d)
|
|
(let [navigator (fixed-float {:w ww, :d dd, :k 0,
|
|
:overflowchar (:overflowchar params),
|
|
:padchar (:padchar params), :at (:at params)}
|
|
navigator offsets)]
|
|
(print (apply str (repeat ee \space)))
|
|
navigator)
|
|
(exponential-float params navigator offsets))))
|
|
|
|
;; the function to render ~$ directives
|
|
;; TODO: support rationals. Back off to ~D/~A in the appropriate cases
|
|
(defn- dollar-float [params navigator offsets]
|
|
(let [[arg navigator] (next-arg navigator)
|
|
[mantissa exp] (float-parts (Math/abs arg))
|
|
d (:d params) ; digits after the decimal
|
|
n (:n params) ; minimum digits before the decimal
|
|
w (:w params) ; minimum field width
|
|
add-sign (or (:at params) (neg? arg))
|
|
[rounded-mantissa scaled-exp expanded] (round-str mantissa exp d nil)
|
|
fixed-repr (get-fixed rounded-mantissa (if expanded (inc scaled-exp) scaled-exp) d)
|
|
full-repr (str (apply str (repeat (- n (.indexOf fixed-repr \.)) \0)) fixed-repr)
|
|
full-len (+ (count full-repr) (if add-sign 1 0))]
|
|
(print (str
|
|
(if (and (:colon params) add-sign) (if (neg? arg) \- \+))
|
|
(apply str (repeat (- w full-len) (:padchar params)))
|
|
(if (and (not (:colon params)) add-sign) (if (neg? arg) \- \+))
|
|
full-repr))
|
|
navigator))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Support for the '~[...~]' conditional construct in its
|
|
;; different flavors
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; ~[...~] without any modifiers chooses one of the clauses based on the param or
|
|
;; next argument
|
|
;; TODO check arg is positive int
|
|
(defn- choice-conditional [params arg-navigator offsets]
|
|
(let [arg (:selector params)
|
|
[arg navigator] (if arg [arg arg-navigator] (next-arg arg-navigator))
|
|
clauses (:clauses params)
|
|
clause (if (or (neg? arg) (>= arg (count clauses)))
|
|
(first (:else params))
|
|
(nth clauses arg))]
|
|
(if clause
|
|
(execute-sub-format clause navigator (:base-args params))
|
|
navigator)))
|
|
|
|
;; ~:[...~] with the colon reads the next argument treating it as a truth value
|
|
(defn- boolean-conditional [params arg-navigator offsets]
|
|
(let [[arg navigator] (next-arg arg-navigator)
|
|
clauses (:clauses params)
|
|
clause (if arg
|
|
(second clauses)
|
|
(first clauses))]
|
|
(if clause
|
|
(execute-sub-format clause navigator (:base-args params))
|
|
navigator)))
|
|
|
|
;; ~@[...~] with the at sign executes the conditional if the next arg is not
|
|
;; nil/false without consuming the arg
|
|
(defn- check-arg-conditional [params arg-navigator offsets]
|
|
(let [[arg navigator] (next-arg arg-navigator)
|
|
clauses (:clauses params)
|
|
clause (if arg (first clauses))]
|
|
(if arg
|
|
(if clause
|
|
(execute-sub-format clause arg-navigator (:base-args params))
|
|
arg-navigator)
|
|
navigator)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Support for the '~{...~}' iteration construct in its
|
|
;; different flavors
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; ~{...~} without any modifiers uses the next argument as an argument list that
|
|
;; is consumed by all the iterations
|
|
(defn- iterate-sublist [params navigator offsets]
|
|
(let [max-count (:max-iterations params)
|
|
param-clause (first (:clauses params))
|
|
[clause navigator] (if (empty? param-clause)
|
|
(get-format-arg navigator)
|
|
[param-clause navigator])
|
|
[arg-list navigator] (next-arg navigator)
|
|
args (init-navigator arg-list)]
|
|
(loop [count 0
|
|
args args
|
|
last-pos (int -1)]
|
|
(if (and (not max-count) (= (:pos args) last-pos) (> count 1))
|
|
;; TODO get the offset in here and call format exception
|
|
(throw (js/Error "%{ construct not consuming any arguments: Infinite loop!")))
|
|
(if (or (and (empty? (:rest args))
|
|
(or (not (:colon (:right-params params))) (> count 0)))
|
|
(and max-count (>= count max-count)))
|
|
navigator
|
|
(let [iter-result (execute-sub-format clause args (:base-args params))]
|
|
(if (= :up-arrow (first iter-result))
|
|
navigator
|
|
(recur (inc count) iter-result (:pos args))))))))
|
|
|
|
;; ~:{...~} with the colon treats the next argument as a list of sublists. Each of the
|
|
;; sublists is used as the arglist for a single iteration.
|
|
(defn- iterate-list-of-sublists [params navigator offsets]
|
|
(let [max-count (:max-iterations params)
|
|
param-clause (first (:clauses params))
|
|
[clause navigator] (if (empty? param-clause)
|
|
(get-format-arg navigator)
|
|
[param-clause navigator])
|
|
[arg-list navigator] (next-arg navigator)]
|
|
(loop [count 0
|
|
arg-list arg-list]
|
|
(if (or (and (empty? arg-list)
|
|
(or (not (:colon (:right-params params))) (> count 0)))
|
|
(and max-count (>= count max-count)))
|
|
navigator
|
|
(let [iter-result (execute-sub-format
|
|
clause
|
|
(init-navigator (first arg-list))
|
|
(init-navigator (next arg-list)))]
|
|
(if (= :colon-up-arrow (first iter-result))
|
|
navigator
|
|
(recur (inc count) (next arg-list))))))))
|
|
|
|
;; ~@{...~} with the at sign uses the main argument list as the arguments to the iterations
|
|
;; is consumed by all the iterations
|
|
(defn- iterate-main-list [params navigator offsets]
|
|
(let [max-count (:max-iterations params)
|
|
param-clause (first (:clauses params))
|
|
[clause navigator] (if (empty? param-clause)
|
|
(get-format-arg navigator)
|
|
[param-clause navigator])]
|
|
(loop [count 0
|
|
navigator navigator
|
|
last-pos (int -1)]
|
|
(if (and (not max-count) (= (:pos navigator) last-pos) (> count 1))
|
|
;; TODO get the offset in here and call format exception
|
|
(throw (js/Error "%@{ construct not consuming any arguments: Infinite loop!")))
|
|
(if (or (and (empty? (:rest navigator))
|
|
(or (not (:colon (:right-params params))) (> count 0)))
|
|
(and max-count (>= count max-count)))
|
|
navigator
|
|
(let [iter-result (execute-sub-format clause navigator (:base-args params))]
|
|
(if (= :up-arrow (first iter-result))
|
|
(second iter-result)
|
|
(recur
|
|
(inc count) iter-result (:pos navigator))))))))
|
|
|
|
;; ~@:{...~} with both colon and at sign uses the main argument list as a set of sublists, one
|
|
;; of which is consumed with each iteration
|
|
(defn- iterate-main-sublists [params navigator offsets]
|
|
(let [max-count (:max-iterations params)
|
|
param-clause (first (:clauses params))
|
|
[clause navigator] (if (empty? param-clause)
|
|
(get-format-arg navigator)
|
|
[param-clause navigator])]
|
|
(loop [count 0
|
|
navigator navigator]
|
|
(if (or (and (empty? (:rest navigator))
|
|
(or (not (:colon (:right-params params))) (> count 0)))
|
|
(and max-count (>= count max-count)))
|
|
navigator
|
|
(let [[sublist navigator] (next-arg-or-nil navigator)
|
|
iter-result (execute-sub-format clause (init-navigator sublist) navigator)]
|
|
(if (= :colon-up-arrow (first iter-result))
|
|
navigator
|
|
(recur (inc count) navigator)))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; The '~< directive has two completely different meanings
|
|
;; in the '~<...~>' form it does justification, but with
|
|
;; ~<...~:>' it represents the logical block operation of the
|
|
;; pretty printer.
|
|
;;
|
|
;; Unfortunately, the current architecture decides what function
|
|
;; to call at form parsing time before the sub-clauses have been
|
|
;; folded, so it is left to run-time to make the decision.
|
|
;;
|
|
;; TODO: make it possible to make these decisions at compile-time.
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(declare ^{:arglists '([params navigator offsets])} format-logical-block)
|
|
(declare ^{:arglists '([params navigator offsets])} justify-clauses)
|
|
|
|
(defn- logical-block-or-justify [params navigator offsets]
|
|
(if (:colon (:right-params params))
|
|
(format-logical-block params navigator offsets)
|
|
(justify-clauses params navigator offsets)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Support for the '~<...~>' justification directive
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- render-clauses [clauses navigator base-navigator]
|
|
(loop [clauses clauses
|
|
acc []
|
|
navigator navigator]
|
|
(if (empty? clauses)
|
|
[acc navigator]
|
|
(let [clause (first clauses)
|
|
[iter-result result-str] (let [sb (StringBuffer.)]
|
|
(binding [*out* (StringBufferWriter. sb)]
|
|
[(execute-sub-format clause navigator base-navigator)
|
|
(str sb)]))]
|
|
(if (= :up-arrow (first iter-result))
|
|
[acc (second iter-result)]
|
|
(recur (next clauses) (conj acc result-str) iter-result))))))
|
|
|
|
;; TODO support for ~:; constructions
|
|
(defn- justify-clauses [params navigator offsets]
|
|
(let [[[eol-str] new-navigator] (when-let [else (:else params)]
|
|
(render-clauses else navigator (:base-args params)))
|
|
navigator (or new-navigator navigator)
|
|
[else-params new-navigator] (when-let [p (:else-params params)]
|
|
(realize-parameter-list p navigator))
|
|
navigator (or new-navigator navigator)
|
|
min-remaining (or (first (:min-remaining else-params)) 0)
|
|
max-columns (or (first (:max-columns else-params))
|
|
(get-max-column *out*))
|
|
clauses (:clauses params)
|
|
[strs navigator] (render-clauses clauses navigator (:base-args params))
|
|
slots (max 1
|
|
(+ (dec (count strs)) (if (:colon params) 1 0) (if (:at params) 1 0)))
|
|
chars (reduce + (map count strs))
|
|
mincol (:mincol params)
|
|
minpad (:minpad params)
|
|
colinc (:colinc params)
|
|
minout (+ chars (* slots minpad))
|
|
result-columns (if (<= minout mincol)
|
|
mincol
|
|
(+ mincol (* colinc
|
|
(+ 1 (quot (- minout mincol 1) colinc)))))
|
|
total-pad (- result-columns chars)
|
|
pad (max minpad (quot total-pad slots))
|
|
extra-pad (- total-pad (* pad slots))
|
|
pad-str (apply str (repeat pad (:padchar params)))]
|
|
(if (and eol-str (> (+ (get-column (:base @@*out*)) min-remaining result-columns)
|
|
max-columns))
|
|
(print eol-str))
|
|
(loop [slots slots
|
|
extra-pad extra-pad
|
|
strs strs
|
|
pad-only (or (:colon params)
|
|
(and (= (count strs) 1) (not (:at params))))]
|
|
(if (seq strs)
|
|
(do
|
|
(print (str (if (not pad-only) (first strs))
|
|
(if (or pad-only (next strs) (:at params)) pad-str)
|
|
(if (pos? extra-pad) (:padchar params))))
|
|
(recur
|
|
(dec slots)
|
|
(dec extra-pad)
|
|
(if pad-only strs (next strs))
|
|
false))))
|
|
navigator))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Support for case modification with ~(...~).
|
|
;;; We do this by wrapping the underlying writer with
|
|
;;; a special writer to do the appropriate modification. This
|
|
;;; allows us to support arbitrary-sized output and sources
|
|
;;; that may block.
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- downcase-writer
|
|
"Returns a proxy that wraps writer, converting all characters to lower case"
|
|
[writer]
|
|
(reify
|
|
IWriter
|
|
(-flush [_] (-flush writer))
|
|
(-write
|
|
;;no multi-arity, not sure of importance
|
|
#_([^chars cbuf ^Integer off ^Integer len]
|
|
(.write writer cbuf off len))
|
|
[this x]
|
|
(condp = (type x)
|
|
js/String
|
|
(let [s x]
|
|
(-write writer (string/lower-case s)))
|
|
|
|
js/Number
|
|
(let [c x]
|
|
;;TODO need to enforce integers only?
|
|
(-write writer (string/lower-case (char c))))))))
|
|
|
|
(defn- upcase-writer
|
|
"Returns a proxy that wraps writer, converting all characters to upper case"
|
|
[writer]
|
|
(reify
|
|
IWriter
|
|
(-flush [_] (-flush writer))
|
|
(-write
|
|
;;no multi-arity, not sure of importance
|
|
#_([^chars cbuf ^Integer off ^Integer len]
|
|
(.write writer cbuf off len))
|
|
[this x]
|
|
(condp = (type x)
|
|
js/String
|
|
(let [s x]
|
|
(-write writer (string/upper-case s)))
|
|
|
|
js/Number
|
|
(let [c x]
|
|
;;TODO need to enforce integers only?
|
|
(-write writer (string/upper-case (char c))))))))
|
|
|
|
(defn- capitalize-string
|
|
"Capitalizes the words in a string. If first? is false, don't capitalize the
|
|
first character of the string even if it's a letter."
|
|
[s first?]
|
|
(let [f (first s)
|
|
s (if (and first? f (gstring/isUnicodeChar f))
|
|
(str (string/upper-case f) (subs s 1))
|
|
s)]
|
|
(apply str
|
|
(first
|
|
(consume
|
|
(fn [s]
|
|
(if (empty? s)
|
|
[nil nil]
|
|
(let [m (.exec (js/RegExp "\\W\\w" "g") s)
|
|
offset (and m (inc (.-index m)))]
|
|
(if offset
|
|
[(str (subs s 0 offset)
|
|
(string/upper-case (nth s offset)))
|
|
(subs s (inc offset))]
|
|
[s nil]))))
|
|
s)))))
|
|
|
|
(defn- capitalize-word-writer
|
|
"Returns a proxy that wraps writer, capitalizing all words"
|
|
[writer]
|
|
(let [last-was-whitespace? (atom true)]
|
|
(reify
|
|
IWriter
|
|
(-flush [_] (-flush writer))
|
|
(-write
|
|
;;no multi-arity
|
|
#_([^chars cbuf ^Integer off ^Integer len]
|
|
(.write writer cbuf off len))
|
|
[this x]
|
|
(condp = (type x)
|
|
js/String
|
|
(let [s x]
|
|
(-write writer
|
|
(capitalize-string (.toLowerCase s) @last-was-whitespace?))
|
|
(when (pos? (.-length s))
|
|
(reset! last-was-whitespace? (gstring/isEmptyOrWhitespace (nth s (dec (count s)))))))
|
|
|
|
js/Number
|
|
(let [c (char x)]
|
|
(let [mod-c (if @last-was-whitespace? (string/upper-case c) c)]
|
|
(-write writer mod-c)
|
|
(reset! last-was-whitespace? (gstring/isEmptyOrWhitespace c)))))))))
|
|
|
|
(defn- init-cap-writer
|
|
"Returns a proxy that wraps writer, capitalizing the first word"
|
|
[writer]
|
|
(let [capped (atom false)]
|
|
(reify
|
|
IWriter
|
|
(-flush [_] (-flush writer))
|
|
(-write
|
|
;;no multi-arity
|
|
#_([^chars cbuf ^Integer off ^Integer len]
|
|
(.write writer cbuf off len))
|
|
[this x]
|
|
(condp = (type x)
|
|
js/String
|
|
(let [s (string/lower-case x)]
|
|
(if (not @capped)
|
|
(let [m (.exec (js/RegExp "\\S" "g") s)
|
|
offset (and m (.-index m))]
|
|
(if offset
|
|
(do (-write writer
|
|
(str (subs s 0 offset)
|
|
(string/upper-case (nth s offset))
|
|
(string/lower-case (subs s (inc offset)))))
|
|
(reset! capped true))
|
|
(-write writer s)))
|
|
(-write writer (string/lower-case s))))
|
|
|
|
js/Number
|
|
(let [c (char x)]
|
|
(if (and (not @capped) (gstring/isUnicodeChar c))
|
|
(do
|
|
(reset! capped true)
|
|
(-write writer (string/upper-case c)))
|
|
(-write writer (string/lower-case c)))))))))
|
|
|
|
(defn- modify-case [make-writer params navigator offsets]
|
|
(let [clause (first (:clauses params))]
|
|
(binding [*out* (make-writer *out*)]
|
|
(execute-sub-format clause navigator (:base-args params)))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; If necessary, wrap the writer in a PrettyWriter object
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; TODO update this doc string to show correct way to print
|
|
(defn get-pretty-writer
|
|
"Returns the IWriter passed in wrapped in a pretty writer proxy, unless it's
|
|
already a pretty writer. Generally, it is unnecessary to call this function, since pprint,
|
|
write, and cl-format all call it if they need to. However if you want the state to be
|
|
preserved across calls, you will want to wrap them with this.
|
|
|
|
For example, when you want to generate column-aware output with multiple calls to cl-format,
|
|
do it like in this example:
|
|
|
|
(defn print-table [aseq column-width]
|
|
(binding [*out* (get-pretty-writer *out*)]
|
|
(doseq [row aseq]
|
|
(doseq [col row]
|
|
(cl-format true \"~4D~7,vT\" col column-width))
|
|
(prn))))
|
|
|
|
Now when you run:
|
|
|
|
user> (print-table (map #(vector % (* % %) (* % % %)) (range 1 11)) 8)
|
|
|
|
It prints a table of squares and cubes for the numbers from 1 to 10:
|
|
|
|
1 1 1
|
|
2 4 8
|
|
3 9 27
|
|
4 16 64
|
|
5 25 125
|
|
6 36 216
|
|
7 49 343
|
|
8 64 512
|
|
9 81 729
|
|
10 100 1000"
|
|
[writer]
|
|
(if (pretty-writer? writer)
|
|
writer
|
|
(pretty-writer writer *print-right-margin* *print-miser-width*)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Support for column-aware operations ~&, ~T
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn fresh-line
|
|
"Make a newline if *out* is not already at the beginning of the line. If *out* is
|
|
not a pretty writer (which keeps track of columns), this function always outputs a newline."
|
|
[]
|
|
(if (satisfies? IDeref *out*)
|
|
(if (not (= 0 (get-column (:base @@*out*))))
|
|
(prn))
|
|
(prn)))
|
|
|
|
(defn- absolute-tabulation [params navigator offsets]
|
|
(let [colnum (:colnum params)
|
|
colinc (:colinc params)
|
|
current (get-column (:base @@*out*))
|
|
space-count (cond
|
|
(< current colnum) (- colnum current)
|
|
(= colinc 0) 0
|
|
:else (- colinc (rem (- current colnum) colinc)))]
|
|
(print (apply str (repeat space-count \space))))
|
|
navigator)
|
|
|
|
(defn- relative-tabulation [params navigator offsets]
|
|
(let [colrel (:colnum params)
|
|
colinc (:colinc params)
|
|
start-col (+ colrel (get-column (:base @@*out*)))
|
|
offset (if (pos? colinc) (rem start-col colinc) 0)
|
|
space-count (+ colrel (if (= 0 offset) 0 (- colinc offset)))]
|
|
(print (apply str (repeat space-count \space))))
|
|
navigator)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Support for accessing the pretty printer from a format
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; TODO: support ~@; per-line-prefix separator
|
|
;; TODO: get the whole format wrapped so we can start the lb at any column
|
|
(defn- format-logical-block [params navigator offsets]
|
|
(let [clauses (:clauses params)
|
|
clause-count (count clauses)
|
|
prefix (cond
|
|
(> clause-count 1) (:string (:params (first (first clauses))))
|
|
(:colon params) "(")
|
|
body (nth clauses (if (> clause-count 1) 1 0))
|
|
suffix (cond
|
|
(> clause-count 2) (:string (:params (first (nth clauses 2))))
|
|
(:colon params) ")")
|
|
[arg navigator] (next-arg navigator)]
|
|
(pprint-logical-block :prefix prefix :suffix suffix
|
|
(execute-sub-format
|
|
body
|
|
(init-navigator arg)
|
|
(:base-args params)))
|
|
navigator))
|
|
|
|
(defn- set-indent [params navigator offsets]
|
|
(let [relative-to (if (:colon params) :current :block)]
|
|
(pprint-indent relative-to (:n params))
|
|
navigator))
|
|
|
|
;;; TODO: support ~:T section options for ~T
|
|
(defn- conditional-newline [params navigator offsets]
|
|
(let [kind (if (:colon params)
|
|
(if (:at params) :mandatory :fill)
|
|
(if (:at params) :miser :linear))]
|
|
(pprint-newline kind)
|
|
navigator))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; The table of directives we support, each with its params,
|
|
;;; properties, and the compilation function
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defdirectives
|
|
(\A
|
|
[:mincol [0 js/Number] :colinc [1 js/Number] :minpad [0 js/Number] :padchar [\space js/String]]
|
|
#{:at :colon :both} {}
|
|
#(format-ascii print-str %1 %2 %3))
|
|
|
|
(\S
|
|
[:mincol [0 js/Number] :colinc [1 js/Number] :minpad [0 js/Number] :padchar [\space js/String]]
|
|
#{:at :colon :both} {}
|
|
#(format-ascii pr-str %1 %2 %3))
|
|
|
|
(\D
|
|
[:mincol [0 js/Number] :padchar [\space js/String] :commachar [\, js/String]
|
|
:commainterval [3 js/Number]]
|
|
#{:at :colon :both} {}
|
|
#(format-integer 10 %1 %2 %3))
|
|
|
|
(\B
|
|
[:mincol [0 js/Number] :padchar [\space js/String] :commachar [\, js/String]
|
|
:commainterval [3 js/Number]]
|
|
#{:at :colon :both} {}
|
|
#(format-integer 2 %1 %2 %3))
|
|
|
|
(\O
|
|
[:mincol [0 js/Number] :padchar [\space js/String] :commachar [\, js/String]
|
|
:commainterval [3 js/Number]]
|
|
#{:at :colon :both} {}
|
|
#(format-integer 8 %1 %2 %3))
|
|
|
|
(\X
|
|
[:mincol [0 js/Number] :padchar [\space js/String] :commachar [\, js/String]
|
|
:commainterval [3 js/Number]]
|
|
#{:at :colon :both} {}
|
|
#(format-integer 16 %1 %2 %3))
|
|
|
|
(\R
|
|
[:base [nil js/Number] :mincol [0 js/Number] :padchar [\space js/String] :commachar [\, js/String]
|
|
:commainterval [3 js/Number]]
|
|
#{:at :colon :both} {}
|
|
(do
|
|
(cond ; ~R is overloaded with bizareness
|
|
(first (:base params)) #(format-integer (:base %1) %1 %2 %3)
|
|
(and (:at params) (:colon params)) #(format-old-roman %1 %2 %3)
|
|
(:at params) #(format-new-roman %1 %2 %3)
|
|
(:colon params) #(format-ordinal-english %1 %2 %3)
|
|
true #(format-cardinal-english %1 %2 %3))))
|
|
|
|
(\P
|
|
[]
|
|
#{:at :colon :both} {}
|
|
(fn [params navigator offsets]
|
|
(let [navigator (if (:colon params) (relative-reposition navigator -1) navigator)
|
|
strs (if (:at params) ["y" "ies"] ["" "s"])
|
|
[arg navigator] (next-arg navigator)]
|
|
(print (if (= arg 1) (first strs) (second strs)))
|
|
navigator)))
|
|
|
|
(\C
|
|
[:char-format [nil js/String]]
|
|
#{:at :colon :both} {}
|
|
(cond
|
|
(:colon params) pretty-character
|
|
(:at params) readable-character
|
|
:else plain-character))
|
|
|
|
(\F
|
|
[:w [nil js/Number] :d [nil js/Number] :k [0 js/Number] :overflowchar [nil js/String]
|
|
:padchar [\space js/String]]
|
|
#{:at} {}
|
|
fixed-float)
|
|
|
|
(\E
|
|
[:w [nil js/Number] :d [nil js/Number] :e [nil js/Number] :k [1 js/Number]
|
|
:overflowchar [nil js/String] :padchar [\space js/String]
|
|
:exponentchar [nil js/String]]
|
|
#{:at} {}
|
|
exponential-float)
|
|
|
|
(\G
|
|
[:w [nil js/Number] :d [nil js/Number] :e [nil js/Number] :k [1 js/Number]
|
|
:overflowchar [nil js/String] :padchar [\space js/String]
|
|
:exponentchar [nil js/String]]
|
|
#{:at} {}
|
|
general-float)
|
|
|
|
(\$
|
|
[:d [2 js/Number] :n [1 js/Number] :w [0 js/Number] :padchar [\space js/String]]
|
|
#{:at :colon :both} {}
|
|
dollar-float)
|
|
|
|
(\%
|
|
[:count [1 js/Number]]
|
|
#{} {}
|
|
(fn [params arg-navigator offsets]
|
|
(dotimes [i (:count params)]
|
|
(prn))
|
|
arg-navigator))
|
|
|
|
(\&
|
|
[:count [1 js/Number]]
|
|
#{:pretty} {}
|
|
(fn [params arg-navigator offsets]
|
|
(let [cnt (:count params)]
|
|
(if (pos? cnt) (fresh-line))
|
|
(dotimes [i (dec cnt)]
|
|
(prn)))
|
|
arg-navigator))
|
|
|
|
(\|
|
|
[:count [1 js/Number]]
|
|
#{} {}
|
|
(fn [params arg-navigator offsets]
|
|
(dotimes [i (:count params)]
|
|
(print \formfeed))
|
|
arg-navigator))
|
|
|
|
(\~
|
|
[:n [1 js/Number]]
|
|
#{} {}
|
|
(fn [params arg-navigator offsets]
|
|
(let [n (:n params)]
|
|
(print (apply str (repeat n \~)))
|
|
arg-navigator)))
|
|
|
|
(\newline ;; Whitespace supression is handled in the compilation loop
|
|
[]
|
|
#{:colon :at} {}
|
|
(fn [params arg-navigator offsets]
|
|
(if (:at params)
|
|
(prn))
|
|
arg-navigator))
|
|
|
|
(\T
|
|
[:colnum [1 js/Number] :colinc [1 js/Number]]
|
|
#{:at :pretty} {}
|
|
(if (:at params)
|
|
#(relative-tabulation %1 %2 %3)
|
|
#(absolute-tabulation %1 %2 %3)))
|
|
|
|
(\*
|
|
[:n [1 js/Number]]
|
|
#{:colon :at} {}
|
|
(fn [params navigator offsets]
|
|
(let [n (:n params)]
|
|
(if (:at params)
|
|
(absolute-reposition navigator n)
|
|
(relative-reposition navigator (if (:colon params) (- n) n))))))
|
|
|
|
(\?
|
|
[]
|
|
#{:at} {}
|
|
(if (:at params)
|
|
(fn [params navigator offsets] ; args from main arg list
|
|
(let [[subformat navigator] (get-format-arg navigator)]
|
|
(execute-sub-format subformat navigator (:base-args params))))
|
|
(fn [params navigator offsets] ; args from sub-list
|
|
(let [[subformat navigator] (get-format-arg navigator)
|
|
[subargs navigator] (next-arg navigator)
|
|
sub-navigator (init-navigator subargs)]
|
|
(execute-sub-format subformat sub-navigator (:base-args params))
|
|
navigator))))
|
|
|
|
(\(
|
|
[]
|
|
#{:colon :at :both} {:right \), :allows-separator nil, :else nil}
|
|
(let [mod-case-writer (cond
|
|
(and (:at params) (:colon params))
|
|
upcase-writer
|
|
|
|
(:colon params)
|
|
capitalize-word-writer
|
|
|
|
(:at params)
|
|
init-cap-writer
|
|
|
|
:else
|
|
downcase-writer)]
|
|
#(modify-case mod-case-writer %1 %2 %3)))
|
|
|
|
(\) [] #{} {} nil)
|
|
|
|
(\[
|
|
[:selector [nil js/Number]]
|
|
#{:colon :at} {:right \], :allows-separator true, :else :last}
|
|
(cond
|
|
(:colon params)
|
|
boolean-conditional
|
|
|
|
(:at params)
|
|
check-arg-conditional
|
|
|
|
true
|
|
choice-conditional))
|
|
|
|
(\; [:min-remaining [nil js/Number] :max-columns [nil js/Number]]
|
|
#{:colon} {:separator true} nil)
|
|
|
|
(\] [] #{} {} nil)
|
|
|
|
(\{
|
|
[:max-iterations [nil js/Number]]
|
|
#{:colon :at :both} {:right \}, :allows-separator false}
|
|
(cond
|
|
(and (:at params) (:colon params))
|
|
iterate-main-sublists
|
|
|
|
(:colon params)
|
|
iterate-list-of-sublists
|
|
|
|
(:at params)
|
|
iterate-main-list
|
|
|
|
true
|
|
iterate-sublist))
|
|
|
|
(\} [] #{:colon} {} nil)
|
|
|
|
(\<
|
|
[:mincol [0 js/Number] :colinc [1 js/Number] :minpad [0 js/Number] :padchar [\space js/String]]
|
|
#{:colon :at :both :pretty} {:right \>, :allows-separator true, :else :first}
|
|
logical-block-or-justify)
|
|
|
|
(\> [] #{:colon} {} nil)
|
|
|
|
;; TODO: detect errors in cases where colon not allowed
|
|
(\^ [:arg1 [nil js/Number] :arg2 [nil js/Number] :arg3 [nil js/Number]]
|
|
#{:colon} {}
|
|
(fn [params navigator offsets]
|
|
(let [arg1 (:arg1 params)
|
|
arg2 (:arg2 params)
|
|
arg3 (:arg3 params)
|
|
exit (if (:colon params) :colon-up-arrow :up-arrow)]
|
|
(cond
|
|
(and arg1 arg2 arg3)
|
|
(if (<= arg1 arg2 arg3) [exit navigator] navigator)
|
|
|
|
(and arg1 arg2)
|
|
(if (= arg1 arg2) [exit navigator] navigator)
|
|
|
|
arg1
|
|
(if (= arg1 0) [exit navigator] navigator)
|
|
|
|
true ; TODO: handle looking up the arglist stack for info
|
|
(if (if (:colon params)
|
|
(empty? (:rest (:base-args params)))
|
|
(empty? (:rest navigator)))
|
|
[exit navigator] navigator)))))
|
|
|
|
(\W
|
|
[]
|
|
#{:at :colon :both :pretty} {}
|
|
(if (or (:at params) (:colon params))
|
|
(let [bindings (concat
|
|
(if (:at params) [:level nil :length nil] [])
|
|
(if (:colon params) [:pretty true] []))]
|
|
(fn [params navigator offsets]
|
|
(let [[arg navigator] (next-arg navigator)]
|
|
(if (apply write arg bindings)
|
|
[:up-arrow navigator]
|
|
navigator))))
|
|
(fn [params navigator offsets]
|
|
(let [[arg navigator] (next-arg navigator)]
|
|
(if (write-out arg)
|
|
[:up-arrow navigator]
|
|
navigator)))))
|
|
|
|
(\_
|
|
[]
|
|
#{:at :colon :both} {}
|
|
conditional-newline)
|
|
|
|
(\I
|
|
[:n [0 js/Number]]
|
|
#{:colon} {}
|
|
set-indent)
|
|
)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Code to manage the parameters and flags associated with each
|
|
;; directive in the format string.
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^{:private true}
|
|
param-pattern #"^([vV]|#|('.)|([+-]?\d+)|(?=,))")
|
|
|
|
(def ^{:private true}
|
|
special-params #{:parameter-from-args :remaining-arg-count})
|
|
|
|
(defn- extract-param [[s offset saw-comma]]
|
|
(let [m (js/RegExp. (.-source param-pattern) "g")
|
|
param (.exec m s)]
|
|
(if param
|
|
(let [token-str (first param)
|
|
remainder (subs s (.-lastIndex m))
|
|
new-offset (+ offset (.-lastIndex m))]
|
|
(if (not (= \, (nth remainder 0)))
|
|
[[token-str offset] [remainder new-offset false]]
|
|
[[token-str offset] [(subs remainder 1) (inc new-offset) true]]))
|
|
(if saw-comma
|
|
(format-error "Badly formed parameters in format directive" offset)
|
|
[nil [s offset]]))))
|
|
|
|
(defn- extract-params [s offset]
|
|
(consume extract-param [s offset false]))
|
|
|
|
(defn- translate-param
|
|
"Translate the string representation of a param to the internalized
|
|
representation"
|
|
[[p offset]]
|
|
[(cond
|
|
(= (.-length p) 0) nil
|
|
(and (= (.-length p) 1) (contains? #{\v \V} (nth p 0))) :parameter-from-args
|
|
(and (= (.-length p) 1) (= \# (nth p 0))) :remaining-arg-count
|
|
(and (= (.-length p) 2) (= \' (nth p 0))) (nth p 1)
|
|
true (js/parseInt p 10))
|
|
offset])
|
|
|
|
(def ^{:private true}
|
|
flag-defs {\: :colon, \@ :at})
|
|
|
|
(defn- extract-flags [s offset]
|
|
(consume
|
|
(fn [[s offset flags]]
|
|
(if (empty? s)
|
|
[nil [s offset flags]]
|
|
(let [flag (get flag-defs (first s))]
|
|
(if flag
|
|
(if (contains? flags flag)
|
|
(format-error
|
|
(str "Flag \"" (first s) "\" appears more than once in a directive")
|
|
offset)
|
|
[true [(subs s 1) (inc offset) (assoc flags flag [true offset])]])
|
|
[nil [s offset flags]]))))
|
|
[s offset {}]))
|
|
|
|
(defn- check-flags [def flags]
|
|
(let [allowed (:flags def)]
|
|
(if (and (not (:at allowed)) (:at flags))
|
|
(format-error (str "\"@\" is an illegal flag for format directive \"" (:directive def) "\"")
|
|
(nth (:at flags) 1)))
|
|
(if (and (not (:colon allowed)) (:colon flags))
|
|
(format-error (str "\":\" is an illegal flag for format directive \"" (:directive def) "\"")
|
|
(nth (:colon flags) 1)))
|
|
(if (and (not (:both allowed)) (:at flags) (:colon flags))
|
|
(format-error (str "Cannot combine \"@\" and \":\" flags for format directive \""
|
|
(:directive def) "\"")
|
|
(min (nth (:colon flags) 1) (nth (:at flags) 1))))))
|
|
|
|
(defn- map-params
|
|
"Takes a directive definition and the list of actual parameters and
|
|
a map of flags and returns a map of the parameters and flags with defaults
|
|
filled in. We check to make sure that there are the right types and number
|
|
of parameters as well."
|
|
[def params flags offset]
|
|
(check-flags def flags)
|
|
(if (> (count params) (count (:params def)))
|
|
(format-error
|
|
(cl-format
|
|
nil
|
|
"Too many parameters for directive \"~C\": ~D~:* ~[were~;was~:;were~] specified but only ~D~:* ~[are~;is~:;are~] allowed"
|
|
(:directive def) (count params) (count (:params def)))
|
|
(second (first params))))
|
|
(doall
|
|
(map #(let [val (first %1)]
|
|
(if (not (or (nil? val) (contains? special-params val)
|
|
(= (second (second %2)) (type val))))
|
|
(format-error (str "Parameter " (name (first %2))
|
|
" has bad type in directive \"" (:directive def) "\": "
|
|
(type val))
|
|
(second %1))) )
|
|
params (:params def)))
|
|
|
|
(merge ; create the result map
|
|
(into (array-map) ; start with the default values, make sure the order is right
|
|
(reverse (for [[name [default]] (:params def)] [name [default offset]])))
|
|
(reduce #(apply assoc %1 %2) {} (filter #(first (nth % 1)) (zipmap (keys (:params def)) params))) ; add the specified parameters, filtering out nils
|
|
flags)); and finally add the flags
|
|
|
|
(defn- compile-directive [s offset]
|
|
(let [[raw-params [rest offset]] (extract-params s offset)
|
|
[_ [rest offset flags]] (extract-flags rest offset)
|
|
directive (first rest)
|
|
def (get directive-table (string/upper-case directive))
|
|
params (if def (map-params def (map translate-param raw-params) flags offset))]
|
|
(if (not directive)
|
|
(format-error "Format string ended in the middle of a directive" offset))
|
|
(if (not def)
|
|
(format-error (str "Directive \"" directive "\" is undefined") offset))
|
|
[(compiled-directive. ((:generator-fn def) params offset) def params offset)
|
|
(let [remainder (subs rest 1)
|
|
offset (inc offset)
|
|
trim? (and (= \newline (:directive def))
|
|
(not (:colon params)))
|
|
trim-count (if trim? (prefix-count remainder [\space \tab]) 0)
|
|
remainder (subs remainder trim-count)
|
|
offset (+ offset trim-count)]
|
|
[remainder offset])]))
|
|
|
|
(defn- compile-raw-string [s offset]
|
|
(compiled-directive. (fn [_ a _] (print s) a) nil {:string s} offset))
|
|
|
|
(defn- right-bracket [this] (:right (:bracket-info (:def this))))
|
|
|
|
(defn- separator? [this] (:separator (:bracket-info (:def this))))
|
|
|
|
(defn- else-separator? [this]
|
|
(and (:separator (:bracket-info (:def this)))
|
|
(:colon (:params this))))
|
|
|
|
(declare ^{:arglists '([bracket-info offset remainder])} collect-clauses)
|
|
|
|
(defn- process-bracket [this remainder]
|
|
(let [[subex remainder] (collect-clauses (:bracket-info (:def this))
|
|
(:offset this) remainder)]
|
|
[(compiled-directive.
|
|
(:func this) (:def this)
|
|
(merge (:params this) (tuple-map subex (:offset this)))
|
|
(:offset this))
|
|
remainder]))
|
|
|
|
(defn- process-clause [bracket-info offset remainder]
|
|
(consume
|
|
(fn [remainder]
|
|
(if (empty? remainder)
|
|
(format-error "No closing bracket found." offset)
|
|
(let [this (first remainder)
|
|
remainder (next remainder)]
|
|
(cond
|
|
(right-bracket this)
|
|
(process-bracket this remainder)
|
|
|
|
(= (:right bracket-info) (:directive (:def this)))
|
|
[ nil [:right-bracket (:params this) nil remainder]]
|
|
|
|
(else-separator? this)
|
|
[nil [:else nil (:params this) remainder]]
|
|
|
|
(separator? this)
|
|
[nil [:separator nil nil remainder]] ;; TODO: check to make sure that there are no params on ~;
|
|
|
|
true
|
|
[this remainder]))))
|
|
remainder))
|
|
|
|
(defn- collect-clauses [bracket-info offset remainder]
|
|
(second
|
|
(consume
|
|
(fn [[clause-map saw-else remainder]]
|
|
(let [[clause [type right-params else-params remainder]]
|
|
(process-clause bracket-info offset remainder)]
|
|
(cond
|
|
(= type :right-bracket)
|
|
[nil [(merge-with concat clause-map
|
|
{(if saw-else :else :clauses) [clause]
|
|
:right-params right-params})
|
|
remainder]]
|
|
|
|
(= type :else)
|
|
(cond
|
|
(:else clause-map)
|
|
(format-error "Two else clauses (\"~:;\") inside bracket construction." offset)
|
|
|
|
(not (:else bracket-info))
|
|
(format-error "An else clause (\"~:;\") is in a bracket type that doesn't support it."
|
|
offset)
|
|
|
|
(and (= :first (:else bracket-info)) (seq (:clauses clause-map)))
|
|
(format-error
|
|
"The else clause (\"~:;\") is only allowed in the first position for this directive."
|
|
offset)
|
|
|
|
true ; if the ~:; is in the last position, the else clause
|
|
; is next, this was a regular clause
|
|
(if (= :first (:else bracket-info))
|
|
[true [(merge-with concat clause-map {:else [clause] :else-params else-params})
|
|
false remainder]]
|
|
[true [(merge-with concat clause-map {:clauses [clause]})
|
|
true remainder]]))
|
|
|
|
(= type :separator)
|
|
(cond
|
|
saw-else
|
|
(format-error "A plain clause (with \"~;\") follows an else clause (\"~:;\") inside bracket construction." offset)
|
|
|
|
(not (:allows-separator bracket-info))
|
|
(format-error "A separator (\"~;\") is in a bracket type that doesn't support it."
|
|
offset)
|
|
|
|
true
|
|
[true [(merge-with concat clause-map {:clauses [clause]})
|
|
false remainder]]))))
|
|
[{:clauses []} false remainder])))
|
|
|
|
(defn- process-nesting
|
|
"Take a linearly compiled format and process the bracket directives to give it
|
|
the appropriate tree structure"
|
|
[format]
|
|
(first
|
|
(consume
|
|
(fn [remainder]
|
|
(let [this (first remainder)
|
|
remainder (next remainder)
|
|
bracket (:bracket-info (:def this))]
|
|
(if (:right bracket)
|
|
(process-bracket this remainder)
|
|
[this remainder])))
|
|
format)))
|
|
|
|
(defn- compile-format
|
|
"Compiles format-str into a compiled format which can be used as an argument
|
|
to cl-format just like a plain format string. Use this function for improved
|
|
performance when you're using the same format string repeatedly"
|
|
[format-str]
|
|
(binding [*format-str* format-str]
|
|
(process-nesting
|
|
(first
|
|
(consume
|
|
(fn [[s offset]]
|
|
(if (empty? s)
|
|
[nil s]
|
|
(let [tilde (.indexOf s \~)]
|
|
(cond
|
|
(neg? tilde) [(compile-raw-string s offset) ["" (+ offset (.-length s))]]
|
|
(zero? tilde) (compile-directive (subs s 1) (inc offset))
|
|
true
|
|
[(compile-raw-string (subs s 0 tilde) offset) [(subs s tilde) (+ tilde offset)]]))))
|
|
[format-str 0])))))
|
|
|
|
(defn- needs-pretty
|
|
"determine whether a given compiled format has any directives that depend on the
|
|
column number or pretty printing"
|
|
[format]
|
|
(loop [format format]
|
|
(if (empty? format)
|
|
false
|
|
(if (or (:pretty (:flags (:def (first format))))
|
|
(some needs-pretty (first (:clauses (:params (first format)))))
|
|
(some needs-pretty (first (:else (:params (first format))))))
|
|
true
|
|
(recur (next format))))))
|
|
|
|
;;NB We depart from the original api. In clj, if execute-format is called multiple times with the same stream or
|
|
;; called on *out*, the results are different than if the same calls are made with different streams or printing
|
|
;; to a string. The reason is that mutating the underlying stream changes the result by changing spacing.
|
|
;;
|
|
;; clj:
|
|
;; * stream => "1 2 3"
|
|
;; * true (prints to *out*) => "1 2 3"
|
|
;; * nil (prints to string) => "1 2 3"
|
|
;; cljs:
|
|
;; * stream => "1 2 3"
|
|
;; * true (prints via *print-fn*) => "1 2 3"
|
|
;; * nil (prints to string) => "1 2 3"
|
|
(defn- execute-format
|
|
"Executes the format with the arguments."
|
|
{:skip-wiki true}
|
|
([stream format args]
|
|
(let [sb (StringBuffer.)
|
|
real-stream (if (or (not stream) (true? stream))
|
|
(StringBufferWriter. sb)
|
|
stream)
|
|
wrapped-stream (if (and (needs-pretty format)
|
|
(not (pretty-writer? real-stream)))
|
|
(get-pretty-writer real-stream)
|
|
real-stream)]
|
|
(binding [*out* wrapped-stream]
|
|
(try
|
|
(execute-format format args)
|
|
(finally
|
|
(if-not (identical? real-stream wrapped-stream)
|
|
(-flush wrapped-stream))))
|
|
(cond
|
|
(not stream) (str sb)
|
|
(true? stream) (string-print (str sb))
|
|
:else nil))))
|
|
([format args]
|
|
(map-passing-context
|
|
(fn [element context]
|
|
(if (abort? context)
|
|
[nil context]
|
|
(let [[params args] (realize-parameter-list
|
|
(:params element) context)
|
|
[params offsets] (unzip-map params)
|
|
params (assoc params :base-args args)]
|
|
[nil (apply (:func element) [params args offsets])])))
|
|
args
|
|
format)
|
|
nil))
|
|
|
|
;;; This is a bad idea, but it prevents us from leaking private symbols
|
|
;;; This should all be replaced by really compiled formats anyway.
|
|
(def ^{:private true} cached-compile (memoize compile-format))
|
|
|
|
;;======================================================================
|
|
;; dispatch.clj
|
|
;;======================================================================
|
|
|
|
(defn- use-method
|
|
"Installs a function as a new method of multimethod associated with dispatch-value. "
|
|
[multifn dispatch-val func]
|
|
(-add-method multifn dispatch-val func))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Implementations of specific dispatch table entries
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;; Handle forms that can be "back-translated" to reader macros
|
|
;;; Not all reader macros can be dealt with this way or at all.
|
|
;;; Macros that we can't deal with at all are:
|
|
;;; ; - The comment character is absorbed by the reader and never is part of the form
|
|
;;; ` - Is fully processed at read time into a lisp expression (which will contain concats
|
|
;;; and regular quotes).
|
|
;;; ~@ - Also fully eaten by the processing of ` and can't be used outside.
|
|
;;; , - is whitespace and is lost (like all other whitespace). Formats can generate commas
|
|
;;; where they deem them useful to help readability.
|
|
;;; ^ - Adding metadata completely disappears at read time and the data appears to be
|
|
;;; completely lost.
|
|
;;;
|
|
;;; Most other syntax stuff is dealt with directly by the formats (like (), [], {}, and #{})
|
|
;;; or directly by printing the objects using Clojure's built-in print functions (like
|
|
;;; :keyword, \char, or ""). The notable exception is #() which is special-cased.
|
|
|
|
(def ^{:private true} reader-macros
|
|
{'quote "'"
|
|
'var "#'"
|
|
'clojure.core/deref "@",
|
|
'clojure.core/unquote "~"
|
|
'cljs.core/deref "@",
|
|
'cljs.core/unquote "~"})
|
|
|
|
(defn- pprint-reader-macro [alis]
|
|
(let [macro-char (reader-macros (first alis))]
|
|
(when (and macro-char (= 2 (count alis)))
|
|
(-write *out* macro-char)
|
|
(write-out (second alis))
|
|
true)))
|
|
|
|
;;======================================================================
|
|
;; Dispatch for the basic data types when interpreted
|
|
;; as data (as opposed to code).
|
|
;;======================================================================
|
|
|
|
;;; TODO: inline these formatter statements into funcs so that we
|
|
;;; are a little easier on the stack. (Or, do "real" compilation, a
|
|
;;; la Common Lisp)
|
|
|
|
;;; (def pprint-simple-list (formatter-out "~:<~@{~w~^ ~_~}~:>"))
|
|
(defn- pprint-simple-list [alis]
|
|
(pprint-logical-block :prefix "(" :suffix ")"
|
|
(print-length-loop [alis (seq alis)]
|
|
(when alis
|
|
(write-out (first alis))
|
|
(when (next alis)
|
|
(-write *out* " ")
|
|
(pprint-newline :linear)
|
|
(recur (next alis)))))))
|
|
|
|
(defn- pprint-list [alis]
|
|
(if-not (pprint-reader-macro alis)
|
|
(pprint-simple-list alis)))
|
|
|
|
;;; (def pprint-vector (formatter-out "~<[~;~@{~w~^ ~_~}~;]~:>"))
|
|
(defn- pprint-vector [avec]
|
|
(pprint-logical-block :prefix "[" :suffix "]"
|
|
(print-length-loop [aseq (seq avec)]
|
|
(when aseq
|
|
(write-out (first aseq))
|
|
(when (next aseq)
|
|
(-write *out* " ")
|
|
(pprint-newline :linear)
|
|
(recur (next aseq)))))))
|
|
|
|
(def ^{:private true} pprint-array (formatter-out "~<[~;~@{~w~^, ~:_~}~;]~:>"))
|
|
|
|
;;; (def pprint-map (formatter-out "~<{~;~@{~<~w~^ ~_~w~:>~^, ~_~}~;}~:>"))
|
|
(defn- pprint-map [amap]
|
|
(let [[ns lift-map] (when (not (record? amap))
|
|
(#'cljs.core/lift-ns amap))
|
|
amap (or lift-map amap)
|
|
prefix (if ns (str "#:" ns "{") "{")]
|
|
(pprint-logical-block :prefix prefix :suffix "}"
|
|
(print-length-loop [aseq (seq amap)]
|
|
(when aseq
|
|
;;compiler gets confused with nested macro if it isn't namespaced
|
|
;;it tries to use clojure.pprint/pprint-logical-block for some reason
|
|
(m/pprint-logical-block
|
|
(write-out (ffirst aseq))
|
|
(-write *out* " ")
|
|
(pprint-newline :linear)
|
|
(set! *current-length* 0) ;always print both parts of the [k v] pair
|
|
(write-out (fnext (first aseq))))
|
|
(when (next aseq)
|
|
(-write *out* ", ")
|
|
(pprint-newline :linear)
|
|
(recur (next aseq))))))))
|
|
|
|
(defn- pprint-simple-default [obj]
|
|
;;TODO: Update to handle arrays (?) and suppressing namespaces
|
|
(-write *out* (pr-str obj)))
|
|
|
|
(def pprint-set (formatter-out "~<#{~;~@{~w~^ ~:_~}~;}~:>"))
|
|
|
|
(def ^{:private true}
|
|
type-map {"core$future_call" "Future",
|
|
"core$promise" "Promise"})
|
|
|
|
(defn- map-ref-type
|
|
"Map ugly type names to something simpler"
|
|
[name]
|
|
(or (when-let [match (re-find #"^[^$]+\$[^$]+" name)]
|
|
(type-map match))
|
|
name))
|
|
|
|
(defn- pprint-ideref [o]
|
|
(let [prefix (str "#<" (map-ref-type (.-name (type o)))
|
|
"@" (goog/getUid o) ": ")]
|
|
(pprint-logical-block :prefix prefix :suffix ">"
|
|
(pprint-indent :block (-> (count prefix) (- 2) -))
|
|
(pprint-newline :linear)
|
|
(write-out
|
|
(if (and (satisfies? IPending o) (not (-realized? o)))
|
|
:not-delivered
|
|
@o)))))
|
|
|
|
(def ^{:private true} pprint-pqueue (formatter-out "~<<-(~;~@{~w~^ ~_~}~;)-<~:>"))
|
|
|
|
(defn- type-dispatcher [obj]
|
|
(cond
|
|
(instance? PersistentQueue obj) :queue
|
|
(satisfies? IDeref obj) :deref
|
|
(symbol? obj) :symbol
|
|
(seq? obj) :list
|
|
(map? obj) :map
|
|
(vector? obj) :vector
|
|
(set? obj) :set
|
|
(nil? obj) nil
|
|
:default :default))
|
|
|
|
(defmulti simple-dispatch
|
|
"The pretty print dispatch function for simple data structure format."
|
|
type-dispatcher)
|
|
|
|
(use-method simple-dispatch :list pprint-list)
|
|
(use-method simple-dispatch :vector pprint-vector)
|
|
(use-method simple-dispatch :map pprint-map)
|
|
(use-method simple-dispatch :set pprint-set)
|
|
(use-method simple-dispatch nil #(-write *out* (pr-str nil)))
|
|
(use-method simple-dispatch :default pprint-simple-default)
|
|
|
|
(set-pprint-dispatch simple-dispatch)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Dispatch for the code table
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(declare ^{:arglists '([alis])} pprint-simple-code-list)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Format the namespace ("ns") macro. This is quite complicated because of all the
|
|
;;; different forms supported and because programmers can choose lists or vectors
|
|
;;; in various places.
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- brackets
|
|
"Figure out which kind of brackets to use"
|
|
[form]
|
|
(if (vector? form)
|
|
["[" "]"]
|
|
["(" ")"]))
|
|
|
|
(defn- pprint-ns-reference
|
|
"Pretty print a single reference (import, use, etc.) from a namespace decl"
|
|
[reference]
|
|
(if (sequential? reference)
|
|
(let [[start end] (brackets reference)
|
|
[keyw & args] reference]
|
|
(pprint-logical-block :prefix start :suffix end
|
|
((formatter-out "~w~:i") keyw)
|
|
(loop [args args]
|
|
(when (seq args)
|
|
((formatter-out " "))
|
|
(let [arg (first args)]
|
|
(if (sequential? arg)
|
|
(let [[start end] (brackets arg)]
|
|
(pprint-logical-block :prefix start :suffix end
|
|
(if (and (= (count arg) 3) (keyword? (second arg)))
|
|
(let [[ns kw lis] arg]
|
|
((formatter-out "~w ~w ") ns kw)
|
|
(if (sequential? lis)
|
|
((formatter-out (if (vector? lis)
|
|
"~<[~;~@{~w~^ ~:_~}~;]~:>"
|
|
"~<(~;~@{~w~^ ~:_~}~;)~:>"))
|
|
lis)
|
|
(write-out lis)))
|
|
(apply (formatter-out "~w ~:i~@{~w~^ ~:_~}") arg)))
|
|
(when (next args)
|
|
((formatter-out "~_"))))
|
|
(do
|
|
(write-out arg)
|
|
(when (next args)
|
|
((formatter-out "~:_"))))))
|
|
(recur (next args))))))
|
|
(write-out reference)))
|
|
|
|
(defn- pprint-ns
|
|
"The pretty print dispatch chunk for the ns macro"
|
|
[alis]
|
|
(if (next alis)
|
|
(let [[ns-sym ns-name & stuff] alis
|
|
[doc-str stuff] (if (string? (first stuff))
|
|
[(first stuff) (next stuff)]
|
|
[nil stuff])
|
|
[attr-map references] (if (map? (first stuff))
|
|
[(first stuff) (next stuff)]
|
|
[nil stuff])]
|
|
(pprint-logical-block :prefix "(" :suffix ")"
|
|
((formatter-out "~w ~1I~@_~w") ns-sym ns-name)
|
|
(when (or doc-str attr-map (seq references))
|
|
((formatter-out "~@:_")))
|
|
(when doc-str
|
|
(cl-format true "\"~a\"~:[~;~:@_~]" doc-str (or attr-map (seq references))))
|
|
(when attr-map
|
|
((formatter-out "~w~:[~;~:@_~]") attr-map (seq references)))
|
|
(loop [references references]
|
|
(pprint-ns-reference (first references))
|
|
(when-let [references (next references)]
|
|
(pprint-newline :linear)
|
|
(recur references)))))
|
|
(write-out alis)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Format something that looks like a simple def (sans metadata, since the reader
|
|
;;; won't give it to us now).
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^{:private true} pprint-hold-first (formatter-out "~:<~w~^ ~@_~w~^ ~_~@{~w~^ ~_~}~:>"))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Format something that looks like a defn or defmacro
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;; Format the params and body of a defn with a single arity
|
|
(defn- single-defn [alis has-doc-str?]
|
|
(if (seq alis)
|
|
(do
|
|
(if has-doc-str?
|
|
((formatter-out " ~_"))
|
|
((formatter-out " ~@_")))
|
|
((formatter-out "~{~w~^ ~_~}") alis))))
|
|
|
|
;;; Format the param and body sublists of a defn with multiple arities
|
|
(defn- multi-defn [alis has-doc-str?]
|
|
(if (seq alis)
|
|
((formatter-out " ~_~{~w~^ ~_~}") alis)))
|
|
|
|
;;; TODO: figure out how to support capturing metadata in defns (we might need a
|
|
;;; special reader)
|
|
(defn- pprint-defn [alis]
|
|
(if (next alis)
|
|
(let [[defn-sym defn-name & stuff] alis
|
|
[doc-str stuff] (if (string? (first stuff))
|
|
[(first stuff) (next stuff)]
|
|
[nil stuff])
|
|
[attr-map stuff] (if (map? (first stuff))
|
|
[(first stuff) (next stuff)]
|
|
[nil stuff])]
|
|
(pprint-logical-block :prefix "(" :suffix ")"
|
|
((formatter-out "~w ~1I~@_~w") defn-sym defn-name)
|
|
(if doc-str
|
|
((formatter-out " ~_~w") doc-str))
|
|
(if attr-map
|
|
((formatter-out " ~_~w") attr-map))
|
|
;; Note: the multi-defn case will work OK for malformed defns too
|
|
(cond
|
|
(vector? (first stuff)) (single-defn stuff (or doc-str attr-map))
|
|
:else (multi-defn stuff (or doc-str attr-map)))))
|
|
(pprint-simple-code-list alis)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Format something with a binding form
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- pprint-binding-form [binding-vec]
|
|
(pprint-logical-block :prefix "[" :suffix "]"
|
|
(print-length-loop [binding binding-vec]
|
|
(when (seq binding)
|
|
(pprint-logical-block binding
|
|
(write-out (first binding))
|
|
(when (next binding)
|
|
(-write *out* " ")
|
|
(pprint-newline :miser)
|
|
(write-out (second binding))))
|
|
(when (next (rest binding))
|
|
(-write *out* " ")
|
|
(pprint-newline :linear)
|
|
(recur (next (rest binding))))))))
|
|
|
|
(defn- pprint-let [alis]
|
|
(let [base-sym (first alis)]
|
|
(pprint-logical-block :prefix "(" :suffix ")"
|
|
(if (and (next alis) (vector? (second alis)))
|
|
(do
|
|
((formatter-out "~w ~1I~@_") base-sym)
|
|
(pprint-binding-form (second alis))
|
|
((formatter-out " ~_~{~w~^ ~_~}") (next (rest alis))))
|
|
(pprint-simple-code-list alis)))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Format something that looks like "if"
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^{:private true} pprint-if (formatter-out "~:<~1I~w~^ ~@_~w~@{ ~_~w~}~:>"))
|
|
|
|
(defn- pprint-cond [alis]
|
|
(pprint-logical-block :prefix "(" :suffix ")"
|
|
(pprint-indent :block 1)
|
|
(write-out (first alis))
|
|
(when (next alis)
|
|
(-write *out* " ")
|
|
(pprint-newline :linear)
|
|
(print-length-loop [alis (next alis)]
|
|
(when alis
|
|
(pprint-logical-block alis
|
|
(write-out (first alis))
|
|
(when (next alis)
|
|
(-write *out* " ")
|
|
(pprint-newline :miser)
|
|
(write-out (second alis))))
|
|
(when (next (rest alis))
|
|
(-write *out* " ")
|
|
(pprint-newline :linear)
|
|
(recur (next (rest alis)))))))))
|
|
|
|
(defn- pprint-condp [alis]
|
|
(if (> (count alis) 3)
|
|
(pprint-logical-block :prefix "(" :suffix ")"
|
|
(pprint-indent :block 1)
|
|
(apply (formatter-out "~w ~@_~w ~@_~w ~_") alis)
|
|
(print-length-loop [alis (seq (drop 3 alis))]
|
|
(when alis
|
|
(pprint-logical-block alis
|
|
(write-out (first alis))
|
|
(when (next alis)
|
|
(-write *out* " ")
|
|
(pprint-newline :miser)
|
|
(write-out (second alis))))
|
|
(when (next (rest alis))
|
|
(-write *out* " ")
|
|
(pprint-newline :linear)
|
|
(recur (next (rest alis)))))))
|
|
(pprint-simple-code-list alis)))
|
|
|
|
;;; The map of symbols that are defined in an enclosing #() anonymous function
|
|
(def ^:dynamic ^{:private true} *symbol-map* {})
|
|
|
|
(defn- pprint-anon-func [alis]
|
|
(let [args (second alis)
|
|
nlis (first (rest (rest alis)))]
|
|
(if (vector? args)
|
|
(binding [*symbol-map* (if (= 1 (count args))
|
|
{(first args) "%"}
|
|
(into {}
|
|
(map
|
|
#(vector %1 (str \% %2))
|
|
args
|
|
(range 1 (inc (count args))))))]
|
|
((formatter-out "~<#(~;~@{~w~^ ~_~}~;)~:>") nlis))
|
|
(pprint-simple-code-list alis))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; The master definitions for formatting lists in code (that is, (fn args...) or
|
|
;;; special forms).
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;;; This is the equivalent of (formatter-out "~:<~1I~@{~w~^ ~_~}~:>"), but is
|
|
;;; easier on the stack.
|
|
|
|
(defn- pprint-simple-code-list [alis]
|
|
(pprint-logical-block :prefix "(" :suffix ")"
|
|
(pprint-indent :block 1)
|
|
(print-length-loop [alis (seq alis)]
|
|
(when alis
|
|
(write-out (first alis))
|
|
(when (next alis)
|
|
(-write *out* " ")
|
|
(pprint-newline :linear)
|
|
(recur (next alis)))))))
|
|
|
|
;;; Take a map with symbols as keys and add versions with no namespace.
|
|
;;; That is, if ns/sym->val is in the map, add sym->val to the result.
|
|
(defn- two-forms [amap]
|
|
(into {}
|
|
(mapcat
|
|
identity
|
|
(for [x amap]
|
|
[x [(symbol (name (first x))) (second x)]]))))
|
|
|
|
(defn- add-core-ns [amap]
|
|
(let [core "clojure.core"]
|
|
(into {}
|
|
(map #(let [[s f] %]
|
|
(if (not (or (namespace s) (special-symbol? s)))
|
|
[(symbol core (name s)) f]
|
|
%))
|
|
amap))))
|
|
|
|
(def ^:dynamic ^{:private true} *code-table*
|
|
(two-forms
|
|
(add-core-ns
|
|
{'def pprint-hold-first, 'defonce pprint-hold-first,
|
|
'defn pprint-defn, 'defn- pprint-defn, 'defmacro pprint-defn, 'fn pprint-defn,
|
|
'let pprint-let, 'loop pprint-let, 'binding pprint-let,
|
|
'with-local-vars pprint-let, 'with-open pprint-let, 'when-let pprint-let,
|
|
'if-let pprint-let, 'doseq pprint-let, 'dotimes pprint-let,
|
|
'when-first pprint-let,
|
|
'if pprint-if, 'if-not pprint-if, 'when pprint-if, 'when-not pprint-if,
|
|
'cond pprint-cond, 'condp pprint-condp,
|
|
'fn* pprint-anon-func,
|
|
'. pprint-hold-first, '.. pprint-hold-first, '-> pprint-hold-first,
|
|
'locking pprint-hold-first, 'struct pprint-hold-first,
|
|
'struct-map pprint-hold-first, 'ns pprint-ns
|
|
})))
|
|
|
|
(defn- pprint-code-list [alis]
|
|
(if-not (pprint-reader-macro alis)
|
|
(if-let [special-form (*code-table* (first alis))]
|
|
(special-form alis)
|
|
(pprint-simple-code-list alis))))
|
|
|
|
(defn- pprint-code-symbol [sym]
|
|
(if-let [arg-num (sym *symbol-map*)]
|
|
(print arg-num)
|
|
(if *print-suppress-namespaces*
|
|
(print (name sym))
|
|
(pr sym))))
|
|
|
|
(defmulti
|
|
code-dispatch
|
|
"The pretty print dispatch function for pretty printing Clojure code."
|
|
{:added "1.2" :arglists '[[object]]}
|
|
type-dispatcher)
|
|
|
|
(use-method code-dispatch :list pprint-code-list)
|
|
(use-method code-dispatch :symbol pprint-code-symbol)
|
|
|
|
;; The following are all exact copies of simple-dispatch
|
|
(use-method code-dispatch :vector pprint-vector)
|
|
(use-method code-dispatch :map pprint-map)
|
|
(use-method code-dispatch :set pprint-set)
|
|
(use-method code-dispatch :queue pprint-pqueue)
|
|
(use-method code-dispatch :deref pprint-ideref)
|
|
(use-method code-dispatch nil pr)
|
|
(use-method code-dispatch :default pprint-simple-default)
|
|
|
|
(set-pprint-dispatch simple-dispatch)
|
|
|
|
;;; For testing
|
|
(comment
|
|
|
|
(with-pprint-dispatch code-dispatch
|
|
(pprint
|
|
'(defn cl-format
|
|
"An implementation of a Common Lisp compatible format function"
|
|
[stream format-in & args]
|
|
(let [compiled-format (if (string? format-in) (compile-format format-in) format-in)
|
|
navigator (init-navigator args)]
|
|
(execute-format stream compiled-format navigator)))))
|
|
|
|
(with-pprint-dispatch code-dispatch
|
|
(pprint
|
|
'(defn cl-format
|
|
[stream format-in & args]
|
|
(let [compiled-format (if (string? format-in) (compile-format format-in) format-in)
|
|
navigator (init-navigator args)]
|
|
(execute-format stream compiled-format navigator)))))
|
|
|
|
(with-pprint-dispatch code-dispatch
|
|
(pprint
|
|
'(defn- -write
|
|
([this x]
|
|
(condp = (class x)
|
|
String
|
|
(let [s0 (write-initial-lines this x)
|
|
s (.replaceFirst s0 "\\s+$" "")
|
|
white-space (.substring s0 (count s))
|
|
mode (getf :mode)]
|
|
(if (= mode :writing)
|
|
(dosync
|
|
(write-white-space this)
|
|
(.col_write this s)
|
|
(setf :trailing-white-space white-space))
|
|
(add-to-buffer this (make-buffer-blob s white-space))))
|
|
|
|
Integer
|
|
(let [c ^Character x]
|
|
(if (= (getf :mode) :writing)
|
|
(do
|
|
(write-white-space this)
|
|
(.col_write this x))
|
|
(if (= c (int \newline))
|
|
(write-initial-lines this "\n")
|
|
(add-to-buffer this (make-buffer-blob (str (char c)) nil))))))))))
|
|
|
|
(with-pprint-dispatch code-dispatch
|
|
(pprint
|
|
'(defn pprint-defn [writer alis]
|
|
(if (next alis)
|
|
(let [[defn-sym defn-name & stuff] alis
|
|
[doc-str stuff] (if (string? (first stuff))
|
|
[(first stuff) (next stuff)]
|
|
[nil stuff])
|
|
[attr-map stuff] (if (map? (first stuff))
|
|
[(first stuff) (next stuff)]
|
|
[nil stuff])]
|
|
(pprint-logical-block writer :prefix "(" :suffix ")"
|
|
(cl-format true "~w ~1I~@_~w" defn-sym defn-name)
|
|
(if doc-str
|
|
(cl-format true " ~_~w" doc-str))
|
|
(if attr-map
|
|
(cl-format true " ~_~w" attr-map))
|
|
;; Note: the multi-defn case will work OK for malformed defns too
|
|
(cond
|
|
(vector? (first stuff)) (single-defn stuff (or doc-str attr-map))
|
|
:else (multi-defn stuff (or doc-str attr-map)))))
|
|
(pprint-simple-code-list writer alis)))))
|
|
)
|
|
|
|
;;======================================================================
|
|
;; print_table.clj
|
|
;;======================================================================
|
|
|
|
(defn- add-padding [width s]
|
|
(let [padding (max 0 (- width (count s)))]
|
|
(apply str (clojure.string/join (repeat padding \space)) s)))
|
|
|
|
(defn print-table
|
|
"Prints a collection of maps in a textual table. Prints table headings
|
|
ks, and then a line of output for each row, corresponding to the keys
|
|
in ks. If ks are not specified, use the keys of the first item in rows."
|
|
{:added "1.3"}
|
|
([ks rows]
|
|
(when (seq rows)
|
|
(let [widths (map
|
|
(fn [k]
|
|
(apply max (count (str k)) (map #(count (str (get % k))) rows)))
|
|
ks)
|
|
spacers (map #(apply str (repeat % "-")) widths)
|
|
fmt-row (fn [leader divider trailer row]
|
|
(str leader
|
|
(apply str (interpose divider
|
|
(for [[col width] (map vector (map #(get row %) ks) widths)]
|
|
(add-padding width (str col)))))
|
|
trailer))]
|
|
(cljs.core/println)
|
|
(cljs.core/println (fmt-row "| " " | " " |" (zipmap ks ks)))
|
|
(cljs.core/println (fmt-row "|-" "-+-" "-|" (zipmap ks spacers)))
|
|
(doseq [row rows]
|
|
(cljs.core/println (fmt-row "| " " | " " |" row))))))
|
|
([rows] (print-table (keys (first rows)) rows)))
|