Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -20,6 +20,8 @@
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Utility for parsing and generating Markdown files with YAML frontmatter.
Expand Down Expand Up @@ -54,6 +56,8 @@
*/
public class MarkdownSkillParser {

private static final Logger logger = LoggerFactory.getLogger(MarkdownSkillParser.class);

/**
* Private constructor to prevent instantiation.
*/
Expand Down Expand Up @@ -81,7 +85,6 @@ private MarkdownSkillParser() {}
*
* @param markdown Markdown content (may or may not have frontmatter)
* @return ParsedMarkdown containing metadata and content
* @throws IllegalArgumentException if YAML syntax is invalid
*/
public static ParsedMarkdown parse(String markdown) {
if (markdown == null || markdown.isEmpty()) {
Expand All @@ -102,14 +105,8 @@ public static ParsedMarkdown parse(String markdown) {
return new ParsedMarkdown(Map.of(), markdownContent);
}

try {
Map<String, String> metadata = SimpleYamlParser.parse(yamlContent);
return new ParsedMarkdown(metadata, markdownContent);
} catch (IllegalArgumentException e) {
throw e;
} catch (RuntimeException e) {
throw new IllegalArgumentException("Invalid YAML frontmatter syntax", e);
}
Map<String, String> metadata = SimpleYamlParser.parse(yamlContent);
return new ParsedMarkdown(metadata, markdownContent);
}

/**
Expand Down Expand Up @@ -158,9 +155,14 @@ private static class SimpleYamlParser {
/**
* Parse YAML string into a map of key-value pairs.
*
* <p>This is a simplified parser designed for flat string-to-string mappings.
* Block-style complex YAML structures (such as multi-line lists or indented
* nested objects) are not supported and will be gracefully skipped.
* However, flow-style inline structures (e.g., single-line JSON strings)
* are treated as standard scalar values and will be parsed as raw strings.
*
* @param yaml YAML content to parse
* @return Map of key-value pairs
* @throws IllegalArgumentException if YAML syntax is invalid
*/
static Map<String, String> parse(String yaml) {
Map<String, String> result = new LinkedHashMap<>();
Expand All @@ -184,14 +186,23 @@ static Map<String, String> parse(String yaml) {

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

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

result.put(key, value);
if (!value.isEmpty()) {
result.put(key, value);
} else {
logger.debug(
"Skipping key '{}': empty values or block-style complex structures are"
+ " unsupported",
key);
}
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.agentscope.core.skill.util.MarkdownSkillParser.ParsedMarkdown;
Expand Down Expand Up @@ -283,28 +282,29 @@ void testParseUnicodeCharacters() {
class ErrorHandlingTests {

@Test
@DisplayName("Should throw exception for invalid YAML")
@DisplayName("Should gracefully ignore invalid YAML lines instead of throwing exception")
void testInvalidYaml() {
String markdown = "---\nname: test\nthis is not a valid line\n---\nContent";

IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() -> MarkdownSkillParser.parse(markdown));
assertTrue(exception.getMessage().contains("Invalid YAML line"));
assertTrue(exception.getMessage().contains("expected 'key: value' format"));
MarkdownSkillParser.ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);
Map<String, String> metadata = parsed.getMetadata();

assertEquals("test", metadata.get("name"));
assertFalse(metadata.containsKey("this is not a valid line"));
assertEquals("Content", parsed.getContent());
}

@Test
@DisplayName("Should throw exception for list format")
@DisplayName("Should gracefully ignore list format instead of throwing exception")
void testListFormat() {
String markdown = "---\n- item1\n- item2\n---\nContent";
String markdown = "---\nname: test_skill\n- item1\n- item2\n---\nContent";

MarkdownSkillParser.ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);
Map<String, String> metadata = parsed.getMetadata();

IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() -> MarkdownSkillParser.parse(markdown));
assertTrue(exception.getMessage().contains("Invalid YAML line"));
assertEquals("test_skill", metadata.get("name"));
assertFalse(metadata.containsKey("- item1"));
assertFalse(metadata.containsKey("- item2"));
}
}

Expand Down Expand Up @@ -429,7 +429,7 @@ void testGenerateEmptyValue() {
String generated = MarkdownSkillParser.generate(metadata, "Content");
ParsedMarkdown parsed = MarkdownSkillParser.parse(generated);

assertEquals("", parsed.getMetadata().get("empty"));
assertNull(parsed.getMetadata().get("empty"));
}
}

Expand Down Expand Up @@ -534,5 +534,43 @@ void testToString() {
assertTrue(toString.contains("metadata"));
assertTrue(toString.contains("content"));
}

@Test
@DisplayName(
"Should parse basic scalars and gracefully ignore complex YAML structures like"
+ " lists or JSON")
void testParseAndIgnoreComplexMetadata() {
String markdown =
"""
---
name: Agent Browser
description: A fast Rust-based headless browser automation CLI
read_when:
- Automating web interactions
- Extracting structured data from pages
metadata: {"clawdbot":{"emoji":"🌐"}}
allowed-tools: Bash(agent-browser:*)
---

# Content
This is the content.\
""";

MarkdownSkillParser.ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);
Map<String, String> metadata = parsed.getMetadata();

assertEquals("Agent Browser", metadata.get("name"));
assertEquals(
"A fast Rust-based headless browser automation CLI",
metadata.get("description"));
assertEquals("Bash(agent-browser:*)", metadata.get("allowed-tools"));

assertEquals("{\"clawdbot\":{\"emoji\":\"🌐\"}}", metadata.get("metadata"));

assertNull(metadata.get("read_when"));
assertNull(metadata.get("- Automating web interactions"));

assertTrue(parsed.getContent().contains("# Content"));
}
}
}
Loading