Add Markdown document reader with enhanced features

This commit introduces a new Markdown document reader with several
key features and improvements:

* Add support for text with various formatting elements
* Implement handling for horizontal rules and hard line breaks
* Add functionality for inline and block code sections
* Incorporate blockquote handling
* Support ordered and unordered lists
* Introduce additional metadata capabilities
* Include JavaDocs

Update ETL documentation to reflect these new features and usage.

Fixes #105
This commit is contained in:
Piotr Olaszewski
2024-07-23 18:13:13 +02:00
committed by Mark Pollack
parent a0ee10fa91
commit 56e678c487
14 changed files with 1364 additions and 104 deletions

View File

@@ -0,0 +1,230 @@
package org.springframework.ai.reader.markdown;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
/**
* @author Piotr Olaszewski
*/
class MarkdownDocumentReaderTest {
@Test
void testOnlyHeadersWithParagraphs() {
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/only-headers.md");
List<Document> documents = reader.get();
assertThat(documents).hasSize(4)
.extracting(Document::getMetadata, Document::getContent)
.containsOnly(tuple(Map.of("category", "header_1", "title", "Header 1a"),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur diam eros, laoreet sit amet cursus vitae, varius sed nisi. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue."),
tuple(Map.of("category", "header_1", "title", "Header 1b"),
"Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam lobortis risus libero, sed sollicitudin risus cursus in. Morbi enim metus, ornare vel lacinia eget, venenatis vel nibh."),
tuple(Map.of("category", "header_2", "title", "Header 2b"),
"Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget sapien odio. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero."),
tuple(Map.of("category", "header_2", "title", "Header 2c"),
"Ut rhoncus nec justo a porttitor. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum suscipit."));
}
@Test
void testWithFormatting() {
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/with-formatting.md");
List<Document> documents = reader.get();
assertThat(documents).hasSize(2)
.extracting(Document::getMetadata, Document::getContent)
.containsOnly(tuple(Map.of("category", "header_1", "title", "This is a fancy header name"),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt velit non bibendum gravida. Cras accumsan tincidunt ornare. Donec hendrerit consequat tellus blandit accumsan. Aenean aliquam metus at arcu elementum dignissim."),
tuple(Map.of("category", "header_3", "title", "Header 3"),
"Aenean eu leo eu nibh tristique posuere quis quis massa."));
}
@Test
void testDocumentDividedViaHorizontalRules() {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/horizontal-rules.md", config);
List<Document> documents = reader.get();
assertThat(documents).hasSize(7)
.extracting(Document::getMetadata, Document::getContent)
.containsOnly(tuple(Map.of(),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt velit non bibendum gravida."),
tuple(Map.of(),
"Cras accumsan tincidunt ornare. Donec hendrerit consequat tellus blandit accumsan. Aenean aliquam metus at arcu elementum dignissim."),
tuple(Map.of(),
"Nullam nisi dui, egestas nec sem nec, interdum lobortis enim. Pellentesque odio orci, faucibus eu luctus nec, venenatis et magna."),
tuple(Map.of(),
"Vestibulum nec eros non felis fermentum posuere eget ac risus. Curabitur et fringilla massa. Cras facilisis nec nisl sit amet sagittis."),
tuple(Map.of(),
"Aenean eu leo eu nibh tristique posuere quis quis massa. Nullam lacinia luctus sem ut vehicula."),
tuple(Map.of(),
"Aenean quis vulputate mi. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam tincidunt nunc a tortor tincidunt, nec lobortis diam rhoncus."),
tuple(Map.of(), "Nulla facilisi. Phasellus eget tellus sed nibh ornare interdum eu eu mi."));
}
@Test
void testDocumentNotDividedViaHorizontalRulesWhenIsDisabled() {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(false)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/horizontal-rules.md", config);
List<Document> documents = reader.get();
assertThat(documents).hasSize(1);
Document documentsFirst = documents.get(0);
assertThat(documentsFirst.getMetadata()).isEmpty();
assertThat(documentsFirst.getContent()).startsWith("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
.endsWith("Phasellus eget tellus sed nibh ornare interdum eu eu mi.");
}
@Test
void testSimpleMarkdownDocumentWithHardAndSoftLineBreaks() {
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/simple.md");
List<Document> documents = reader.get();
assertThat(documents).hasSize(1);
Document documentsFirst = documents.get(0);
assertThat(documentsFirst.getMetadata()).isEmpty();
assertThat(documentsFirst.getContent()).isEqualTo(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt velit non bibendum gravida. Cras accumsan tincidunt ornare. Donec hendrerit consequat tellus blandit accumsan. Aenean aliquam metus at arcu elementum dignissim.Nullam nisi dui, egestas nec sem nec, interdum lobortis enim. Pellentesque odio orci, faucibus eu luctus nec, venenatis et magna. Vestibulum nec eros non felis fermentum posuere eget ac risus.Aenean eu leo eu nibh tristique posuere quis quis massa. Nullam lacinia luctus sem ut vehicula.");
}
@Test
void testCode() {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/code.md", config);
List<Document> documents = reader.get();
assertThat(documents).satisfiesExactly(document -> {
assertThat(document.getMetadata()).isEqualTo(Map.of());
assertThat(document.getContent()).isEqualTo("This is a Java sample application:");
}, document -> {
assertThat(document.getMetadata()).isEqualTo(Map.of("lang", "java", "category", "code_block"));
assertThat(document.getContent()).startsWith("package com.example.demo;")
.contains("SpringApplication.run(DemoApplication.class, args);");
}, document -> {
assertThat(document.getMetadata()).isEqualTo(Map.of("category", "code_inline"));
assertThat(document.getContent()).isEqualTo(
"Markdown also provides the possibility to use inline code formatting throughout the entire sentence.");
}, document -> {
assertThat(document.getMetadata()).isEqualTo(Map.of());
assertThat(document.getContent())
.isEqualTo("Another possibility is to set block code without specific highlighting:");
}, document -> {
assertThat(document.getMetadata()).isEqualTo(Map.of("lang", "", "category", "code_block"));
assertThat(document.getContent()).isEqualTo("./mvnw spring-javaformat:apply\n");
});
}
@Test
void testCodeWhenCodeBlockShouldNotBeSeparatedDocument() {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(true)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/code.md", config);
List<Document> documents = reader.get();
assertThat(documents).satisfiesExactly(document -> {
assertThat(document.getMetadata()).isEqualTo(Map.of("lang", "java", "category", "code_block"));
assertThat(document.getContent()).startsWith("This is a Java sample application: package com.example.demo")
.contains("SpringApplication.run(DemoApplication.class, args);");
}, document -> {
assertThat(document.getMetadata()).isEqualTo(Map.of("category", "code_inline"));
assertThat(document.getContent()).isEqualTo(
"Markdown also provides the possibility to use inline code formatting throughout the entire sentence.");
}, document -> {
assertThat(document.getMetadata()).isEqualTo(Map.of("lang", "", "category", "code_block"));
assertThat(document.getContent()).isEqualTo(
"Another possibility is to set block code without specific highlighting: ./mvnw spring-javaformat:apply\n");
});
}
@Test
void testBlockquote() {
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/blockquote.md");
List<Document> documents = reader.get();
assertThat(documents).hasSize(2)
.extracting(Document::getMetadata, Document::getContent)
.containsOnly(tuple(Map.of(),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur diam eros, laoreet sit amet cursus vitae, varius sed nisi. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue."),
tuple(Map.of("category", "blockquote"),
"Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget sapien odio. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero. Ut rhoncus nec justo a porttitor. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum suscipit."));
}
@Test
void testBlockquoteWhenBlockquoteShouldNotBeSeparatedDocument() {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withIncludeBlockquote(true)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/blockquote.md", config);
List<Document> documents = reader.get();
assertThat(documents).hasSize(1);
Document documentsFirst = documents.get(0);
assertThat(documentsFirst.getMetadata()).isEqualTo(Map.of("category", "blockquote"));
assertThat(documentsFirst.getContent()).isEqualTo(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur diam eros, laoreet sit amet cursus vitae, varius sed nisi. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue. Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget sapien odio. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero. Ut rhoncus nec justo a porttitor. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum suscipit.");
}
@Test
void testLists() {
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/lists.md");
List<Document> documents = reader.get();
assertThat(documents).hasSize(2)
.extracting(Document::getMetadata, Document::getContent)
.containsOnly(tuple(Map.of("category", "header_2", "title", "Ordered list"),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur diam eros, laoreet sit amet cursus vitae, varius sed nisi. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue. Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget sapien odio. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum suscipit. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero. Ut rhoncus nec justo a porttitor."),
tuple(Map.of("category", "header_2", "title", "Unordered list"),
"Aenean eu leo eu nibh tristique posuere quis quis massa. Aenean imperdiet libero dui, nec malesuada dui maximus vel. Vestibulum sed dui condimentum, cursus libero in, dapibus tortor. Etiam facilisis enim in egestas dictum."));
}
@Test
void testWithAdditionalMetadata() {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withAdditionalMetadata("service", "some-service-name")
.withAdditionalMetadata("env", "prod")
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader("classpath:/simple.md", config);
List<Document> documents = reader.get();
assertThat(documents).hasSize(1);
Document documentsFirst = documents.get(0);
assertThat(documentsFirst.getMetadata()).isEqualTo(Map.of("service", "some-service-name", "env", "prod"));
assertThat(documentsFirst.getContent()).startsWith("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
}
}

View File

@@ -0,0 +1,8 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur diam eros, laoreet sit amet cursus vitae, varius sed
nisi. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue.
> Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget
> sapien odio. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero. Ut rhoncus nec justo a
> porttitor. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum
> suscipit.

View File

@@ -0,0 +1,25 @@
This is a Java sample application:
```java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
```
Markdown also provides the possibility to `use inline code formatting throughout` the entire sentence.
---
Another possibility is to set block code without specific highlighting:
```
./mvnw spring-javaformat:apply
```

View File

@@ -0,0 +1,27 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt velit non bibendum gravida.
---
Cras accumsan tincidunt ornare. Donec hendrerit consequat tellus blandit accumsan. Aenean aliquam metus at arcu
elementum dignissim.
***
Nullam nisi dui, egestas nec sem nec, interdum lobortis enim. Pellentesque odio orci, faucibus eu luctus nec, venenatis
et magna.
* * *
Vestibulum nec eros non felis fermentum posuere eget ac risus. Curabitur et fringilla massa. Cras facilisis nec nisl sit
amet sagittis.
*****
Aenean eu leo eu nibh tristique posuere quis quis massa. Nullam lacinia luctus sem ut vehicula.
---------------------------------------
Aenean quis vulputate mi. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam tincidunt nunc a tortor tincidunt, nec lobortis diam rhoncus.
- - -
Nulla facilisi. Phasellus eget tellus sed nibh ornare interdum eu eu mi.

View File

@@ -0,0 +1,17 @@
## Ordered list
1. Lorem ipsum dolor sit *amet*, consectetur adipiscing elit. **Curabitur** diam eros, laoreet sit _amet_ cursus vitae,
varius sed nisi.
2. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue.
3. Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget
sapien odio.
1. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum
suscipit.
2. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero. Ut rhoncus nec justo a porttitor.
## Unordered list
* Aenean eu leo eu nibh tristique posuere quis quis massa.
* Aenean imperdiet libero dui, nec malesuada dui maximus vel. Vestibulum sed dui condimentum, cursus libero in, dapibus
tortor.
* Etiam facilisis enim in egestas dictum.

View File

@@ -0,0 +1,20 @@
# Header 1a
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur diam eros, laoreet sit amet cursus vitae, varius sed
nisi. Cras sit amet quam quis velit commodo porta consectetur id nisi. Phasellus tincidunt pulvinar augue.
# Header 1b
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam lobortis risus libero, sed
sollicitudin risus cursus in. Morbi enim metus, ornare vel lacinia eget, venenatis vel nibh.
## Header 2b
Proin vel laoreet leo, sed luctus augue. Sed et ligula commodo, commodo lacus at, consequat turpis. Maecenas eget sapien
odio. Maecenas urna lectus, pellentesque in accumsan aliquam, congue eu libero.
# Header 1c
## Header 2c
Ut rhoncus nec justo a porttitor. Pellentesque auctor pharetra eros, viverra sodales lorem aliquet id. Curabitur semper nisi vel sem interdum suscipit.

View File

@@ -0,0 +1,8 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt velit non bibendum gravida. Cras accumsan
tincidunt ornare. Donec hendrerit consequat tellus blandit accumsan. Aenean aliquam metus at arcu elementum dignissim.
Nullam nisi dui, egestas nec sem nec, interdum lobortis enim. Pellentesque odio orci, faucibus eu luctus nec, venenatis et magna. Vestibulum nec eros non felis fermentum posuere eget ac risus.
Aenean eu leo eu nibh tristique posuere quis quis massa.\
Nullam lacinia luctus sem ut vehicula.

View File

@@ -0,0 +1,9 @@
# This is a fancy header name
Lorem ipsum dolor sit amet, **consectetur adipiscing elit**. Donec tincidunt velit non bibendum gravida. Cras accumsan
tincidunt ornare. Donec hendrerit consequat tellus *blandit* accumsan. Aenean aliquam metus at ***arcu elementum***
dignissim.
### Header 3
Aenean eu leo eu nibh tristique _posuere quis quis massa_.