-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e6ada69
commit 60cd283
Showing
2 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
222 changes: 222 additions & 0 deletions
222
jpro-mdfx/src/main/java/one/jpro/platform/mdfx/MDFXUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
package one.jpro.platform.mdfx; | ||
|
||
import com.vladsch.flexmark.ast.Heading; | ||
import com.vladsch.flexmark.parser.Parser; | ||
import com.vladsch.flexmark.util.ast.Node; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
/** | ||
* Utility class for splitting Markdown text into Chapters and Subchapters. | ||
*/ | ||
public class MDFXUtil { | ||
|
||
/** | ||
* Parses the given Markdown string and splits it into a list of Chapter objects. | ||
* Level 1 headings (#) are treated as Chapters. | ||
* Level 2 headings (##) are treated as Subchapters. | ||
* | ||
* @param markdown the input Markdown string | ||
* @return a list of Chapter objects, each containing zero or more Subchapters | ||
*/ | ||
public static List<Chapter> getChapters(String markdown) { | ||
// Build a Flexmark parser | ||
Parser parser = Parser.builder().build(); | ||
Node document = parser.parse(markdown); | ||
|
||
List<Chapter> chapters = new ArrayList<>(); | ||
Chapter currentChapter = null; | ||
int chapterIndex = 0; | ||
|
||
for (Node node = document.getFirstChild(); node != null; node = node.getNext()) { | ||
// We only look at Heading nodes or anything else (Paragraph, etc.) | ||
if (node instanceof Heading) { | ||
Heading heading = (Heading) node; | ||
int level = heading.getLevel(); | ||
String headingText = heading.getText().toString().trim(); | ||
|
||
if (level == 1) { | ||
// Start a new Chapter | ||
chapterIndex++; | ||
currentChapter = new Chapter(chapterIndex, headingText); | ||
chapters.add(currentChapter); | ||
|
||
} else if (level == 2) { | ||
// Start a new Subchapter | ||
if (currentChapter == null) { | ||
// If there's no Chapter yet, we create one so we don't lose content | ||
chapterIndex++; | ||
currentChapter = new Chapter(chapterIndex, "Unnamed Chapter"); | ||
chapters.add(currentChapter); | ||
} | ||
currentChapter.addSubchapter(headingText); | ||
} else { | ||
// For heading level 3+ we ignore or you can handle differently if needed | ||
} | ||
} else { | ||
// If it's not a heading, it's part of the current chapter/subchapter content | ||
if (currentChapter != null) { | ||
currentChapter.appendContentToCurrentSubchapterOrChapter(node.getChars().toString()); | ||
} | ||
} | ||
} | ||
|
||
return chapters; | ||
} | ||
|
||
// -------------------------------------------------- | ||
// Inner classes: Chapter, Subchapter | ||
// -------------------------------------------------- | ||
|
||
/** | ||
* Represents a top-level Chapter with an index, a heading, and optional subchapters. | ||
*/ | ||
public static class Chapter { | ||
private final int index; | ||
private final String headingText; // The text of the # heading, e.g. "Chapter 1" | ||
private final List<Subchapter> subchapters = new ArrayList<>(); | ||
private final StringBuilder chapterContent = new StringBuilder(); // Content under # heading | ||
private int subchapterIndexCounter = 0; | ||
private Subchapter currentSubchapter = null; | ||
|
||
public Chapter(int index, String headingText) { | ||
this.index = index; | ||
this.headingText = headingText; | ||
} | ||
|
||
/** | ||
* Call this when encountering a level-2 heading. | ||
*/ | ||
public void addSubchapter(String headingText) { | ||
// End the current subchapter if one is open | ||
this.currentSubchapter = null; | ||
|
||
// Create a new subchapter | ||
subchapterIndexCounter++; | ||
Subchapter sub = new Subchapter(subchapterIndexCounter, headingText); | ||
subchapters.add(sub); | ||
|
||
// Mark as current subchapter so further text goes here | ||
this.currentSubchapter = sub; | ||
} | ||
|
||
/** | ||
* Append non-heading text to the current subchapter (if present) or to the chapter content otherwise. | ||
*/ | ||
public void appendContentToCurrentSubchapterOrChapter(String content) { | ||
if (currentSubchapter == null) { | ||
chapterContent.append(content).append("\n"); | ||
} else { | ||
currentSubchapter.appendContent(content); | ||
} | ||
} | ||
|
||
/** | ||
* Returns the integer index for this chapter. | ||
*/ | ||
public int getIndex() { | ||
return index; | ||
} | ||
|
||
/** | ||
* Returns the full Markdown for this chapter, including the # heading line plus all content. | ||
*/ | ||
public String getFullMD() { | ||
StringBuilder sb = new StringBuilder(); | ||
// The heading line (including the # symbol) | ||
sb.append("# ").append(headingText).append("\n"); | ||
// The chapter content (before any subchapters) | ||
if (chapterContent.length() > 0) { | ||
sb.append(chapterContent); | ||
} | ||
// Each subchapter’s full MD | ||
for (Subchapter sc : subchapters) { | ||
sb.append(sc.getFullMD()); | ||
} | ||
return sb.toString(); | ||
} | ||
|
||
/** | ||
* Returns the content of this chapter (excluding the # heading line, but *including* subchapters). | ||
* If you want to exclude the subchapters in the content, you can do so by adjusting the logic. | ||
*/ | ||
public String getContent() { | ||
StringBuilder sb = new StringBuilder(); | ||
if (chapterContent.length() > 0) { | ||
sb.append(chapterContent); | ||
} | ||
for (Subchapter sc : subchapters) { | ||
// Add subchapter content (excluding the subchapter heading itself). | ||
// If you do NOT want subchapter content included, remove this loop. | ||
sb.append(sc.getContent()); | ||
} | ||
return sb.toString(); | ||
} | ||
|
||
/** | ||
* Returns the plain text for the chapter’s heading (no # prefix). | ||
*/ | ||
public String getHeadingText() { | ||
return headingText; | ||
} | ||
|
||
public List<Subchapter> getSubchapters() { | ||
return subchapters; | ||
} | ||
} | ||
|
||
/** | ||
* Represents a subchapter with an index, a heading, and content. | ||
*/ | ||
public static class Subchapter { | ||
private final int index; | ||
private final String headingText; // The text of the ## heading | ||
private final StringBuilder content = new StringBuilder(); | ||
|
||
public Subchapter(int index, String headingText) { | ||
this.index = index; | ||
this.headingText = headingText; | ||
} | ||
|
||
/** | ||
* Append text to this subchapter. | ||
*/ | ||
public void appendContent(String c) { | ||
content.append(c).append("\n"); | ||
} | ||
|
||
/** | ||
* The subchapter index (starts from 1 for the first subchapter in a chapter). | ||
*/ | ||
public int getIndex() { | ||
return index; | ||
} | ||
|
||
/** | ||
* Returns the subchapter heading text (excluding ##). | ||
*/ | ||
public String getHeadingText() { | ||
return headingText; | ||
} | ||
|
||
/** | ||
* Returns the full Markdown for this subchapter, including the ## heading plus content. | ||
*/ | ||
public String getFullMD() { | ||
StringBuilder sb = new StringBuilder(); | ||
sb.append("## ").append(headingText).append("\n"); | ||
if (content.length() > 0) { | ||
sb.append(content); | ||
} | ||
return sb.toString(); | ||
} | ||
|
||
/** | ||
* Returns only the content of this subchapter, excluding the ## heading line. | ||
*/ | ||
public String getContent() { | ||
return content.toString(); | ||
} | ||
} | ||
} |
93 changes: 93 additions & 0 deletions
93
jpro-mdfx/src/test/java/one/jpro/platform/mdfx/MDFXUtilTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package one.jpro.platform.mdfx; | ||
|
||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.util.List; | ||
|
||
public class MDFXUtilTest { | ||
|
||
@Test | ||
public void testSingleChapterNoSubchapters() { | ||
String md = "# Chapter 1\n" + | ||
"Some introduction text.\n" + | ||
"More text..."; | ||
|
||
List<MDFXUtil.Chapter> chapters = MDFXUtil.getChapters(md); | ||
Assertions.assertEquals(1, chapters.size()); | ||
|
||
MDFXUtil.Chapter ch = chapters.get(0); | ||
Assertions.assertEquals(1, ch.getIndex()); | ||
Assertions.assertEquals("Chapter 1", ch.getHeadingText()); | ||
|
||
// Check full MD | ||
String fullMD = ch.getFullMD(); | ||
Assertions.assertTrue(fullMD.contains("# Chapter 1")); | ||
Assertions.assertTrue(fullMD.contains("Some introduction text.")); | ||
|
||
// No subchapters | ||
Assertions.assertTrue(ch.getSubchapters().isEmpty()); | ||
} | ||
|
||
@Test | ||
public void testChapterWithSubchapters() { | ||
String md = "# Main Chapter\n" + | ||
"Intro for main chapter\n" + | ||
"## Sub A\n" + | ||
"Content for sub A\n" + | ||
"## Sub B\n" + | ||
"Content for sub B\n"; | ||
|
||
List<MDFXUtil.Chapter> chapters = MDFXUtil.getChapters(md); | ||
Assertions.assertEquals(1, chapters.size(), "Should have 1 chapter"); | ||
|
||
MDFXUtil.Chapter chapter = chapters.get(0); | ||
Assertions.assertEquals(1, chapter.getIndex()); | ||
Assertions.assertEquals("Main Chapter", chapter.getHeadingText()); | ||
|
||
// Check subchapters | ||
List<MDFXUtil.Subchapter> subs = chapter.getSubchapters(); | ||
Assertions.assertEquals(2, subs.size(), "Should have 2 subchapters"); | ||
|
||
MDFXUtil.Subchapter subA = subs.get(0); | ||
Assertions.assertEquals(1, subA.getIndex()); | ||
Assertions.assertEquals("Sub A", subA.getHeadingText()); | ||
Assertions.assertTrue(subA.getContent().contains("Content for sub A")); | ||
|
||
MDFXUtil.Subchapter subB = subs.get(1); | ||
Assertions.assertEquals(2, subB.getIndex()); | ||
Assertions.assertEquals("Sub B", subB.getHeadingText()); | ||
Assertions.assertTrue(subB.getContent().contains("Content for sub B")); | ||
} | ||
|
||
@Test | ||
public void testMultipleChapters() { | ||
String md = "# Chapter One\n" + | ||
"Text for chapter one.\n" + | ||
"## Sub-1\n" + | ||
"Text for sub-1.\n" + | ||
"# Chapter Two\n" + | ||
"Text for chapter two.\n" + | ||
"## Sub-2\n" + | ||
"Text for sub-2.\n"; | ||
|
||
List<MDFXUtil.Chapter> chapters = MDFXUtil.getChapters(md); | ||
Assertions.assertEquals(2, chapters.size(), "Should have 2 chapters"); | ||
|
||
// Chapter 1 checks | ||
MDFXUtil.Chapter ch1 = chapters.get(0); | ||
Assertions.assertEquals(1, ch1.getIndex()); | ||
Assertions.assertEquals("Chapter One", ch1.getHeadingText()); | ||
Assertions.assertTrue(ch1.getContent().contains("Text for chapter one.")); | ||
Assertions.assertEquals(1, ch1.getSubchapters().size()); | ||
Assertions.assertEquals("Sub-1", ch1.getSubchapters().get(0).getHeadingText()); | ||
|
||
// Chapter 2 checks | ||
MDFXUtil.Chapter ch2 = chapters.get(1); | ||
Assertions.assertEquals(2, ch2.getIndex()); | ||
Assertions.assertEquals("Chapter Two", ch2.getHeadingText()); | ||
Assertions.assertTrue(ch2.getContent().contains("Text for chapter two.")); | ||
Assertions.assertEquals(1, ch2.getSubchapters().size()); | ||
Assertions.assertEquals("Sub-2", ch2.getSubchapters().get(0).getHeadingText()); | ||
} | ||
} |