diff --git a/src/main/java/io/github/robwin/markup/builder/AbstractMarkupDocBuilder.java b/src/main/java/io/github/robwin/markup/builder/AbstractMarkupDocBuilder.java index d33e0380..985d58d1 100644 --- a/src/main/java/io/github/robwin/markup/builder/AbstractMarkupDocBuilder.java +++ b/src/main/java/io/github/robwin/markup/builder/AbstractMarkupDocBuilder.java @@ -23,13 +23,16 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; +import java.io.Reader; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; import java.text.Normalizer; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.apache.commons.lang3.StringUtils.defaultString; @@ -47,6 +50,7 @@ public abstract class AbstractMarkupDocBuilder implements MarkupDocBuilder { private static final Pattern ANCHOR_UNIGNORABLE_PATTERN = Pattern.compile("[^0-9a-zA-Z-_]+"); private static final Pattern ANCHOR_IGNORABLE_PATTERN = Pattern.compile("[\\s@#&(){}\\[\\]!$*%+=/:.;,?\\\\<>|]+"); private static final String ANCHOR_SEPARATION_CHARACTERS = "_-"; + private static final int MAX_TITLE_LEVEL = 4; protected StringBuilder documentBuilder = new StringBuilder(); protected String newLine = System.getProperty("line.separator"); @@ -265,6 +269,46 @@ public abstract class AbstractMarkupDocBuilder implements MarkupDocBuilder { return this; } + @Override + public MarkupDocBuilder importMarkup(Reader markupText) throws IOException { + return importMarkup(markupText, 0); + } + + protected void importMarkup(Markup titlePrefix, Reader markupText, int levelOffset) throws IOException { + if (levelOffset > MAX_TITLE_LEVEL) + throw new IllegalArgumentException(String.format("Specified levelOffset (%d) > max levelOffset (%d)", levelOffset, MAX_TITLE_LEVEL)); + if (levelOffset < -MAX_TITLE_LEVEL) + throw new IllegalArgumentException(String.format("Specified levelOffset (%d) < min levelOffset (%d)", levelOffset, -MAX_TITLE_LEVEL)); + + final Pattern titlePattern = Pattern.compile(String.format("^(%s{1,%d})\\s+(.*)$", titlePrefix, MAX_TITLE_LEVEL + 1)); + + StringBuffer leveledText = new StringBuffer(); + try (BufferedReader bufferedReader = new BufferedReader(markupText)) { + String readLine; + while ((readLine = bufferedReader.readLine()) != null) { + Matcher titleMatcher = titlePattern.matcher(readLine); + + while (titleMatcher.find()) { + int titleLevel = titleMatcher.group(1).length() - 1; + String title = titleMatcher.group(2); + + if (titleLevel + levelOffset > MAX_TITLE_LEVEL) + throw new IllegalArgumentException(String.format("Specified levelOffset (%d) set title '%s' level (%d) > max title level (%d)", levelOffset, title, titleLevel, MAX_TITLE_LEVEL)); + if (titleLevel + levelOffset < 0) + throw new IllegalArgumentException(String.format("Specified levelOffset (%d) set title '%s' level (%d) < 0", levelOffset, title, titleLevel)); + else + titleMatcher.appendReplacement(leveledText, StringUtils.repeat(titlePrefix.toString(), 1 + titleLevel + levelOffset) + " " + title); + } + titleMatcher.appendTail(leveledText); + leveledText.append(newLine); + } + } + + documentBuilder.append(newLine); + documentBuilder.append(leveledText.toString()); + documentBuilder.append(newLine); + } + @Override public MarkupDocBuilder table(List> cells) { return tableWithColumnSpecs(null, cells); @@ -279,6 +323,9 @@ public abstract class AbstractMarkupDocBuilder implements MarkupDocBuilder { return fileName + "." + markup; } + /** + * 2 newLines are needed at the end of file for file to be included without protection. + */ @Override public void writeToFileWithoutExtension(String directory, String fileName, Charset charset) throws IOException { Files.createDirectories(Paths.get(directory)); diff --git a/src/main/java/io/github/robwin/markup/builder/MarkupDocBuilder.java b/src/main/java/io/github/robwin/markup/builder/MarkupDocBuilder.java index b885988d..90fa6e80 100644 --- a/src/main/java/io/github/robwin/markup/builder/MarkupDocBuilder.java +++ b/src/main/java/io/github/robwin/markup/builder/MarkupDocBuilder.java @@ -19,6 +19,7 @@ package io.github.robwin.markup.builder; import java.io.IOException; +import java.io.Reader; import java.nio.charset.Charset; import java.util.List; @@ -386,6 +387,29 @@ public interface MarkupDocBuilder { */ MarkupDocBuilder newLine(boolean forceLineBreak); + /** + * Import some markup text into this builder.
+ * This is an alias for {@link #importMarkup(Reader, int) importMarkup(markupText, 0)}. + * Newlines are normalized in the process. + * + * @param markupText markup reader to read data from + * @return this builder + * @throws IOException if I/O error occurs while reading {@code markupText} + */ + MarkupDocBuilder importMarkup(Reader markupText) throws IOException; + + /** + * Import some markup text into this builder. + * Newlines are normalized in the process. + * + * @param markupText markup reader to read data from + * @param levelOffset adapt section leveling by adding {@code levelOffset} [0-5] + * @return this builder + * @throws IllegalArgumentException if levelOffset is too high for the imported markup + * @throws IOException if I/O error occurs while reading {@code markupText} + */ + MarkupDocBuilder importMarkup(Reader markupText, int levelOffset) throws IOException; + /** * Returns a string representation of the document. */ diff --git a/src/main/java/io/github/robwin/markup/builder/MarkupTableColumn.java b/src/main/java/io/github/robwin/markup/builder/MarkupTableColumn.java index 7de228d6..5a041574 100644 --- a/src/main/java/io/github/robwin/markup/builder/MarkupTableColumn.java +++ b/src/main/java/io/github/robwin/markup/builder/MarkupTableColumn.java @@ -15,7 +15,8 @@ public class MarkupTableColumn { } /** - * Header constructor + * Header constructor. + * * @param header header name */ public MarkupTableColumn(String header) { @@ -23,7 +24,8 @@ public class MarkupTableColumn { } /** - * Header and specifiers constructor + * Header and specifiers constructor. + * * @param header header name * @param widthRatio width ratio */ @@ -33,7 +35,8 @@ public class MarkupTableColumn { } /** - * Set header name for this column + * Set header name for this column. + * * @param header header name * @return this builder */ @@ -43,7 +46,9 @@ public class MarkupTableColumn { } /** - * Set column width ratio for this column + * Set column width ratio for this column.
+ * Limited support : Markdown does not support column width specifiers and will ignore {@code widthRatio}. + * * @param widthRatio width ratio integer value [0-100]. Accept relative width specifiers [0-9] for languages supporting it. * @return this builder */ @@ -53,9 +58,10 @@ public class MarkupTableColumn { } /** - * Overrides all other specifiers (for the specified language) with this language-dependent {@code specifiers} string + * Overrides all other specifiers (for the specified language) with this language-dependent {@code specifiers} string. + * * @param language apply the {@code specifiers} to this language only - * @param specifiers RAW language-dependent specifiers for the column + * @param specifiers RAW language-dependent specifiers string for the column * @return this builder */ public MarkupTableColumn withMarkupSpecifiers(MarkupLanguage language, String specifiers) { diff --git a/src/main/java/io/github/robwin/markup/builder/asciidoc/AsciiDoc.java b/src/main/java/io/github/robwin/markup/builder/asciidoc/AsciiDoc.java index 590c7bbe..9a297ac8 100644 --- a/src/main/java/io/github/robwin/markup/builder/asciidoc/AsciiDoc.java +++ b/src/main/java/io/github/robwin/markup/builder/asciidoc/AsciiDoc.java @@ -31,6 +31,7 @@ public enum AsciiDoc implements Markup { TABLE_COLUMN_DELIMITER_ESCAPE("\\|"), // AsciiDoctor supports both \| and {vbar} LISTING("----"), HARDBREAKS("[%hardbreaks]"), + TITLE("="), DOCUMENT_TITLE("= "), SECTION_TITLE_LEVEL1("== "), SECTION_TITLE_LEVEL2("=== "), diff --git a/src/main/java/io/github/robwin/markup/builder/asciidoc/AsciiDocBuilder.java b/src/main/java/io/github/robwin/markup/builder/asciidoc/AsciiDocBuilder.java index 3a8e5bb3..418dee57 100644 --- a/src/main/java/io/github/robwin/markup/builder/asciidoc/AsciiDocBuilder.java +++ b/src/main/java/io/github/robwin/markup/builder/asciidoc/AsciiDocBuilder.java @@ -27,6 +27,8 @@ import io.github.robwin.markup.builder.MarkupTableColumn; import org.apache.commons.collections.CollectionUtils; import java.io.File; +import java.io.IOException; +import java.io.Reader; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -254,6 +256,12 @@ public class AsciiDocBuilder extends AbstractMarkupDocBuilder { return this; } + @Override + public MarkupDocBuilder importMarkup(Reader markupText, int levelOffset) throws IOException { + importMarkup(AsciiDoc.TITLE, markupText, levelOffset); + return this; + } + @Override public String addfileExtension(String fileName) { return addfileExtension(AsciiDoc.FILE_EXTENSION, fileName); diff --git a/src/main/java/io/github/robwin/markup/builder/markdown/Markdown.java b/src/main/java/io/github/robwin/markup/builder/markdown/Markdown.java index 94fe3c64..f42d9f39 100644 --- a/src/main/java/io/github/robwin/markup/builder/markdown/Markdown.java +++ b/src/main/java/io/github/robwin/markup/builder/markdown/Markdown.java @@ -29,6 +29,7 @@ public enum Markdown implements Markup { TABLE_COLUMN_DELIMITER_ESCAPE("\\|"), TABLE_ROW("-"), LISTING("```"), + TITLE("#"), DOCUMENT_TITLE("# "), SECTION_TITLE_LEVEL1("## "), SECTION_TITLE_LEVEL2("### "), diff --git a/src/main/java/io/github/robwin/markup/builder/markdown/MarkdownBuilder.java b/src/main/java/io/github/robwin/markup/builder/markdown/MarkdownBuilder.java index bacc72bb..3b195233 100644 --- a/src/main/java/io/github/robwin/markup/builder/markdown/MarkdownBuilder.java +++ b/src/main/java/io/github/robwin/markup/builder/markdown/MarkdownBuilder.java @@ -26,9 +26,12 @@ import io.github.robwin.markup.builder.MarkupTableColumn; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import java.io.IOException; +import java.io.Reader; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.regex.Pattern; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.join; @@ -38,6 +41,9 @@ import static org.apache.commons.lang3.StringUtils.join; */ public class MarkdownBuilder extends AbstractMarkupDocBuilder { + private static final int MAX_TITLE_LEVEL = 5; + private static final char TITLE_PREFIX = '#'; + private static Pattern TITLE_PATTERN = Pattern.compile(String.format("^%c({1,%d})( .*)$", TITLE_PREFIX, MAX_TITLE_LEVEL)); @Override public MarkupDocBuilder copy() { @@ -243,6 +249,12 @@ public class MarkdownBuilder extends AbstractMarkupDocBuilder return this; } + @Override + public MarkupDocBuilder importMarkup(Reader markupText, int levelOffset) throws IOException { + importMarkup(Markdown.TITLE, markupText, levelOffset); + return this; + } + @Override public String addfileExtension(String fileName) { return addfileExtension(Markdown.FILE_EXTENSION, fileName); diff --git a/src/test/java/io/github/robwin/markup/builder/AbstractMarkupDocBuilderTest.java b/src/test/java/io/github/robwin/markup/builder/AbstractMarkupDocBuilderTest.java index a83640b7..acd583e0 100644 --- a/src/test/java/io/github/robwin/markup/builder/AbstractMarkupDocBuilderTest.java +++ b/src/test/java/io/github/robwin/markup/builder/AbstractMarkupDocBuilderTest.java @@ -7,7 +7,11 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import java.io.IOException; +import java.io.StringReader; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; public class AbstractMarkupDocBuilderTest { @@ -17,6 +21,8 @@ public class AbstractMarkupDocBuilderTest { @Before public void setUp() { builder = mock(AbstractMarkupDocBuilder.class, Mockito.CALLS_REAL_METHODS); + builder.newLine = System.getProperty("line.separator"); + builder.documentBuilder = new StringBuilder(); } private String normalize(Markup markup, String anchor) { @@ -64,4 +70,65 @@ public class AbstractMarkupDocBuilderTest { assertNormalization(Markdown.SPACE_ESCAPE, "", " @#&(){}[]!$*%+=/:.;,?\\<>| "); assertNormalization(Markdown.SPACE_ESCAPE, "sub-action-html-query-value", " /sub/action.html/?query=value "); } + + private void assertImportMarkup(Markup markup, String expected, String text, int levelOffset) throws IOException { + builder.documentBuilder = new StringBuilder(); + builder.importMarkup(markup, new StringReader(text), levelOffset); + assertEquals(expected, builder.documentBuilder.toString()); + } + + private void assertImportMarkupException(Markup markup, String expected, String text, int levelOffset) throws IOException { + builder.documentBuilder = new StringBuilder(); + try { + builder.importMarkup(markup, new StringReader(text), levelOffset); + fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + assertEquals(expected, e.getMessage()); + } + } + + @Test + public void testImportMarkupAsciiDoc() throws IOException { + assertImportMarkup(AsciiDoc.TITLE, "\n\n", "", 0); + assertImportMarkup(AsciiDoc.TITLE, "\n\n", "", 4); + assertImportMarkupException(AsciiDoc.TITLE, "Specified levelOffset (5) > max levelOffset (4)", "", 5); + assertImportMarkup(AsciiDoc.TITLE, "\n\n", "", -4); + assertImportMarkupException(AsciiDoc.TITLE, "Specified levelOffset (-5) < min levelOffset (-4)", "", -5); + + assertImportMarkup(AsciiDoc.TITLE, "\n= title\nline 1\nline 2\n\n", "= title\r\nline 1\r\nline 2", 0); + + assertImportMarkup(AsciiDoc.TITLE, "\nline 1\nline 2\n\n", "line 1\nline 2", 0); + assertImportMarkup(AsciiDoc.TITLE, "\nline 1\nline 2\n\n", "line 1\nline 2", 4); + + assertImportMarkup(AsciiDoc.TITLE, "\n= title\nline 1\nline 2\n= title 2\nline 3\n\n", "= title\nline 1\nline 2\n= title 2\nline 3", 0); + assertImportMarkup(AsciiDoc.TITLE, "\n===== title\nline 1\nline 2\n\n", "= title\nline 1\nline 2", 4); + assertImportMarkup(AsciiDoc.TITLE, "\n= title\nline 1\nline 2\n\n", "===== title\nline 1\nline 2", -4); + + assertImportMarkupException(AsciiDoc.TITLE, "Specified levelOffset (4) set title 'title' level (1) > max title level (4)", "== title\nline 1\nline 2", 4); + assertImportMarkupException(AsciiDoc.TITLE, "Specified levelOffset (-1) set title 'title' level (0) < 0", "= title\nline 1\nline 2", -1); + assertImportMarkupException(AsciiDoc.TITLE, "Specified levelOffset (-3) set title 'title' level (1) < 0", "== title\nline 1\nline 2", -3); + } + + @Test + public void testImportMarkupMarkdown() throws IOException { + assertImportMarkup(Markdown.TITLE, "\n\n", "", 0); + assertImportMarkup(Markdown.TITLE, "\n\n", "", 4); + assertImportMarkup(Markdown.TITLE, "\n\n", "", -4); + assertImportMarkupException(Markdown.TITLE, "Specified levelOffset (5) > max levelOffset (4)", "", 5); + assertImportMarkupException(Markdown.TITLE, "Specified levelOffset (-5) < min levelOffset (-4)", "", -5); + + assertImportMarkup(Markdown.TITLE, "\n# title\nline 1\nline 2\n\n", "# title\r\nline 1\r\nline 2", 0); + + assertImportMarkup(Markdown.TITLE, "\nline 1\nline 2\n\n", "line 1\nline 2", 0); + assertImportMarkup(Markdown.TITLE, "\nline 1\nline 2\n\n", "line 1\nline 2", 4); + + assertImportMarkup(Markdown.TITLE, "\n# title\nline 1\nline 2\n# title 2\nline 3\n\n", "# title\nline 1\nline 2\n# title 2\nline 3", 0); + assertImportMarkup(Markdown.TITLE, "\n##### title\nline 1\nline 2\n\n", "# title\nline 1\nline 2", 4); + assertImportMarkup(Markdown.TITLE, "\n# title\nline 1\nline 2\n\n", "##### title\nline 1\nline 2", -4); + + assertImportMarkupException(Markdown.TITLE, "Specified levelOffset (4) set title 'title' level (1) > max title level (4)", "## title\nline 1\nline 2", 4); + assertImportMarkupException(Markdown.TITLE, "Specified levelOffset (-1) set title 'title' level (0) < 0", "# title\nline 1\nline 2", -1); + assertImportMarkupException(Markdown.TITLE, "Specified levelOffset (-3) set title 'title' level (1) < 0", "## title\nline 1\nline 2", -3); + } + } \ No newline at end of file