Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package io.agentscope.core.skill.util;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -170,6 +172,8 @@ static Map<String, String> parse(String yaml) {
}

String[] lines = yaml.split("[\\r\\n]+");
String curKey = null;
List<String> yamlList = new ArrayList<>();

for (String line : lines) {
// Skip empty lines
Expand All @@ -182,16 +186,39 @@ static Map<String, String> parse(String yaml) {
continue;
}

// Handle list items
if (line.trim().startsWith("-")) {
// Start with a list item but no key, throw it
if (curKey == null) {
throw new IllegalArgumentException(
"List item without a preceding key: " + line);
}
String yamlListItem = line.trim().substring(1).trim();
yamlList.add(yamlListItem);
continue;
}

// check if list item exists
if (curKey != null && !yamlList.isEmpty()) {
result.put(curKey, String.join(",", yamlList));
yamlList.clear();
}

Matcher matcher = KEY_VALUE_PATTERN.matcher(line.trim());
if (!matcher.matches()) {
throw new IllegalArgumentException(
"Invalid YAML line (expected 'key: value' format): " + line);
}

String key = matcher.group(1);
curKey = matcher.group(1);
String value = parseValue(matcher.group(2));

result.put(key, value);
result.put(curKey, value);
}

// when list in the end
if (curKey != null && !yamlList.isEmpty()) {
result.put(curKey, String.join(",", yamlList));
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,15 @@ void testInvalidYaml() {
}

@Test
@DisplayName("Should throw exception for list format")
@DisplayName("Should throw exception for preceding list format")
void testListFormat() {
String markdown = "---\n- item1\n- item2\n---\nContent";

IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() -> MarkdownSkillParser.parse(markdown));
assertTrue(exception.getMessage().contains("Invalid YAML line"));
assertTrue(exception.getMessage().contains("List item without a preceding key"));
}
}

Expand Down Expand Up @@ -535,4 +535,258 @@ void testToString() {
assertTrue(toString.contains("content"));
}
}

@Nested
@DisplayName("ParseMarkdownList Tests")
class ParseMarkdownListTests {
@Test
@DisplayName("Test parsing YAML with single list item")
void testParseSingleListItem() {
String yaml = "name: test\n" + "tags:\n" + " - feature1";

Map<String, String> result = parseYaml(yaml);

assertEquals("test", result.get("name"));
assertEquals("feature1", result.get("tags"));
}

@Test
@DisplayName("Test parsing YAML with multiple list items")
void testParseMultipleListItems() {
String yaml =
"name: test\n"
+ "tags:\n"
+ " - feature1\n"
+ " - feature2\n"
+ " - feature3";

Map<String, String> result = parseYaml(yaml);

assertEquals("test", result.get("name"));
assertEquals("feature1,feature2,feature3", result.get("tags"));
}

@Test
@DisplayName("Test parsing YAML with list in the middle")
void testParseListInMiddle() {
String yaml =
"name: test\n"
+ "tags:\n"
+ " - feature1\n"
+ " - feature2\n"
+ "description: A test skill";

Map<String, String> result = parseYaml(yaml);

assertEquals("test", result.get("name"));
assertEquals("feature1,feature2", result.get("tags"));
assertEquals("A test skill", result.get("description"));
}

@Test
@DisplayName("Test parsing YAML with multiple lists")
void testParseMultipleLists() {
String yaml =
"name: test\n"
+ "tags:\n"
+ " - tag1\n"
+ " - tag2\n"
+ "description: A test skill\n"
+ "inputs:\n"
+ " - input1\n"
+ " - input2\n"
+ " - input3";

Map<String, String> result = parseYaml(yaml);

assertEquals("test", result.get("name"));
assertEquals("tag1,tag2", result.get("tags"));
assertEquals("A test skill", result.get("description"));
assertEquals("input1,input2,input3", result.get("inputs"));
}

@Test
@DisplayName("Test parsing YAML with list item without key should throw exception")
void testParseListItemWithoutKey() {
String yaml = "- item1\n" + "- item2";

assertThrows(IllegalArgumentException.class, () -> parseYaml(yaml));
}

@Test
@DisplayName("Test parsing YAML with empty list")
void testParseEmptyList() {
String yaml = "name: test\n" + "tags:\n" + "description: A test skill";

Map<String, String> result = parseYaml(yaml);

assertEquals("test", result.get("name"));
assertEquals("A test skill", result.get("description"));
}

@Test
@DisplayName("Test parsing YAML with list containing spaces")
void testParseListWithSpaces() {
String yaml = "name: test\n" + "tags:\n" + " - feature1 \n" + " - feature2";

Map<String, String> result = parseYaml(yaml);

assertEquals("test", result.get("name"));
assertEquals("feature1,feature2", result.get("tags"));
}

@Test
@DisplayName("Test parsing markdown with list in frontmatter")
void testParseMarkdownWithListInFrontmatter() {
String markdown =
"---\n"
+ "name: test_skill\n"
+ "tags:\n"
+ " - ai\n"
+ " - ml\n"
+ " - deep-learning\n"
+ "version: 1.0.0\n"
+ "---\n"
+ "# Test Skill\n"
+ "This is a test skill description.";

ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);

assertTrue(parsed.hasFrontmatter());
assertEquals("test_skill", parsed.getMetadata().get("name"));
assertEquals("ai,ml,deep-learning", parsed.getMetadata().get("tags"));
assertEquals("1.0.0", parsed.getMetadata().get("version"));
assertTrue(parsed.getContent().contains("# Test Skill"));
}

