Skip to content

Commit

Permalink
added utility classes for MDFX
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianKirmaier committed Jan 3, 2025
1 parent e6ada69 commit 60cd283
Show file tree
Hide file tree
Showing 2 changed files with 315 additions and 0 deletions.
222 changes: 222 additions & 0 deletions jpro-mdfx/src/main/java/one/jpro/platform/mdfx/MDFXUtil.java
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 jpro-mdfx/src/test/java/one/jpro/platform/mdfx/MDFXUtilTest.java
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());
}
}

0 comments on commit 60cd283

Please sign in to comment.