001/* 002* Copyright 2025 - 2025 the original author or authors. 003* 004* Licensed under the Apache License, Version 2.0 (the "License"); 005* you may not use this file except in compliance with the License. 006* You may obtain a copy of the License at 007* 008* https://www.apache.org/licenses/LICENSE-2.0 009* 010* Unless required by applicable law or agreed to in writing, software 011* distributed under the License is distributed on an "AS IS" BASIS, 012* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013* See the License for the specific language governing permissions and 014* limitations under the License. 015*/ 016package com.agentsflex.skill.util; 017 018import java.util.HashMap; 019import java.util.Map; 020 021/** 022 * Parser for Markdown documents with optional YAML front matter. 023 * <p> 024 * This parser extracts YAML front matter (metadata) and content from Markdown documents. 025 * Front matter is delimited by triple dashes (---) at the beginning of the document and 026 * contains key-value pairs in YAML format. 027 * <p> 028 * Example Markdown with front matter: 029 * 030 * <pre>{@code 031 * --- 032 * title: My Document 033 * author: John Doe 034 * date: 2024-01-15 035 * --- 036 * 037 * # Heading 038 * 039 * Document content here. 040 * }</pre> 041 * <p> 042 * The parser supports: 043 * <ul> 044 * <li>Front matter with key-value pairs separated by colons</li> 045 * <li>Values with or without quotes (both single and double quotes are supported)</li> 046 * <li>Documents without front matter (entire content is treated as body)</li> 047 * <li>Empty or null markdown input</li> 048 * </ul> 049 * 050 * @author Christian Tzolov 051 */ 052public class MarkdownParser { 053 054 /** 055 * Map containing the parsed front matter key-value pairs. 056 */ 057 private Map<String, Object> frontMatter; 058 059 /** 060 * The content of the markdown document (everything after the front matter). 061 */ 062 private String content; 063 064 /** 065 * Constructs a new MarkdownParser and parses the provided markdown content. Parses 066 * the markdown content to extract front matter and body content. 067 * <p> 068 * Front matter must start with "---" at the beginning of the document and end with 069 * another "---". Everything between these delimiters is parsed as front matter. 070 * Everything after the closing delimiter is treated as content. 071 * @param markdown the markdown string to parse, may contain front matter delimited by 072 * triple dashes (---). Can be null or empty. 073 */ 074 public MarkdownParser(String markdown) { 075 076 frontMatter = new HashMap<>(); 077 content = ""; 078 079 if (markdown == null || markdown.isEmpty()) { 080 return; 081 } 082 083 // Check if document starts with front-matter delimiter (---) 084 if (markdown.startsWith("---")) { 085 // Find the closing delimiter 086 int endIndex = markdown.indexOf("---", 3); 087 088 if (endIndex != -1) { 089 // Extract front-matter section 090 String frontMatterSection = markdown.substring(3, endIndex).trim(); 091 parseFrontMatter(frontMatterSection); 092 093 // Extract remaining content (skip the closing --- and any following 094 // newlines) 095 content = markdown.substring(endIndex + 3).trim(); 096 } 097 else { 098 // No closing delimiter found, treat entire document as content 099 content = markdown; 100 } 101 } 102 else { 103 // No front-matter, entire document is content 104 content = markdown; 105 } 106 107 } 108 109 private void parseFrontMatter(String frontMatterSection) { 110 String[] lines = frontMatterSection.split("\n"); 111 112 for (String line : lines) { 113 line = line.trim(); 114 115 if (line.isEmpty()) { 116 continue; 117 } 118 119 // Split on first colon 120 int colonIndex = line.indexOf(':'); 121 if (colonIndex > 0) { 122 String key = line.substring(0, colonIndex).trim(); 123 String value = line.substring(colonIndex + 1).trim(); 124 125 // Removes surrounding quotes from a value string if present. 126 value = removeQuotes(value); 127 128 frontMatter.put(key, value); 129 } 130 } 131 } 132 133 private String removeQuotes(String value) { 134 if (value.length() >= 2) { 135 if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) { 136 return value.substring(1, value.length() - 1); 137 } 138 } 139 return value; 140 } 141 142 /** 143 * Returns a copy of the parsed front matter as a map. 144 * <p> 145 * The returned map contains all key-value pairs extracted from the front matter 146 * section. If no front matter was present or the input was null/empty, returns an 147 * empty map. 148 * @return a new map containing the front matter key-value pairs 149 */ 150 public Map<String, Object> getFrontMatter() { 151 return new HashMap<>(frontMatter); 152 } 153 154 /** 155 * Returns the content portion of the markdown document. 156 * <p> 157 * This is everything after the closing front matter delimiter (---), with leading and 158 * trailing whitespace trimmed. If no front matter was present, returns the entire 159 * document. If the input was null or empty, returns an empty string. 160 * @return the markdown content as a string 161 */ 162 public String getContent() { 163 return content; 164 } 165}