]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/winterwell.markdown/src/winterwell/markdown/pagemodel/MarkdownFormatter.java
Tycho compilation changes for SVN version also.
[simantics/platform.git] / bundles / winterwell.markdown / src / winterwell / markdown / pagemodel / MarkdownFormatter.java
1
2 package winterwell.markdown.pagemodel;
3
4 import java.util.List;
5
6 import winterwell.utils.StrUtils;
7
8 /**
9  * Formats a string that is compatible with the Markdown syntax.
10  * Strings must not include headers.
11  * 
12  * @author Howard Abrams
13  */
14 public class MarkdownFormatter
15 {
16   // Expect everyone to simply use the public static methods...
17   private MarkdownFormatter ()
18   {
19   }
20   
21   /**
22    * Formats a collection of lines to a particular width and honors typical
23    * Markdown syntax and formatting. 
24    * 
25    * The method <i>assumes</i> that if the first line ends with a line
26    * termination character, all the other lines will as well.
27    * 
28    * @param lines     A list of strings that should be formatted and wrapped.
29    * @param lineWidth The width of the page
30    * @return          A string containing each 
31    */
32   public static String format (List<String> lines, int lineWidth)
33   {
34     if (lines == null)
35       return null;      // Should we return an empty string?
36     
37     final String lineEndings;
38     if ( lines.get(0).endsWith ("\r\n") )
39       lineEndings = "\r\n";
40     else if ( lines.get(0).endsWith ("\r") )
41       lineEndings = "\r";
42     else
43       lineEndings = StrUtils.LINEEND;
44     
45     final StringBuilder buf = new StringBuilder();
46     for (String line : lines) {
47       buf.append (line);
48       buf.append (' ');     // We can add extra spaces with impunity, and this
49                             // makes sure our lines don't run together.
50     }
51     return format ( buf.toString(), lineWidth, lineEndings );
52   }
53   
54
55   /**
56    * Formats a string of text. The formatting does line wrapping at the 
57    * <code>lineWidth</code> boundary, but it also honors the formatting
58    * of initial paragraph lines, allowing indentation of the entire
59    * paragraph.
60    * 
61    * @param text       The line of text to format
62    * @param lineWidth  The width of the lines
63    * @return           A string containing the formatted text.
64    */
65   public static String format ( final String text, final int lineWidth)
66   {
67     return format(text, lineWidth, StrUtils.LINEEND);
68   }
69   
70   /**
71    * Formats a string of text. The formatting does line wrapping at the 
72    * <code>lineWidth</code> boundary, but it also honors the formatting
73    * of initial paragraph lines, allowing indentation of the entire
74    * paragraph.
75    * 
76    * @param text       The line of text to format
77    * @param lineWidth  The width of the lines
78    * @param lineEnding The line ending that overrides the default System value
79    * @return           A string containing the formatted text.
80    */
81   public static String format (final String text, final int lineWidth, final String lineEnding)
82   {
83     return new String( format(text.toCharArray (), lineWidth, lineEnding));
84   }
85   
86   /**
87    * The available cursor position states as it sits in the buffer.
88    */
89   private enum StatePosition { 
90     /** The beginning of a paragraph ... the start of the buffer */
91     BEGIN_FIRST_LINE, 
92     
93     /** The beginning of the next line, which may be completely ignored. */
94     BEGIN_OTHER_LINE, 
95     
96     /** The beginning of a new line that will not be ignored, but appended. */
97     BEGIN_NEW_LINE, 
98     
99     /** The middle of a line. */
100     MIDDLE_OF_LINE 
101   }
102
103   /**
104    * The method that does the work of formatting a string of text. The text,
105    * however, is a character array, which is more efficient to work with.
106    * 
107    * TODO: Should we make the format(char[]) method public?
108    * 
109    * @param text       The line of text to format
110    * @param lineWidth  The width of the lines
111    * @param lineEnding The line ending that overrides the default System value
112    * @return           A string containing the formatted text.
113    */
114   static char[] format ( final char[] text, final int lineWidth, final String lineEnding )
115   {
116     final StringBuilder word   = new StringBuilder();
117     final StringBuilder indent = new StringBuilder();
118     final StringBuilder buffer = new StringBuilder(text.length + 10);
119     
120     StatePosition state = StatePosition.BEGIN_FIRST_LINE;
121     int lineLength = 0;
122
123     // There are times when we will run across a character(s) that will 
124     // cause us to stop doing word wrap until we get to the 
125     // "end of non-wordwrap" character(s).
126     //
127     // If this string is set to null, it tells us to "do" word-wrapping.
128     char endWordwrap1 = 0;
129     char endWordwrap2 = 0;
130     
131     // We loop one character past the end of the loop, and when we get to
132     // this position, we assign 'c' to be 0 ... as a marker for the end of
133     // the string...
134     
135     for (int i = 0; i <= text.length; i++)
136     {
137       final char c;
138       if (i < text.length)
139         c = text[i];
140       else
141         c = 0;
142       
143       final char nextChar;
144       if (i+1 < text.length)
145         nextChar = text[i+1];
146       else
147         nextChar = 0;
148       
149       // Are we actually word-wrapping?
150       if (endWordwrap1 != 0) {
151         // Did we get the ending sequence of the non-word-wrap?  
152         if ( ( endWordwrap2 == 0 && c == endWordwrap1 ) || 
153              ( c == endWordwrap1 && nextChar == endWordwrap2 ) )
154           endWordwrap1 = 0;
155         buffer.append (c);
156         lineLength++;
157         
158         if (endWordwrap1 == 0 && endWordwrap2 != 0) {
159           buffer.append (nextChar);
160           lineLength++;
161           i++;
162         }
163         continue;
164       }
165
166       // Check to see if we got one of our special non-word-wrapping
167       // character sequences ...
168       
169       if ( c == '['  ) {                           //    [Hyperlink]
170         endWordwrap1 = ']';
171       }
172       else if ( c == '*' && nextChar == '*' ) {    //    **Bold**
173         endWordwrap1 = '*';
174         endWordwrap2 = '*';
175       }                                            //    *Italics*
176       else if ( c == '*' && state == StatePosition.MIDDLE_OF_LINE ) {
177         endWordwrap1 = '*';
178       }
179       else if ( c == '`' ) {                       //    `code`
180         endWordwrap1 = '`';
181       }
182       else if ( c == '(' && nextChar == '(' ) {    //    ((Footnote))
183         endWordwrap1 = ')';
184         endWordwrap2 = ')';
185       }
186       else if ( c == '!' && nextChar == '[' ) {    //    ![Image]
187         endWordwrap1 = ')';
188       }
189       
190       // We are no longer doing word-wrapping, so tidy the situation up...
191       if (endWordwrap1 != 0) {
192         if (word.length() > 0)
193           lineLength = addWordToBuffer (lineWidth, lineEnding, word, indent, buffer, lineLength);
194         else if (buffer.length() > 0 && buffer.charAt (buffer.length()-1) != ']' )
195           buffer.append(' ');
196         // We are adding an extra space for most situations, unless we get a
197         // [link][ref] where we want them to be together without a space.
198         
199         buffer.append (c);
200         lineLength++;
201         continue;
202       }
203
204       // Normal word-wrapping processing continues ...
205       
206       if (state == StatePosition.BEGIN_FIRST_LINE)
207       {
208         if ( c == '\n' || c == '\r' ) { // Keep, but ignore initial line feeds
209           buffer.append (c);
210           lineLength = 0;
211           continue;
212         }
213
214         if (Character.isWhitespace (c))
215           indent.append (c);
216         else if ( (c == '*' || c == '-' || c == '.' ) &&
217                 Character.isWhitespace (nextChar) )
218           indent.append (' ');
219         else if ( Character.isDigit (c) && nextChar == '.' &&
220                 Character.isWhitespace (text[i+2]))
221           indent.append (' ');
222         else if ( c == '>' )
223           indent.append ('>');
224         else
225           state = StatePosition.MIDDLE_OF_LINE;
226
227         // If we are still in the initial state, then put 'er in...
228         if (state == StatePosition.BEGIN_FIRST_LINE) {
229           buffer.append (c);
230           lineLength++;
231         }
232       }
233       
234       // While it would be more accurate to explicitely state the range of
235       // possibilities, with something like:
236       //    EnumSet.range (StatePosition.BEGIN_OTHER_LINE, StatePosition.MIDDLE_OF_LINE ).contains (state)
237       // We know that what is left is just the BEGIN_FIRST_LINE ...
238       
239       if ( state != StatePosition.BEGIN_FIRST_LINE )
240       {
241         // If not the middle of the line, then it must be at the first of a line
242         // Either   BEGIN_OTHER_LINE  or  BEGIN_NEW_LINE
243         if (state != StatePosition.MIDDLE_OF_LINE)
244         {
245           if ( Character.isWhitespace(c) || c == '>' || c == '.' )
246             word.append (c);
247           else if ( ( ( c == '*' || c == '-' ) && Character.isWhitespace (nextChar) ) ||
248                     ( Character.isDigit(c) && nextChar == '.' && Character.isWhitespace( text[i+2] ) ) ) {
249             word.append (c);
250             state = StatePosition.BEGIN_NEW_LINE;
251           }
252           else {
253             if (state == StatePosition.BEGIN_NEW_LINE) {
254               buffer.append (word);
255               lineLength = word.substring ( word.indexOf("\n")+1 ).length();
256             }
257             word.setLength (0);
258             state = StatePosition.MIDDLE_OF_LINE;
259           }
260         }
261         
262         if (state == StatePosition.MIDDLE_OF_LINE)
263         {
264           // Are we at the end of a word? Then we need to calculate whether
265           // to wrap the line or not.
266           //
267           // This condition does double duty, in that is also serves to
268           // ignore multiple spaces and special characters that may be at
269           // the beginning of the line.
270           if ( Character.isWhitespace(c) || c == 0 ) 
271           {
272             if ( word.length() > 0) {
273               lineLength = addWordToBuffer (lineWidth, lineEnding, word, indent, buffer, lineLength);
274             }
275             // Do we we two spaces at the end of the line? Honor this...
276             else if ( c == ' ' && ( nextChar == '\r' || nextChar == '\n' ) &&
277                     state != StatePosition.BEGIN_OTHER_LINE ) {
278               buffer.append ("  ");
279               buffer.append (lineEnding);
280               lineLength = 0;
281             }
282
283             if ( c == '\r' || c == '\n' ) {
284               state = StatePosition.BEGIN_OTHER_LINE;
285               word.append(c);
286             }
287             
288             // Linefeeds are completely ignored and just treated as whitespace,
289             // unless, of course, there are two of 'em... and of course, end of
290             // lines are simply evil on Windows machines.
291
292             if ( (c == '\n' && nextChar == '\n') ||    // Unix-style line-ends
293                     ( c == '\r' && nextChar == '\n' &&    // Windows-style line-ends
294                             text[i+2] == '\r' && text[i+3] == '\n' )  ) 
295             {
296               state = StatePosition.BEGIN_FIRST_LINE;
297               word.setLength(0);
298               indent.setLength (0);
299               lineLength = 0;
300
301               if (c == '\r') { // If we are dealing with Windows-style line-ends,
302                 i++;           // we need to skip past the next character...
303                 buffer.append("\r\n");
304               } else
305                 buffer.append(c);
306             }
307
308           } else {
309             word.append (c);
310             state = StatePosition.MIDDLE_OF_LINE;
311           }
312         }
313       }
314     }
315     
316     return buffer.toString().toCharArray();
317   }
318
319   /**
320    * Adds a word to the buffer, performing word wrap if necessary.
321    * @param lineWidth    The current width of the line
322    * @param lineEnding   The line ending to append, if necessary
323    * @param word         The word to append
324    * @param indent       The indentation string to insert, if necesary
325    * @param buffer       The buffer to perform all this stuff to
326    * @param lineLength   The current length of the current line
327    * @return             The new length of the current line
328    */
329   private static int addWordToBuffer (final int lineWidth, final String lineEnding, 
330                                       final StringBuilder word, 
331                                       final StringBuilder indent, 
332                                       final StringBuilder buffer, int lineLength)
333   {
334     if ( word.length() + lineLength + 1 > lineWidth )
335     {
336       buffer.append (lineEnding);
337       buffer.append (indent);
338       buffer.append (word);
339
340       lineLength = indent.length() + word.length();
341     }
342     else {
343       if ( lineLength > indent.length() )
344         buffer.append (' ');
345       buffer.append (word);
346       lineLength += word.length() + 1;
347     }
348     word.setLength (0);
349     return lineLength;
350   }
351 }