1 // Copyright (c) 2001, 2002, 2006, 2016  Per M.A. Bothner.
2 // This is free software;  for terms and warranty disclaimer see ./COPYING.
3 
4 package gnu.kawa.functions;
5 import gnu.mapping.*;
6 import gnu.lists.*;
7 import gnu.math.RatNum;
8 import gnu.math.IntNum;
9 import gnu.math.UnsignedPrim;
10 import java.io.*;
11 import gnu.text.Char;
12 import gnu.expr.Keyword;
13 /* #ifdef enable:XML */
14 import gnu.kawa.xml.KNode;
15 import gnu.xml.XMLPrinter;
16 /* #endif */
17 import gnu.kawa.format.AbstractFormat;
18 import gnu.kawa.format.GenericFormat;
19 import gnu.kawa.format.GenericFormat.TryFormatResult;
20 import gnu.kawa.io.CheckConsole;
21 import gnu.kawa.io.OutPort;
22 import gnu.kawa.io.PrettyWriter;
23 import gnu.kawa.xml.XmlNamespace;
24 import gnu.kawa.lispexpr.LispLanguage;
25 import gnu.kawa.format.Printable;
26 /* #ifdef use:java.awt */
27 import gnu.kawa.models.DrawImage;
28 import gnu.kawa.models.DrawShape;
29 import gnu.kawa.models.Picture;
30 import java.awt.Shape;
31 import java.awt.image.BufferedImage;
32 import gnu.kawa.models.SVGUtils;
33 /* #endif */
34 import java.util.List;
35 /* #ifdef use:java.util.regex */
36 import java.util.regex.*;
37 /* #endif */
38 
39 /** Handle formatted output for Lisp-like languages. */
40 
41 public class DisplayFormat extends GenericFormat
42 {
43     public static GenericFormat standardFormat = new GenericFormat();
44     static {
45         Class thisCls = DisplayFormat.class;
46         // NOTE - order is important: The search order is from
47         // most-recently-added to older, so more specific types come later.
48         // Default - java.lang.Object or null
standardFormat.add(thisCls, R)49         standardFormat.add(thisCls, "writeObjectDefault");
50         // Printable or Consumable
standardFormat.add(thisCls, R)51         standardFormat.add(thisCls, "writePrintableConsumable");
52         // gnu.mapping.Values
standardFormat.add(thisCls, R)53         standardFormat.add(thisCls, "writeValues");
54         // gnu.mapping.Symbol
standardFormat.add(thisCls, R)55         standardFormat.add(thisCls, "writeSymbol");
56         // java.lang.Boolean
standardFormat.add(thisCls, R)57         standardFormat.add(thisCls, "writeBoolean");
58         // java.lang.CharValue or gnu.text.Char
standardFormat.add(thisCls, R)59         standardFormat.add(thisCls, "writeChar");
60         // gnu.mapping.Lazy
standardFormat.add(thisCls, R)61         standardFormat.add(thisCls, "writePromise");
62         // java.net.URI
standardFormat.add(thisCls, R)63         standardFormat.add(thisCls, "writeURI");
64         // gnu.lists.Array
standardFormat.add(thisCls, R)65         standardFormat.add(thisCls, "writeArray");
66         // java.util.List
standardFormat.add(thisCls, R)67         standardFormat.add(thisCls, "writeSequence");
68         // gnu.lists.LList
standardFormat.add(thisCls, R)69         standardFormat.add(thisCls, "writeList");
70         // gnu.lists.Range
standardFormat.add(thisCls, R)71         standardFormat.add(thisCls, "writeRange");
72         // java.lang.CharSequence
standardFormat.add(thisCls, R)73         standardFormat.add(thisCls, "writeCharSeq");
74         // gnu.kawa.xml.KNode
75         /* #ifdef enable:XML */
standardFormat.add(thisCls, R)76         standardFormat.add(thisCls, "writeKNode");
77         /* #endif */
78         // RatNum, UnsignedPrim, Long, Integer, Short, Byte, BigInteger
standardFormat.add(thisCls, R)79         standardFormat.add(thisCls, "writeRational");
80         // Picture, Shape, or BufferedImage
81         /* #ifdef use:java.awt */
standardFormat.add(thisCls, R)82         standardFormat.add(thisCls, "writePicture");
83         /* #endif */
84         // java.lang.Enum
standardFormat.add(thisCls, R)85         standardFormat.add(thisCls, "writeEnum");
86         // Java native arrays
standardFormat.add(thisCls, R)87         standardFormat.add(thisCls, "writeJavaArray");
88     }
89 
90   /** Fluid parameter to specify default output base for printing rationals. */
91   public static final ThreadLocation outBase
92     = new ThreadLocation("out-base");
IntNum.ten()93   static { outBase.setGlobal(IntNum.ten()); }
94   /** True if we should print a radix indicator when printing rationals.
95    * The default is no; otherwise we follow Common Lisp conventions. */
96   public static final ThreadLocation outRadix
97     = new ThreadLocation("out-radix");
98 
99     public static final DisplayFormat schemeDisplayFormat
100         = new DisplayFormat(false, 'S');
101 
102     public static final DisplayFormat schemeWriteSimpleFormat
103         = new DisplayFormat(true, 'S');
104     public static final DisplayFormat schemeWriteFormat
105         = new DisplayFormat(true, 'S');
106     public static final DisplayFormat schemeWriteSharedFormat
107         = new DisplayFormat(true, 'S');
108     static {
109         schemeWriteFormat.checkSharing = 0;
110         schemeWriteSharedFormat.checkSharing = 1;
111     }
112 
113     /** Controls whether we check for sharing and cycles.
114      * 1: check for sharing; 0: check for cycles: -1: no checking. */
115     public int checkSharing = -1;
116 
117   /** Create a new instance.
118    * @param readable if output should be formatted so it could be read
119    *   back in again, for example strings shoudl be quoted.
120    * @param language the programming language style to use, where
121    *   'S' is Scheme, 'C' is Common Lisp, and 'E' is Emacs Lisp.
122    */
DisplayFormat(boolean readable, char language)123   public DisplayFormat(boolean readable, char language)
124   {
125     super(standardFormat);
126     this.readable = readable;
127     this.language = language;
128   }
129 
getEmacsLispFormat(boolean readable)130   public static DisplayFormat getEmacsLispFormat(boolean readable)
131   {
132     return new DisplayFormat(readable, 'E');
133   }
134 
getCommonLispFormat(boolean readable)135   public static DisplayFormat getCommonLispFormat(boolean readable)
136   {
137     return new DisplayFormat(readable, 'C');
138   }
139 
getSchemeFormat(boolean readable)140   public static DisplayFormat getSchemeFormat(boolean readable)
141   {
142     return new DisplayFormat(readable, 'S');
143   }
144 
145   boolean readable;
146 
147   /** 'S' is Scheme-style; 'C' is CommonLisp-style;  'E' is Emacs-style.
148    * Note Emacs has its own sub-class gnu.jemacs.lang.Print. */
149   char language;
150 
151   @Override
getReadableOutput()152   public boolean getReadableOutput() { return readable; }
153 
154     @Override
textIsCopied()155     public boolean textIsCopied() { return ! readable; }
156 
157   @Override
writeBoolean(boolean v, Consumer out)158   public void writeBoolean(boolean v, Consumer out)
159   {
160     write (language == 'S' ? (v ? "#t" : "#f") : (v ? "t" : "nil"), out);
161   }
writeBoolean(Object v, AbstractFormat f, Consumer out)162     public static TryFormatResult writeBoolean(Object v, AbstractFormat f, Consumer out) {
163         if (! (v instanceof Boolean))
164             return TryFormatResult.INVALID_CLASS;
165         f.writeBoolean(((Boolean) v).booleanValue(), out);
166         return TryFormatResult.HANDLED;
167     }
168 
writeChar(Object v, AbstractFormat f, Consumer out)169     public static TryFormatResult writeChar(Object v, AbstractFormat f, Consumer out) {
170         boolean readable;
171         char language;
172         if (f instanceof DisplayFormat) {
173             DisplayFormat dformat = (DisplayFormat) f;
174             readable = dformat.readable;
175             language = dformat.language;
176         } else {
177             readable = false;
178             language = 'S';
179         }
180         if (v instanceof Char) {
181              writeChar(((Char) v).intValue(), readable, language, out);
182             return TryFormatResult.HANDLED;
183         } else if (v instanceof Character) {
184             writeChar(((Character) v).charValue(), readable, language, out);
185             return TryFormatResult.HANDLED;
186         }
187         return TryFormatResult.INVALID_CLASS;
188     }
189 
writeRational(Object obj, AbstractFormat format, Consumer out)190     public static TryFormatResult writeRational(Object obj, AbstractFormat format,
191                                                 Consumer out) {
192         if (obj instanceof RatNum
193             || obj instanceof UnsignedPrim
194             || (obj instanceof Number
195                 && (obj instanceof Long
196                     || obj instanceof Integer
197                     || obj instanceof Short
198                     || obj instanceof Byte
199                     || obj instanceof java.math.BigInteger))) {
200             int b = 10;
201             boolean showRadix = false;
202             Object base = outBase.get(null);
203             Object printRadix = outRadix.get(null);
204             if (printRadix != null
205                 && (printRadix == Boolean.TRUE
206                     || "yes".equals(printRadix.toString())))
207                 showRadix = true;
208             if (base instanceof Number)
209                 b = ((IntNum) base).intValue();
210             else if (base != null)
211                 b = Integer.parseInt(base.toString());
212             String asString = Arithmetic.asRatNum(obj).toString(b);
213             if (showRadix) {
214                 if (b == 16)
215                     out.write("#x");
216                 else if (b == 8)
217                     out.write("#o");
218                 else if (b == 2)
219                     out.write("#b");
220                 else if (b != 10 || ! (obj instanceof IntNum))
221                     out.write("#"+base+"r");
222             }
223             out.write(asString);
224             if (showRadix && b == 10 && obj instanceof IntNum)
225                 out.write(".");
226             return TryFormatResult.HANDLED;
227         }
228         return TryFormatResult.INVALID_CLASS;
229     }
230 
writeChar(int v, boolean readable, char language, Consumer out)231   public static void writeChar(int v, boolean readable, char language, Consumer out)
232   {
233     if (! readable)
234       Char.print(v, out);
235     else
236       {
237 	if (language == 'E'
238 	    && v > ' ')
239 	  {
240 	    out.write('?');
241             Char.print(v, out);
242 	  }
243 	// else if (language == 'E') ...
244 	else
245 	  out.write(Char.toScmReadableString(v));
246       }
247   }
248 
249   /**
250    * Format a list.
251    *
252    * Try to find shared structures in a list. To accomplish this, each subobject
253    * is hashed to the idhash, which is used later to determine whether we've seen
254    * a subobject before. There is an added complication when you consider cases
255    * like this:
256    *
257    * '((b . #1=(a . z)) 3)
258    *
259    * It is not known in advance that the printer will have to emit an extra
260    * ')' after the '(a . z) pair. Every time we CDR further into the list, we
261    * push a position marker onto a stack. Once we've examined the tail of this
262    * sublist, we pop all the posn markers off and tell the pretty printer that
263    * it might have to emit an extra ')' if the corresponding posn marker
264    * becomes active.
265    *
266    * @param list The list on which the method CDR's, termination occurs when
267    * this becomes a non-pair or the empty list
268    * @param out The output port that is responsible for the pretty printing
269    */
writeList(Object list, AbstractFormat format, Consumer out)270   public static TryFormatResult writeList(Object list, AbstractFormat format, Consumer out)
271   {
272     if (! (list instanceof LList))
273         return TryFormatResult.INVALID_CLASS;
274     PrettyWriter pout =
275         out instanceof PrintConsumer ? ((PrintConsumer) out).getPrettyWriter()
276         : null;
277     boolean readable = format instanceof DisplayFormat ? ((DisplayFormat) format).readable : false;
278     // The stack of position markers, populated by CDR'ing further into the list.
279     int[] posnStack = null;
280     int checkSharing = format instanceof DisplayFormat ? ((DisplayFormat) format).checkSharing : -1;
281     Object[] tailStack = null;
282     int stackTail = 0;
283     PrintConsumer.startLogicalBlock("(", false, ")", out);
284 
285     while (list instanceof Pair)
286       {
287 	Pair pair = (Pair) list;
288 	format.writeObject(pair.getCar(), out);
289         list = pair.getCdr();
290         if (! readable)
291           list = Promise.force(list);
292         if (list == LList.Empty)
293           break;
294         PrintConsumer.writeSpaceFill(out);
295         if (! (list instanceof Pair))
296 	  {
297 	    out.write(". ");
298 	    format.writeObject(LList.checkNonList(list), (Consumer) out);
299 	    break;
300 	  }
301 	if (pout != null && checkSharing >= 0)
302 	  {
303 
304 	    int hashIndex = pout.IDHashLookup(list);
305 	    int posn = pout.IDHashGetFromIndex(hashIndex);
306 	    if (posn == -1)
307 	      // Then this is a new (sub)object to be marked
308 	      {
309 		// writePositionMarker will return the index to which is was enqueued
310 		posn = pout.writePositionMarker(true);
311 		if (posnStack == null) {
312 		  posnStack = new int[128]; // should be plently for most cases.
313                   tailStack = new Object[128];
314                 }
315 		else if (stackTail >= posnStack.length)
316 		  {
317 		    int[] newPStack = new int[posnStack.length << 1];
318 		    System.arraycopy(posnStack, 0, newPStack, 0, stackTail);
319 		    posnStack = newPStack;
320                     Object[] newTStack = new Object[posnStack.length << 1];
321 		    System.arraycopy(tailStack, 0, newTStack, 0, stackTail);
322 		    tailStack = newTStack;
323 		  }
324 		posnStack[stackTail] = posn;
325                 tailStack[stackTail++] = list;
326 		// Mark (hash) this object
327 		pout.IDHashPutAtIndex(list, posn, hashIndex);
328 	      }
329 	    else
330 	      {
331 		out.write(". ");
332 		pout.writeBreak(PrettyWriter.NEWLINE_FILL);
333 		pout.writeBackReference(posn);
334 		list = LList.Empty;
335 		break;
336 	      }
337 	  }
338       }
339     for (;--stackTail >= 0;) {
340       pout.writePairEnd(posnStack[stackTail]);
341       if (checkSharing == 0)
342           pout.IDHashRemove(tailStack[stackTail]);
343     }
344 
345     PrintConsumer.endLogicalBlock(")", out);
346     return TryFormatResult.HANDLED;
347   }
348 
writeArray(Object value, AbstractFormat format, Consumer out)349     public static TryFormatResult writeArray(Object value, AbstractFormat format,
350                                              Consumer out) {
351         if (! (value instanceof Array))
352             return TryFormatResult.INVALID_CLASS;
353         if (! format.getReadableOutput()
354             && out instanceof OutPort // FIXME PrintConsumer?
355             && ((OutPort) out).atLineStart()
356             && ((OutPort) out).isPrettyPrinting())
357             out.write(ArrayPrint.print(value, null));
358         else
359             write((Array) value, 0, 0, format, out);
360         return TryFormatResult.HANDLED;
361     }
362 
writeRange(Object value, AbstractFormat format, Consumer out)363     public static TryFormatResult writeRange(Object value, AbstractFormat format,
364                                              Consumer out) {
365         if (! (value instanceof Range))
366             return TryFormatResult.INVALID_CLASS;
367         Range range = (Range) value;
368         if (! (format.getReadableOutput() || range.isUnspecifiedStart()))
369             return writeSequence(range, format, out);
370         PrintConsumer.startLogicalBlock("[", false, "]", out);
371         Object rstart = range.getStart();
372         Object rstep = range.getStep();
373         IntNum istep = IntNum.asIntNumOrNull(rstep);
374         IntNum istart = IntNum.asIntNumOrNull(rstart);
375         if (range.isUnspecifiedStart()) {
376             if (istep.isOne())
377                 out.write("<:");
378             else if (istep.isMinusOne())
379                 out.write(">:");
380             else {
381                 out.write("by: ");
382                 format.writeObject(rstep, out);
383             }
384         } else {
385             format.writeObject(rstart, out);
386             int rsize = range.size();
387             if (! range.isUnbounded() && istart != null && istep != null
388                 && (istep.isOne() || istep.isMinusOne())) {
389                 if (istep.isOne()) {
390                     out.write(" <: ");
391                     format.writeObject(IntNum.add(istart, rsize), out);
392                 } else {
393                     out.write(" >: ");
394                     format.writeObject(IntNum.add(istart, -rsize), out);
395                 }
396             } else {
397                 out.write(" by: ");
398                 format.writeObject(rstep, out);
399                 if (! range.isUnbounded()) {
400                     out.write(" size: ");
401                     out.writeInt(rsize);
402                 }
403             }
404         }
405 	PrintConsumer.endLogicalBlock("]", out);
406         return TryFormatResult.HANDLED;
407     }
408 
writeJavaArray(Object value, AbstractFormat format, Consumer out)409     public static TryFormatResult writeJavaArray(Object value, AbstractFormat format,
410                                          Consumer out) {
411         if (value == null || ! value.getClass().isArray())
412             return TryFormatResult.INVALID_CLASS;
413         int len = java.lang.reflect.Array.getLength(value);
414         PrintConsumer.startLogicalBlock("[", false, "]", out);
415         for (int i = 0;  i < len;  i++) {
416             if (i > 0)
417                 PrintConsumer.writeSpaceFill(out);
418             format.writeObject(java.lang.reflect.Array.get(value, i), out);
419         }
420         PrintConsumer.endLogicalBlock("]", out);
421         return TryFormatResult.HANDLED;
422     }
423 
writeSequence(Object value, AbstractFormat format, Consumer out)424     public static TryFormatResult writeSequence(Object value, AbstractFormat format,
425                                         Consumer out) {
426         if (! (value instanceof List))
427             return TryFormatResult.INVALID_CLASS;
428 	List vec = (List) value;
429 	String tag =
430             vec instanceof SimpleVector ? ((SimpleVector) vec).getTag() : null;
431 	String start, end;
432 	if (format instanceof DisplayFormat
433             && ((DisplayFormat) format).language == 'E')
434 	  {
435 	    start = "[";
436 	    end = "]";
437 	  }
438         else if ("b".equals(tag))
439           {
440             start = "#*";
441             end = "";
442           }
443 	else
444 	  {
445 	    start = tag == null ? "#(" : ("#" + tag + "(");
446 	    end = ")";
447 	  }
448         PrintConsumer.startLogicalBlock(start, false, end, out);
449         // Using consumeNext for primtives avoids boxing.
450         // However, for objects we want to recurse
451         if ("b".equals(tag)) {
452             SimpleVector bvec = (SimpleVector) vec;
453             int blen = vec.size();
454             for (int i = 0; i < blen; i++) {
455                 boolean b = bvec.getBooleanRaw(bvec.effectiveIndex(i));
456                 out.write(b ? '1' : '0');
457             }
458         } else if (vec instanceof SimpleVector && tag != null) {
459             int endpos = vec.size() << 1;
460             for (int ipos = 0;  ipos < endpos;  ipos += 2) {
461                 if (ipos > 0)
462                     PrintConsumer.writeSpaceFill(out);
463                 if (! ((SimpleVector) vec).consumeNext(ipos, out))
464                     break;
465                 }
466         } else {
467             boolean first = true;
468             for (Object el : vec) {
469                 if (first)
470                     first = false;
471                 else
472                     PrintConsumer.writeSpaceFill(out);
473                 format.writeObject(el, out);
474             }
475         }
476 	PrintConsumer.endLogicalBlock(end, out);
477         return TryFormatResult.HANDLED;
478     }
479 
writeValues(Object value, AbstractFormat format, Consumer out)480     public static TryFormatResult writeValues(Object value, AbstractFormat format,
481                                               Consumer out) {
482         if (! (value instanceof Values))
483             return TryFormatResult.INVALID_CLASS;
484         if (value == Values.empty && format.getReadableOutput())
485              out.write("#!void");
486         else {
487             Values values = (Values) value;
488             for (int it = 0; (it = values.nextPos(it)) != 0; ) {
489                 Object val = values.getPosPrevious(it);
490                 format.writeObject(val, out);
491             }
492         }
493         return TryFormatResult.HANDLED;
494     }
495 
writePrintableConsumable(Object value, AbstractFormat format, Consumer out)496     public static TryFormatResult writePrintableConsumable
497         (Object value, AbstractFormat format, Consumer out) {
498         if (value instanceof Consumable
499              && (! format.getReadableOutput()
500                  || ! (value instanceof Printable)))
501             ((Consumable) value).consume(out);
502         else if (value instanceof Printable)
503             ((Printable) value).print(out);
504         else
505             return TryFormatResult.INVALID_CLASS;
506         return TryFormatResult.HANDLED;
507     }
508 
writeObject(Object obj, Consumer out)509     public void writeObject(Object obj, Consumer out) {
510         PrettyWriter pout = out instanceof PrintConsumer
511             ? ((PrintConsumer) out).getPrettyWriter()
512             : null;
513         boolean popIDHashNeeded = false;
514         boolean space = false;
515         boolean skip = false;
516         if (out instanceof PrintConsumer
517             && ! (obj instanceof gnu.kawa.xml.UntypedAtomic)
518             && ! (obj instanceof Values)
519             && (getReadableOutput()
520                 || ! (obj instanceof Char || obj instanceof Character
521                       || obj instanceof CharSequence))) {
522             ((PrintConsumer) out).writeWordStart();
523             space = true;
524         }
525         boolean removeNeeded = false;
526         try {
527             if (pout != null && checkSharing >= 0 && isInteresting(obj)) {
528                 popIDHashNeeded = pout.initialiseIDHash();
529                 pout.setSharing(true);
530                 // The value returned from this hash is the respective index in the
531                 // queueInts[] from PrettyWriter to which this object should reference.
532                 int hashIndex = pout.IDHashLookup(obj);
533                 int posn = pout.IDHashGetFromIndex(hashIndex);
534                 if (posn == -1) {
535                     // Find the position in the queueInts that
536                     // future (if any) backreferences should reference
537                     int nposn = pout.writePositionMarker(false);
538                     // Mark (hash) this object
539                     pout.IDHashPutAtIndex(obj, nposn, hashIndex);
540                     removeNeeded = checkSharing == 0;
541                     // Print the object, instead of emitting print-circle notation
542                     skip = false;
543                 } else {
544                     // This object is referring to another part of the expression.
545                     // Activate the referenced position marker
546                     pout.writeBackReference(posn);
547                     // This object is referring to the structure, we shall not
548                     // print it and instead we shall emit print-circle notation.
549                     skip = true;
550                     // Format a fill space after the #N# token
551                     space = true;
552                 }
553             }
554             if (!skip)
555                 super.writeObject(obj, out);
556         } finally {
557             if (removeNeeded)
558                 pout.IDHashRemove(obj);
559             if (space)
560                 ((PrintConsumer) out).writeWordEnd();
561             if (popIDHashNeeded) {
562                 pout.setSharing(true);
563                 pout.finishIDHash();
564             }
565         }
566     }
567 
writeObjectDefault(Object obj, AbstractFormat format, Consumer out)568   public static TryFormatResult writeObjectDefault(Object obj, AbstractFormat format,
569                                            Consumer out)
570   {
571     boolean readable = format.getReadableOutput();
572     char language =
573         format instanceof DisplayFormat ? ((DisplayFormat) format).language
574         : 'S';
575     if (obj instanceof Consumable
576              && (! readable || ! (obj instanceof Printable)))
577       ((Consumable) obj).consume(out);
578     else if (obj instanceof Printable)
579       ((Printable) obj).print(out);
580     else
581       {
582         String asString = obj == null ? null : obj.toString();
583         out.write(asString == null ? "#!null" : asString);
584       }
585     return TryFormatResult.HANDLED;
586   }
587 
588   /** Recursive helper method for writing out Array (sub-) objects.
589    * @param array the Array to write out (part of).
590    * @param index the row-major index to start
591    * @param level the recurssion level, from 0 to array.rank()-1.
592    * @param out the destination
593    */
write(Array array, int index, int level, AbstractFormat format, Consumer out)594   static int write(Array array, int index, int level, AbstractFormat format, Consumer out)
595   {
596     int rank = array.rank();
597     int count = 0;
598     String start;
599     if (level > 0)
600         start = "(";
601     else {
602         boolean printDims = false;
603         int i = rank;
604         while (--i >= 0) {
605             if (array.getLowBound(i) != 0 || array.getSize(i) == 0)
606                 break;
607         }
608         StringBuilder sbuf = new StringBuilder();
609         sbuf.append('#');
610         sbuf.append(rank);
611         String tag = array instanceof GeneralArray
612             ? ((GeneralArray) array).getTag()
613             : null;
614         sbuf.append(tag == null ? 'a' : tag);
615         if (i >= 0) {
616             for (i = 0; i < rank; i++) {
617                 int low = array.getLowBound(i);
618                 if (low != 0) {
619                     sbuf.append('@');
620                     sbuf.append(low);
621                 }
622                 sbuf.append(':');
623                 sbuf.append(array.getSize(i));
624             }
625         }
626         sbuf.append(rank == 0 ? ' ' : '(');
627         start = sbuf.toString();
628     }
629     String end = rank == 0 ? "" : ")";
630     PrintConsumer.startLogicalBlock(start, false, end, out);
631     if (rank > 0)
632       {
633 	int size = array.getSize(level);
634 	level++;
635 	for (int i = 0;  i < size;  i++)
636 	  {
637 	    if (i > 0)
638                 PrintConsumer.writeSpaceFill(out);
639 	    int step;
640 	    if (level == rank)
641 	      {
642 		format.writeObject(array.getRowMajor(index), out);
643 		step = 1;
644 	      }
645 	    else
646                 step = write(array, index, level, format, out);
647 	    index += step;
648 	    count += step;
649 	  }
650       }
651     else
652       format.writeObject(array.getRowMajor(index), out);
653     PrintConsumer.endLogicalBlock(end, out);
654     return count;
655   }
656 
657   /* #ifdef use:java.util.regex */
658   static Pattern r5rsIdentifierMinusInteriorColons =
659     Pattern.compile("(([a-zA-Z]|[!$%&*/:<=>?^_~])"
660                     + "([a-zA-Z]|[!$%&*/<=>?^_~]|[0-9]|([-+.@]))*[:]?)"
661                     + "|([-+]|[.][.][.])");
662   /* #endif */
663 
writeCharSeq(Object value, AbstractFormat format, Consumer out)664     public static TryFormatResult writeCharSeq(Object value, AbstractFormat format, Consumer out) {
665         if (! (value instanceof CharSequence))
666             return TryFormatResult.INVALID_CLASS;
667 	CharSequence str = (CharSequence) value;
668 	if (format.getReadableOutput())
669 	  Strings.printQuoted(str, out, 1);
670         else if (value instanceof String)
671           {
672             out.write(value.toString());
673           }
674 	else if (value instanceof FString || value instanceof IString)
675           { // For some other Consumable types (such as Blob) consume does the wrong thing
676             ((Consumable) value).consume(out);
677           }
678         else
679           {
680             int len = str.length();
681             for (int i = 0; i < len;  i++)
682               out.write(str.charAt(i));
683           }
684         return TryFormatResult.HANDLED;
685     }
686 
writeEnum(Object value, AbstractFormat format, Consumer out)687     public static TryFormatResult writeEnum(Object value, AbstractFormat format, Consumer out) {
688         if (! (value instanceof java.lang.Enum))
689             return TryFormatResult.INVALID_CLASS;
690         if (! format.getReadableOutput())
691             return TryFormatResult.INVALID;
692         out.write(value.getClass().getName());
693         out.write(":");
694         out.write(((java.lang.Enum) value).name());
695         return TryFormatResult.HANDLED;
696     }
697 
writeSymbol(Object value, AbstractFormat format, Consumer out)698     public static TryFormatResult writeSymbol(Object value, AbstractFormat format, Consumer out) {
699         if (! (value instanceof Symbol))
700             return TryFormatResult.INVALID_CLASS;
701         Symbol sym = (Symbol) value;
702         Namespace ns = sym.getNamespace();
703         if (ns == XmlNamespace.HTML) {
704             out.write("html:");
705             out.write(sym.getLocalPart());
706         } else if (ns == LispLanguage.entityNamespace
707                    || ns == LispLanguage.constructNamespace) {
708             out.write(ns.getPrefix());
709             out.write(":");
710             out.write(sym.getLocalPart());
711         } else {
712             String prefix = sym.getPrefix();
713             Namespace namespace = sym.getNamespace();
714             String uri = namespace == null ? null : namespace.getName();
715             boolean readable = format.getReadableOutput();
716             boolean hasUri = uri != null && uri.length() > 0;
717             boolean hasPrefix = prefix != null && prefix.length() > 0;
718             boolean suffixColon = false;
719             if (namespace == Keyword.keywordNamespace)  {
720                 char language = format instanceof DisplayFormat ? ((DisplayFormat) format).language : 'S';
721                 if (language == 'C' || language == 'E')
722                     out.write(':');
723                 else
724                     suffixColon = true;
725             } else if (hasPrefix || hasUri) {
726                 if (hasPrefix)
727                     writeSymbol(prefix, out, readable);
728                 if (hasUri && (readable || ! hasPrefix))
729                 {
730                     out.write('{');
731                     out.write(uri);
732                     out.write('}');
733                 }
734                 out.write(':');
735             }
736             writeSymbol(sym.getName(), out, readable);
737             if (suffixColon)
738                 out.write(':');
739         }
740         return TryFormatResult.HANDLED;
741     }
742 
writeSymbol(String sym, Consumer out, boolean readable)743     static void writeSymbol (String sym, Consumer out, boolean readable) {
744         /* #ifdef use:java.util.regex */
745         /* Use |...| if symbol doesn't follow R5RS conventions
746            for identifiers or has a colon in the interior. */
747         if (readable
748             && ! r5rsIdentifierMinusInteriorColons.matcher(sym).matches()) {
749             int len = sym.length();
750             boolean r7rsStyle = true; // FIXME
751             if (r7rsStyle) {
752                 out.write('|');
753                 for (int i = 0;  i < len;  i++) {
754                     int ch = sym.charAt(i);
755                     if (ch >= 0xD800 && ch <= 0xDBFF) { // surrogates
756                         char next = sym.charAt(++i);
757                         if (next >= 0xDC00 && next <= 0xDFFF)
758                            ch = ((ch - 0xD800) << 10)
759                                + (next - 0xDC00) + 0x10000;
760                     }
761                     if (ch == '|' || ch == '\\' || ch < ' ' || ch == 127) {
762                         out.write('\\');
763                         switch(ch) {
764                         case '\007': out.write('a'); break;
765                         case '\b': out.write('b'); break;
766                         case '\n': out.write('n'); break;
767                         case '\r': out.write('r'); break;
768                         case '\t': out.write('t'); break;
769                         case '\\': out.write('\\'); break;
770                         case '|': out.write('|'); break;
771                         default:
772                             out.write('x');
773                             writeHexDigits(ch, out);
774                             out.write(';');
775                         }
776                     }
777                     else
778                         Char.print(ch, out);
779                 }
780                 out.write('|');
781             } else if (len == 0) {
782                 out.write("||");
783             } else {
784                 boolean inVerticalBars = false;
785                 for (int i = 0;  i < len;  i++) {
786                     char ch = sym.charAt(i);
787                     if (ch == '|') {
788                         out.write(inVerticalBars ? "|\\" : "\\");
789                         inVerticalBars = false;
790                     } else if (! inVerticalBars) {
791                         out.write('|');
792                         inVerticalBars = true;
793                     }
794                     out.write(ch);
795                 }
796                 if (inVerticalBars)
797                     out.write('|');
798             }
799             return;
800         }
801         /* #endif */
802         out.write(sym);
803     }
804 
805     /* #ifdef use:java.awt */
writePicture(Object value, AbstractFormat format, Consumer out)806     public static TryFormatResult writePicture(Object value,
807                                                AbstractFormat format, Consumer out)  {
808         Picture pic = DrawImage.toPictureOrNull(value);
809         if (pic == null)
810             return TryFormatResult.INVALID_CLASS;
811         if (format.getReadableOutput())
812             return TryFormatResult.INVALID;
813         if (! CheckConsole.forDomTerm(out))
814             return writeObjectDefault(value, format, out);
815         out.write("\033]72;"+SVGUtils.toSVG(pic)+"\007");
816         return TryFormatResult.HANDLED;
817     }
818     /* #endif */
819 
820     /* #ifdef enable:XML */
writeKNode(Object value, AbstractFormat format, Consumer out)821     public static TryFormatResult writeKNode(Object value,
822                                      AbstractFormat format, Consumer out) {
823         if (value instanceof KNode) {
824             boolean escapeForDomTerm = false;
825             if (format.getReadableOutput())
826                 out.write("#");
827             else if (CheckConsole.forDomTerm(out)) {
828                 out.write("\033]72;");
829                 escapeForDomTerm = true;
830             }
831             XMLPrinter.make(out, "xhtml").writeObject(value);
832             if (escapeForDomTerm)
833                 out.write("\007");
834             return TryFormatResult.HANDLED;
835         }
836         return TryFormatResult.INVALID_CLASS;
837     }
838     /* #endif */
839 
writePromise(Object value, AbstractFormat format, Consumer out)840     public static TryFormatResult writePromise(Object value,
841                                        AbstractFormat format, Consumer out) {
842         if (! (value instanceof Lazy))
843             return TryFormatResult.INVALID_CLASS;
844         if (format.getReadableOutput())
845             return TryFormatResult.INVALID;
846         format.writeObject(((Lazy) value).getValue(), out);
847         return TryFormatResult.HANDLED;
848     }
writeURI(Object value, AbstractFormat format, Consumer out)849     public static TryFormatResult writeURI(Object value,
850                                    AbstractFormat format, Consumer out) {
851         if (! (value instanceof java.net.URI))
852             return TryFormatResult.INVALID_CLASS;
853         if (! format.getReadableOutput())
854             return TryFormatResult.INVALID;
855         out.write("#,(URI ");
856         Strings.printQuoted(value.toString(), out, 1);
857         out.write(')');
858         return TryFormatResult.HANDLED;
859     }
860 
writeHexDigits(int i, Consumer out)861     static void writeHexDigits(int i, Consumer out) {
862         int high = i >>> 4;
863         if (high != 0) {
864             writeHexDigits(high, out);
865             i &= 15;
866 	}
867         out.write("0123456789ABCDEF".charAt(i));
868     }
869 
870   /**
871    * An "interesting" object is one where object identity is significant.
872    *
873    * Examples are vectors and lists.
874    * No immutable values (for example symbols or numbers) are interesting
875    * @param obj The object under test
876    * @return true if this object is considered interesting
877    */
isInteresting(Object obj)878   private boolean isInteresting (Object obj)
879   {
880     // FIXME Should probably consider empty lists and vectors, as well as FStrings
881     return obj instanceof Pair || obj instanceof SimpleVector;
882   }
883 
884 }
885