View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.api.markup.asciidoc;
18  
19  import static org.forgerock.api.markup.asciidoc.AsciiDocSymbols.*;
20  import static org.forgerock.api.util.ValidationUtil.containsWhitespace;
21  import static org.forgerock.api.util.ValidationUtil.isEmpty;
22  import static java.nio.charset.StandardCharsets.UTF_8;
23  import static org.forgerock.util.Reject.checkNotNull;
24  
25  import java.io.IOException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.util.Locale;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  /**
33   * Root builder for AsciiDoc markup. All operations may be applied at the current linear position within the
34   * document being built, such that markup that must appear at the top should be added first.
35   * <p>
36   * This class is not thread-safe.
37   * </p>
38   */
39  public final class AsciiDoc {
40  
41      // TODO http://asciidoctor.org/docs/user-manual/#preventing-substitutions
42  
43      /**
44       * Regex for finding <a href="http://asciidoctor.org/docs/user-manual/#include-directive">Include</a>-directives,
45       * where group 1 contains the path-value.
46       *
47       * @see #include(String...)
48       */
49      public static final Pattern INCLUDE_PATTERN = Pattern.compile("include[:]{2}([^\\[]+)\\[\\]");
50  
51      /**
52       * Underscore-character is used as the namespace-part delimiter.
53       */
54      private static final String NAMESPACE_DELIMITER = "_";
55  
56      /**
57       * Characters that must be replaced/removed for a filename to be a POSIX "Fully portable filename"
58       * <a href="https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words">[ref]</a>.
59       */
60      private static final Pattern POSIX_FILENAME_REPLACEMENT_PATTERN = Pattern.compile("^[-]|[^A-Za-z0-9._-]");
61  
62      /**
63       * Pattern for replacing multiple underscores with a single underscore.
64       */
65      private static final Pattern SQUASH_UNDERSCORES_PATTERN = Pattern.compile("[_]{2,}");
66  
67      private final StringBuilder builder;
68  
69      private AsciiDoc() {
70          builder = new StringBuilder();
71      }
72  
73      /**
74       * Creates a new builder instance.
75       *
76       * @return builder
77       */
78      public static AsciiDoc asciiDoc() {
79          return new AsciiDoc();
80      }
81  
82      /**
83       * Prefixes the given line-of-content with an AsciiDoc symbol.
84       *
85       * @param symbol AsciiDoc symbol
86       * @param content Content
87       * @return Doc builder
88       */
89      private AsciiDoc line(final AsciiDocSymbols symbol, final String content) {
90          if (isEmpty(content)) {
91              throw new AsciiDocException("content required");
92          }
93          builder.append(NEWLINE).append(checkNotNull(symbol)).append(content).append(NEWLINE);
94          return this;
95      }
96  
97      /**
98       * Surrounds the given content-block with block symbols.
99       *
100      * @param symbol AsciiDoc symbol
101      * @param content Content
102      * @return Doc builder
103      */
104     private AsciiDoc block(final AsciiDocSymbols symbol, final String content) {
105         if (isEmpty(content)) {
106             throw new AsciiDocException("content required");
107         }
108         builder.append(checkNotNull(symbol)).append(NEWLINE)
109                 .append(content).append(NEWLINE)
110                 .append(checkNotNull(symbol)).append(NEWLINE);
111         return this;
112     }
113 
114     /**
115      * Inserts a UNIX newline character, where two adjacent newlines will create a new
116      * <a href="http://asciidoctor.org/docs/user-manual/#paragraph">paragraph</a>.
117      * As a best-practice, they suggest one-sentence-per-line style.
118      *
119      * @return builder
120      */
121     public AsciiDoc newline() {
122         builder.append(NEWLINE);
123         return this;
124     }
125 
126     /**
127      * Inserts raw text (may contain markup or only whitespace).
128      *
129      * @param text Raw text/markup
130      * @return builder
131      */
132     public AsciiDoc rawText(final String text) {
133         if (text == null) {
134             throw new AsciiDocException("text required");
135         }
136         builder.append(text);
137         return this;
138     }
139 
140     /**
141      * Inserts raw line (may contain markup), and will insert one newline-characters above and below, if those
142      * newlines do not already exist.
143      *
144      * @param text Raw text/markup
145      * @return builder
146      */
147     public AsciiDoc rawLine(final String text) {
148         if (isEmpty(text)) {
149             throw new AsciiDocException("text required");
150         }
151 
152         final int newlinesAbove = requireTrailingNewlines(1, builder);
153         if (newlinesAbove == 1) {
154             builder.append(NEWLINE);
155         }
156 
157         builder.append(text);
158 
159         final int newlinesBelow = requireTrailingNewlines(1, text);
160         if (newlinesBelow == 1) {
161             builder.append(NEWLINE);
162         }
163         return this;
164     }
165 
166     /**
167      * Inserts raw paragraph (may contain markup), and will insert two newline-characters above and below, if those
168      * newlines do not already exist [<a href="http://asciidoctor.org/docs/user-manual/#paragraph">ref</a>].
169      *
170      * @param text Raw text/markup
171      * @return builder
172      */
173     public AsciiDoc rawParagraph(final String text) {
174         if (isEmpty(text)) {
175             throw new AsciiDocException("text required");
176         }
177 
178         int newlinesAbove = requireTrailingNewlines(2, builder);
179         while (--newlinesAbove > -1) {
180             builder.append(NEWLINE);
181         }
182 
183         builder.append(text);
184 
185         int newlinesBelow = requireTrailingNewlines(2, text);
186         while (--newlinesBelow > -1) {
187             builder.append(NEWLINE);
188         }
189         return this;
190     }
191 
192     /**
193      * Checks for a minimum-number of trailing newline-characters.
194      *
195      * @param newlines Minimum number of required, trailing newline-characters (1 or higher)
196      * @param text Text to check
197      * @return Number of newline-characters that need to be added to the end of the text
198      */
199     private static int requireTrailingNewlines(int newlines, final CharSequence text) {
200         if (newlines < 0) {
201             throw new IllegalArgumentException("newlines must be positive");
202         }
203         for (int i = 0; newlines > 0; ++i) {
204             if (text.length() > i && text.charAt(text.length() - (i + 1)) == '\n') {
205                 --newlines;
206             } else {
207                 break;
208             }
209         }
210         return newlines;
211     }
212 
213     /**
214      * Inserts bold text.
215      *
216      * @param text Text to make bold
217      * @return builder
218      */
219     public AsciiDoc boldText(final String text) {
220         if (isEmpty(text)) {
221             throw new AsciiDocException("text required");
222         }
223         builder.append(BOLD).append(text).append(BOLD);
224         return this;
225     }
226 
227     /**
228      * Inserts italic text.
229      *
230      * @param text Text to make bold
231      * @return Doc builder
232      */
233     public AsciiDoc italic(final String text) {
234         if (isEmpty(text)) {
235             throw new AsciiDocException("text required");
236         }
237         builder.append(ITALIC).append(text).append(ITALIC);
238         return this;
239     }
240 
241     /**
242      * Inserts monospaced (e.g., code) text.
243      *
244      * @param text Text to make monospaced
245      * @return Doc builder
246      */
247     public AsciiDoc mono(final String text) {
248         if (isEmpty(text)) {
249             throw new AsciiDocException("text required");
250         }
251         builder.append(MONO).append(text).append(MONO);
252         return this;
253     }
254 
255     /**
256      * Inserts a document title.
257      *
258      * @param title Document title
259      * @return Doc builder
260      */
261     public AsciiDoc documentTitle(final String title) {
262         return line(AsciiDocSymbols.DOC_TITLE, title);
263     }
264 
265     /**
266      * Inserts a block title.
267      *
268      * @param title Block title
269      * @return Doc builder
270      */
271     public AsciiDoc blockTitle(final String title) {
272         return line(AsciiDocSymbols.BLOCK_TITLE, title);
273     }
274 
275     /**
276      * Inserts a section title, at a given level.
277      *
278      * @param title Section title
279      * @param level Section level [1-5]
280      * @return Doc builder
281      */
282     public AsciiDoc sectionTitle(final String title, final int level) {
283         final AsciiDocSymbols symbol;
284         // @Checkstyle:off
285         switch (level) {
286             case 1:
287                 return line(AsciiDocSymbols.SECTION_TITLE_1, title);
288             case 2:
289                 return line(AsciiDocSymbols.SECTION_TITLE_2, title);
290             case 3:
291                 return line(AsciiDocSymbols.SECTION_TITLE_3, title);
292             case 4:
293                 return line(AsciiDocSymbols.SECTION_TITLE_4, title);
294             case 5:
295                 return line(AsciiDocSymbols.SECTION_TITLE_5, title);
296             default:
297                 throw new AsciiDocException("Unsupported section-level: " + level);
298         }
299         // @Checkstyle:on
300     }
301 
302     /**
303      * Inserts a section title, level 1.
304      *
305      * @param title Section title
306      * @return Doc builder
307      */
308     public AsciiDoc sectionTitle1(final String title) {
309         return line(AsciiDocSymbols.SECTION_TITLE_1, title);
310     }
311 
312     /**
313      * Inserts a section title, level 2.
314      *
315      * @param title Section title
316      * @return Doc builder
317      */
318     public AsciiDoc sectionTitle2(final String title) {
319         return line(AsciiDocSymbols.SECTION_TITLE_2, title);
320     }
321 
322     /**
323      * Inserts a section title, level 3.
324      *
325      * @param title Section title
326      * @return Doc builder
327      */
328     public AsciiDoc sectionTitle3(final String title) {
329         return line(AsciiDocSymbols.SECTION_TITLE_3, title);
330     }
331 
332     /**
333      * Inserts a section title, level 4.
334      *
335      * @param title Section title
336      * @return Doc builder
337      */
338     public AsciiDoc sectionTitle4(final String title) {
339         return line(AsciiDocSymbols.SECTION_TITLE_4, title);
340     }
341 
342     /**
343      * Inserts a section title, level 5.
344      *
345      * @param title Section title
346      * @return Doc builder
347      */
348     public AsciiDoc sectionTitle5(final String title) {
349         return line(AsciiDocSymbols.SECTION_TITLE_5, title);
350     }
351 
352     /**
353      * Inserts an example-block.
354      *
355      * @param content Content
356      * @return Doc builder
357      */
358     public AsciiDoc exampleBlock(final String content) {
359         return block(EXAMPLE, content);
360     }
361 
362     /**
363      * Inserts a listing-block.
364      *
365      * @param content Content
366      * @return Doc builder
367      */
368     public AsciiDoc listingBlock(final String content) {
369         return block(LISTING, content);
370     }
371 
372     /**
373      * Inserts a listing-block, with the source-code type (e.g., java, json, etc.) noted for formatting purposes.
374      *
375      * @param content Content
376      * @param sourceType Type of source-code in the listing
377      * @return Doc builder
378      */
379     public AsciiDoc listingBlock(final String content, final String sourceType) {
380         if (isEmpty(content) || isEmpty(sourceType)) {
381             throw new AsciiDocException("content and sourceType required");
382         }
383         builder.append("[source,")
384                 .append(sourceType)
385                 .append("]")
386                 .append(NEWLINE);
387         return block(LISTING, content);
388     }
389 
390     /**
391      * Inserts a literal-block.
392      *
393      * @param content Content
394      * @return Doc builder
395      */
396     public AsciiDoc literalBlock(final String content) {
397         return block(LITERAL, content);
398     }
399 
400     /**
401      * Inserts a pass-through-block.
402      *
403      * @param content Content
404      * @return Doc builder
405      */
406     public AsciiDoc passthroughBlock(final String content) {
407         return block(PASSTHROUGH, content);
408     }
409 
410     /**
411      * Inserts a sidebar-block.
412      *
413      * @param content Content
414      * @return Doc builder
415      */
416     public AsciiDoc sidebarBlock(final String content) {
417         return block(SIDEBAR, content);
418     }
419 
420     /**
421      * Inserts a cross-reference anchor.
422      *
423      * @param id Anchor ID
424      * @return Doc builder
425      */
426     public AsciiDoc anchor(final String id) {
427         if (isEmpty(id)) {
428             throw new AsciiDocException("id required");
429         }
430         if (containsWhitespace(id)) {
431             throw new AsciiDocException("id contains whitespace");
432         }
433         builder.append(AsciiDocSymbols.ANCHOR_START)
434                 .append(id)
435                 .append(AsciiDocSymbols.ANCHOR_END);
436         return this;
437     }
438 
439     /**
440      * Inserts a cross-reference anchor, with a custom
441      * <a href="http://asciidoctor.org/docs/user-manual/#anchordef">xreflabel</a>.
442      *
443      * @param id Anchor ID
444      * @param xreflabel Custom cross-reference link
445      * @return Doc builder
446      */
447     public AsciiDoc anchor(final String id, final String xreflabel) {
448         if (isEmpty(id) || isEmpty(xreflabel)) {
449             throw new AsciiDocException("id and xreflabel required");
450         }
451         if (containsWhitespace(id)) {
452             throw new AsciiDocException("id contains whitespace");
453         }
454         builder.append(AsciiDocSymbols.ANCHOR_START)
455                 .append(id)
456                 .append(',').append(xreflabel)
457                 .append(AsciiDocSymbols.ANCHOR_END);
458         return this;
459     }
460 
461     /**
462      * Inserts a cross-reference link.
463      *
464      * @param anchorId Anchor ID
465      * @return Doc builder
466      */
467     public AsciiDoc link(final String anchorId) {
468         if (isEmpty(anchorId)) {
469             throw new AsciiDocException("anchorId required");
470         }
471         if (containsWhitespace(anchorId)) {
472             throw new AsciiDocException("anchorId contains whitespace");
473         }
474         builder.append(AsciiDocSymbols.CROSS_REF_START)
475                 .append(anchorId)
476                 .append(AsciiDocSymbols.CROSS_REF_END);
477         return this;
478     }
479 
480     /**
481      * Inserts a cross-reference link, with a custom
482      * <a href="http://asciidoctor.org/docs/user-manual/#anchordef">xreflabel</a>.
483      *
484      * @param anchorId Anchor ID
485      * @param xreflabel Custom cross-reference link
486      * @return Doc builder
487      */
488     public AsciiDoc link(final String anchorId, final String xreflabel) {
489         if (isEmpty(anchorId) || isEmpty(xreflabel)) {
490             throw new AsciiDocException("anchorId and xreflabel required");
491         }
492         if (containsWhitespace(anchorId)) {
493             throw new AsciiDocException("anchorId contains whitespace");
494         }
495         builder.append(AsciiDocSymbols.CROSS_REF_START)
496                 .append(anchorId)
497                 .append(',').append(xreflabel)
498                 .append(AsciiDocSymbols.CROSS_REF_END);
499         return this;
500     }
501 
502     /**
503      * Inserts a line for an unordered list, at level 1 indentation.
504      *
505      * @param content Line of content
506      * @return Doc builder
507      */
508     public AsciiDoc unorderedList1(final String content) {
509         line(UNORDERED_LIST_1, content);
510         return this;
511     }
512 
513     /**
514      * Inserts a <a href="http://asciidoctor.org/docs/user-manual/#complex-list-content">list-continuation</a>,
515      * for adding complex formatted content to a list.
516      *
517      * @return Doc builder
518      */
519     public AsciiDoc listContinuation() {
520         final int newlinesAbove = requireTrailingNewlines(1, builder);
521         if (newlinesAbove == 1) {
522             builder.append(NEWLINE);
523         }
524         builder.append(LIST_CONTINUATION);
525         return this;
526     }
527 
528     /**
529      * Inserts a horizontal-rule divider.
530      *
531      * @return Doc builder
532      */
533     public AsciiDoc horizontalRule() {
534         final int newlinesAbove = requireTrailingNewlines(1, builder);
535         if (newlinesAbove == 1) {
536             builder.append(NEWLINE);
537         }
538         builder.append(HORIZONTAL_RULE)
539             .append(NEWLINE);
540         return this;
541     }
542 
543     /**
544      * Starts a table at the current position.
545      *
546      * @return Table builder
547      */
548     public AsciiDocTable tableStart() {
549         return new AsciiDocTable(this, builder);
550     }
551 
552     /**
553      * Inserts an include-directive, given a relative path to a file.
554      *
555      * @param path Relative path segments
556      * @return Doc builder
557      */
558     public AsciiDoc include(final String... path) {
559         if (isEmpty(path)) {
560             throw new AsciiDocException("path required");
561         }
562         builder.append(INCLUDE);
563         builder.append(path[0]);
564         for (int i = 1; i < path.length; ++i) {
565             builder.append('/').append(path[i]);
566         }
567         builder.append("[]").append(NEWLINE);
568         return this;
569     }
570 
571     /**
572      * Saves builder content to a file.
573      *
574      * @param outputDirPath Output directory
575      * @param filename Filename
576      * @throws IOException When error occurs while saving.
577      */
578     public void toFile(final Path outputDirPath, final String filename) throws IOException {
579         final Path filePath = outputDirPath.resolve(filename);
580         Files.createDirectories(outputDirPath);
581         Files.createFile(filePath);
582         Files.write(filePath, toString().getBytes(UTF_8));
583     }
584 
585     /**
586      * Converts builder content to a {@code String}.
587      * <p>
588      *
589      * @return Doc builder content
590      */
591     @Override
592     public String toString() {
593         return builder.toString();
594     }
595 
596     /**
597      * Normalizes a name such that it can be used as a unique
598      * <a href="https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words">filename</a>
599      * and/or anchor in AsciiDoc. Names are converted to lower-case, unsupported characters are collapsed to a single
600      * underscore-character, and parts are separated by an underscore.
601      *
602      * @param parts Name-parts to normalize
603      * @return Normalized name
604      */
605     public static String normalizeName(final String... parts) {
606         if (isEmpty(parts)) {
607             throw new AsciiDocException("parts required");
608         }
609         String s = parts[0].toLowerCase(Locale.ROOT);
610         for (int i = 1; i < parts.length; ++i) {
611             s += NAMESPACE_DELIMITER + parts[i].toLowerCase(Locale.ROOT);
612         }
613         final String normalized;
614         final Matcher m = POSIX_FILENAME_REPLACEMENT_PATTERN.matcher(s);
615         if (m.find()) {
616             normalized = m.replaceAll(NAMESPACE_DELIMITER);
617         } else {
618             normalized = s;
619         }
620         final Matcher mm = SQUASH_UNDERSCORES_PATTERN.matcher(normalized);
621         return mm.find() ? mm.replaceAll(NAMESPACE_DELIMITER) : normalized;
622     }
623 }