@Test
@DisplayName("Test parsing markdown with multiple lists in frontmatter")
void testParseMarkdownWithMultipleLists() {
String markdown =
"---\n"
+ "name: vm_renew_skill\n"
+ "tags:\n"
+ " - vm\n"
+ " - renew\n"
+ "inputs:\n"
+ " - vm_id\n"
+ " - duration\n"
+ "outputs:\n"
+ " - result\n"
+ " - status\n"
+ "version: 2.0.0\n"
+ "---\n"
+ "# VM Renew Skill\n"
+ "This skill renews virtual machines.";

ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);

assertTrue(parsed.hasFrontmatter());
assertEquals("vm_renew_skill", parsed.getMetadata().get("name"));
assertEquals("vm,renew", parsed.getMetadata().get("tags"));
assertEquals("vm_id,duration", parsed.getMetadata().get("inputs"));
assertEquals("result,status", parsed.getMetadata().get("outputs"));
assertEquals("2.0.0", parsed.getMetadata().get("version"));
assertTrue(parsed.getContent().contains("# VM Renew Skill"));
}

@Test
@DisplayName("Test generating markdown with list values")
void testGenerateMarkdownWithList() {
Map<String, String> metadata =
Map.of(
"name", "test_skill",
"tags", "ai,ml,deep-learning",
"version", "1.0.0");
String content = "# Test Skill\nThis is content.";

String generated = MarkdownSkillParser.generate(metadata, content);

assertTrue(generated.startsWith("---\n"));
assertTrue(generated.contains("name: test_skill"));
assertTrue(generated.contains("version: 1.0.0"));
assertTrue(generated.contains("# Test Skill"));
}

@Test
@DisplayName("Test round-trip: parse and generate with list")
void testRoundTripWithList() {
String original =
"---\n"
+ "name: round_trip_test\n"
+ "tags:\n"
+ " - tag1\n"
+ " - tag2\n"
+ " - tag3\n"
+ "version: 1.0.0\n"
+ "---\n"
+ "# Content";

ParsedMarkdown parsed = MarkdownSkillParser.parse(original);
String regenerated =
MarkdownSkillParser.generate(parsed.getMetadata(), parsed.getContent());

// Parse again to verify round-trip
ParsedMarkdown reparsed = MarkdownSkillParser.parse(regenerated);

assertEquals(parsed.getMetadata().get("name"), reparsed.getMetadata().get("name"));
assertEquals("tag1,tag2,tag3", reparsed.getMetadata().get("tags"));
assertEquals(
parsed.getMetadata().get("version"), reparsed.getMetadata().get("version"));
}

@Test
@DisplayName("Test parsing empty markdown")
void testParseEmptyMarkdown() {
ParsedMarkdown parsed = MarkdownSkillParser.parse("");

assertFalse(parsed.hasFrontmatter());
assertTrue(parsed.getMetadata().isEmpty());
assertEquals("", parsed.getContent());
}

@Test
@DisplayName("Test parsing markdown without frontmatter")
void testParseMarkdownWithoutFrontmatter() {
String markdown = "# Just Content\nNo frontmatter here.";

ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);

assertFalse(parsed.hasFrontmatter());
assertTrue(parsed.getMetadata().isEmpty());
assertEquals("# Just Content\nNo frontmatter here.", parsed.getContent());
}

@Test
@DisplayName("Test parsing null markdown")
void testParseNullMarkdown() {
ParsedMarkdown parsed = MarkdownSkillParser.parse(null);

assertFalse(parsed.hasFrontmatter());
assertTrue(parsed.getMetadata().isEmpty());
assertEquals("", parsed.getContent());
}

@Test
@DisplayName("Test parsing list with special characters in items")
void testParseListWithSpecialCharacters() {
String yaml =
"name: test\n"
+ "tags:\n"
+ " - tag-with-dash\n"
+ " - tag_with_underscore\n"
+ " - tag123";

Map<String, String> result = parseYaml(yaml);

assertEquals("test", result.get("name"));
assertEquals("tag-with-dash,tag_with_underscore,tag123", result.get("tags"));
}

private Map<String, String> parseYaml(String yaml) {
String markdown = "---\n" + yaml + "\n---\n";
ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);
return parsed.getMetadata();
}
}
}
Loading