1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
34
35
36
37
38
39 public final class AsciiDoc {
40
41
42
43
44
45
46
47
48
49 public static final Pattern INCLUDE_PATTERN = Pattern.compile("include[:]{2}([^\\[]+)\\[\\]");
50
51
52
53
54 private static final String NAMESPACE_DELIMITER = "_";
55
56
57
58
59
60 private static final Pattern POSIX_FILENAME_REPLACEMENT_PATTERN = Pattern.compile("^[-]|[^A-Za-z0-9._-]");
61
62
63
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
75
76
77
78 public static AsciiDoc asciiDoc() {
79 return new AsciiDoc();
80 }
81
82
83
84
85
86
87
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
99
100
101
102
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
116
117
118
119
120
121 public AsciiDoc newline() {
122 builder.append(NEWLINE);
123 return this;
124 }
125
126
127
128
129
130
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
142
143
144
145
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
168
169
170
171
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
194
195
196
197
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
215
216
217
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
229
230
231
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
243
244
245
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
257
258
259
260
261 public AsciiDoc documentTitle(final String title) {
262 return line(AsciiDocSymbols.DOC_TITLE, title);
263 }
264
265
266
267
268
269
270
271 public AsciiDoc blockTitle(final String title) {
272 return line(AsciiDocSymbols.BLOCK_TITLE, title);
273 }
274
275
276
277
278
279
280
281
282 public AsciiDoc sectionTitle(final String title, final int level) {
283 final AsciiDocSymbols symbol;
284
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
300 }
301
302
303
304
305
306
307
308 public AsciiDoc sectionTitle1(final String title) {
309 return line(AsciiDocSymbols.SECTION_TITLE_1, title);
310 }
311
312
313
314
315
316
317
318 public AsciiDoc sectionTitle2(final String title) {
319 return line(AsciiDocSymbols.SECTION_TITLE_2, title);
320 }
321
322
323
324
325
326
327
328 public AsciiDoc sectionTitle3(final String title) {
329 return line(AsciiDocSymbols.SECTION_TITLE_3, title);
330 }
331
332
333
334
335
336
337
338 public AsciiDoc sectionTitle4(final String title) {
339 return line(AsciiDocSymbols.SECTION_TITLE_4, title);
340 }
341
342
343
344
345
346
347
348 public AsciiDoc sectionTitle5(final String title) {
349 return line(AsciiDocSymbols.SECTION_TITLE_5, title);
350 }
351
352
353
354
355
356
357
358 public AsciiDoc exampleBlock(final String content) {
359 return block(EXAMPLE, content);
360 }
361
362
363
364
365
366
367
368 public AsciiDoc listingBlock(final String content) {
369 return block(LISTING, content);
370 }
371
372
373
374
375
376
377
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
392
393
394
395
396 public AsciiDoc literalBlock(final String content) {
397 return block(LITERAL, content);
398 }
399
400
401
402
403
404
405
406 public AsciiDoc passthroughBlock(final String content) {
407 return block(PASSTHROUGH, content);
408 }
409
410
411
412
413
414
415
416 public AsciiDoc sidebarBlock(final String content) {
417 return block(SIDEBAR, content);
418 }
419
420
421
422
423
424
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
441
442
443
444
445
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
463
464
465
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
482
483
484
485
486
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
504
505
506
507
508 public AsciiDoc unorderedList1(final String content) {
509 line(UNORDERED_LIST_1, content);
510 return this;
511 }
512
513
514
515
516
517
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
530
531
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
545
546
547
548 public AsciiDocTable tableStart() {
549 return new AsciiDocTable(this, builder);
550 }
551
552
553
554
555
556
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
573
574
575
576
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
587
588
589
590
591 @Override
592 public String toString() {
593 return builder.toString();
594 }
595
596
597
598
599
600
601
602
603
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 }