diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0807af3..0b5d915 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,9 @@ "Bash(grep:*)", "Bash(git commit:*)", "Bash(echo:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(wc:*)", + "Bash(xcodebuild docbuild:*)" ] } } diff --git a/Package.resolved b/Package.resolved index b41a268..004d361 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "735626bbee1c38fc7d9f3f7cdc6ec8680fd98d0ca2f862df874e896733c8d96b", + "originHash" : "036a8216751eb51cee4201d2f300d6f5feed5f2bbf2b81c90afa8a9808b853dc", "pins" : [ { "identity" : "jmespath.swift", @@ -64,6 +64,24 @@ "version" : "0.7.1" } }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-docc-plugin", + "state" : { + "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", + "version" : "1.4.5" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, { "identity" : "swift-parsing", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 6e942e8..f39d51b 100644 --- a/Package.swift +++ b/Package.swift @@ -25,6 +25,7 @@ let package = Package( .package(url: "https://github.com/kylef/PathKit", from: "1.0.1"), .package(url: "https://github.com/jpsim/Yams.git", from: "6.1.0"), .package(url: "https://github.com/adam-fowler/jmespath.swift.git", from: "1.0.3"), + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), ], targets: [ // MARK: MarkdownUtilities diff --git a/Sources/MarkdownUtilities/Documentation.docc/Articles/FormatConversion/PlainTextConversion.md b/Sources/MarkdownUtilities/Documentation.docc/Articles/FormatConversion/PlainTextConversion.md new file mode 100644 index 0000000..9495958 --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/Articles/FormatConversion/PlainTextConversion.md @@ -0,0 +1,638 @@ +# Plain Text Conversion + +Convert Markdown to plain text with customizable formatting options. + +## Overview + +MarkdownUtilities can convert Markdown documents to plain text, stripping formatting while preserving content structure. This is useful for previews, search indexing, content analysis, and accessibility. + +## Basic Conversion + +### Default Conversion + +Convert with default options: + +```swift +import MarkdownUtilities + +let markdown = """ +# Welcome + +This is **bold** and this is *italic*. + +## Features + +- First item +- Second item +- Third item + +```swift +let code = "example" +``` + +Visit [our website](https://example.com). +""" + +let document = MarkdownDocument(content: markdown) +let plainText = document.toPlainText() + +print(plainText) +``` + +**Output:** +``` +Welcome + +This is bold and this is italic. + +Features + +First item +Second item +Third item + +let code = "example" + +Visit our website. +``` + +### With Custom Options + +Configure conversion behavior: + +```swift +let options = PlainTextOptions( + blockSeparator: "\n\n", + indentLists: true, + preserveCodeBlocks: true +) + +let plainText = document.toPlainText(options: options) +``` + +## Configuration Options + +### Block Separator + +Control spacing between block elements: + +```swift +// Double newlines (default) +let spaced = document.toPlainText( + options: PlainTextOptions(blockSeparator: "\n\n") +) + +// Single newlines (compact) +let compact = document.toPlainText( + options: PlainTextOptions(blockSeparator: "\n") +) + +// Triple newlines (extra spacing) +let wide = document.toPlainText( + options: PlainTextOptions(blockSeparator: "\n\n\n") +) +``` + +**Example:** + +```swift +// Input +let md = """ +# Title + +Paragraph 1. + +Paragraph 2. +""" + +// With "\n\n" (default) +// Output: +// Title +// +// Paragraph 1. +// +// Paragraph 2. + +// With "\n" +// Output: +// Title +// Paragraph 1. +// Paragraph 2. +``` + +### List Indentation + +Control whether list items are indented: + +```swift +// With indentation (default) +let indented = document.toPlainText( + options: PlainTextOptions(indentLists: true) +) + +// Without indentation +let flat = document.toPlainText( + options: PlainTextOptions(indentLists: false) +) +``` + +**Example:** + +```swift +// Input +let md = """ +## Items + +- Top level + - Nested item + - Another nested +- Back to top +""" + +// With indentLists: true +// Items +// +// Top level +// Nested item +// Another nested +// Back to top + +// With indentLists: false +// Items +// +// Top level +// Nested item +// Another nested +// Back to top +``` + +### Code Block Preservation + +Choose whether to preserve or strip code blocks: + +```swift +// Preserve code (default) +let withCode = document.toPlainText( + options: PlainTextOptions(preserveCodeBlocks: true) +) + +// Strip code blocks +let noCode = document.toPlainText( + options: PlainTextOptions(preserveCodeBlocks: false) +) +``` + +**Example:** + +```swift +// Input +let md = """ +## Example + +Here's some code: + +```swift +func hello() { + print("Hello") +} +``` + +And some text after. +""" + +// With preserveCodeBlocks: true +// Example +// +// Here's some code: +// +// func hello() { +// print("Hello") +// } +// +// And some text after. + +// With preserveCodeBlocks: false +// Example +// +// Here's some code: +// +// And some text after. +``` + +## Preset Options + +### PlainTextPresets + +Use predefined configurations: + +```swift +// Default preset +let defaultText = document.toPlainText(options: .default) + +// Compact preset (single line breaks, no indentation) +let compactText = document.toPlainText(options: .compact) + +// Single line (everything on one line) +let singleLine = document.toPlainText(options: .singleLine) +``` + +### Default Preset + +Standard formatting for readability: + +```swift +PlainTextOptions( + blockSeparator: "\n\n", + indentLists: true, + preserveCodeBlocks: true +) +``` + +### Compact Preset + +Minimal spacing for dense output: + +```swift +PlainTextOptions( + blockSeparator: "\n", + indentLists: false, + preserveCodeBlocks: true +) +``` + +### Single Line Preset + +Everything on one line for previews: + +```swift +PlainTextOptions( + blockSeparator: " ", + indentLists: false, + preserveCodeBlocks: false +) +``` + +## Format Stripping + +### What Gets Removed + +The conversion strips all Markdown formatting: + +| Markdown | Plain Text | +|----------|------------| +| `**bold**` | `bold` | +| `*italic*` | `italic` | +| `~~strikethrough~~` | `strikethrough` | +| `[link](url)` | `link` | +| `![alt](image.png)` | `alt` | +| `` `code` `` | `code` | +| `# Heading` | `Heading` | + +### What Gets Preserved + +Content structure is maintained: + +- Paragraph breaks +- List structure (with optional indentation) +- Heading text +- Link text (URLs removed) +- Image alt text +- Code block content (optional) + +## Common Use Cases + +### Content Previews + +Generate preview text for articles: + +```swift +func generatePreview(from document: MarkdownDocument, maxLength: Int = 200) -> String { + let plainText = document.toPlainText(options: .compact) + + // Get first N characters + if plainText.count <= maxLength { + return plainText + } + + let preview = plainText.prefix(maxLength) + + // Find last space to avoid cutting words + if let lastSpace = preview.lastIndex(of: " ") { + return String(preview[.. [String: Any] { + // Convert to plain text + let plainText = document.toPlainText(options: .compact) + + // Extract metadata + let title = document.frontmatter.getValue(forKey: "title") as? String ?? "Untitled" + let tags = document.frontmatter.getValue(forKey: "tags") as? [String] ?? [] + + // Build search index + return [ + "title": title, + "content": plainText, + "tags": tags, + "wordCount": plainText.components(separatedBy: .whitespaces).count + ] +} +``` + +### Word Count + +Calculate accurate word counts: + +```swift +func wordCount(for document: MarkdownDocument) -> Int { + let plainText = document.toPlainText(options: .compact) + let words = plainText.components(separatedBy: .whitespaces) + return words.filter { !$0.isEmpty }.count +} + +let count = wordCount(for: document) +print("Word count: \(count)") +``` + +### Content Analysis + +Analyze text content: + +```swift +import NaturalLanguage + +func analyzeContent(_ document: MarkdownDocument) { + let plainText = document.toPlainText() + + // Language detection + let recognizer = NLLanguageRecognizer() + recognizer.processString(plainText) + if let language = recognizer.dominantLanguage { + print("Language: \(language.rawValue)") + } + + // Sentiment analysis + let tagger = NLTagger(tagSchemes: [.sentimentScore]) + tagger.string = plainText + let (sentiment, _) = tagger.tag(at: plainText.startIndex, + unit: .paragraph, + scheme: .sentimentScore) + print("Sentiment: \(sentiment?.rawValue ?? "neutral")") + + // Word frequency + let words = plainText.lowercased() + .components(separatedBy: .whitespacesAndNewlines) + .filter { !$0.isEmpty } + + let frequency = Dictionary(grouping: words, by: { $0 }) + .mapValues(\.count) + .sorted { $0.value > $1.value } + + print("Top words:") + for (word, count) in frequency.prefix(10) { + print(" \(word): \(count)") + } +} +``` + +### Email Content + +Prepare Markdown for plain-text emails: + +```swift +func convertToEmailBody(_ document: MarkdownDocument) -> String { + let options = PlainTextOptions( + blockSeparator: "\n\n", + indentLists: true, + preserveCodeBlocks: true + ) + + var plainText = document.toPlainText(options: options) + + // Wrap lines at 72 characters (email convention) + let wrapped = wrapLines(plainText, width: 72) + + return wrapped +} + +func wrapLines(_ text: String, width: Int) -> String { + text.components(separatedBy: .newlines) + .map { line in + guard line.count > width else { return line } + + var wrapped: [String] = [] + var current = "" + + for word in line.components(separatedBy: " ") { + if (current + " " + word).count > width { + if !current.isEmpty { + wrapped.append(current) + } + current = word + } else { + current += (current.isEmpty ? "" : " ") + word + } + } + + if !current.isEmpty { + wrapped.append(current) + } + + return wrapped.joined(separator: "\n") + } + .joined(separator: "\n") +} +``` + +### Accessibility + +Generate screen reader-friendly text: + +```swift +func accessibilityText(from document: MarkdownDocument) -> String { + // Use default options for natural reading + let plainText = document.toPlainText() + + // Add document metadata for context + var result = "" + + if let title = document.frontmatter.getValue(forKey: "title") as? String { + result += "Document title: \(title)\n\n" + } + + if let author = document.frontmatter.getValue(forKey: "author") as? String { + result += "Author: \(author)\n\n" + } + + result += plainText + + return result +} +``` + +## Advanced Techniques + +### Preserving Specific Formatting + +Custom conversion that preserves certain elements: + +```swift +import Markdown + +func convertPreservingEmphasis(_ document: MarkdownDocument) -> String { + let ast = document.parsedContent + var result = "" + + func process(_ element: Markup) -> String { + var text = "" + + if let emphasis = element as? Emphasis { + // Preserve italic with underscores + text += "_" + for child in emphasis.children { + text += process(child) + } + text += "_" + } else if let strong = element as? Strong { + // Preserve bold with asterisks + text += "**" + for child in strong.children { + text += process(child) + } + text += "**" + } else if let textElement = element as? Text { + text += textElement.string + } else { + for child in element.children { + text += process(child) + } + } + + return text + } + + for child in ast.children { + result += process(child) + "\n\n" + } + + return result.trimmingCharacters(in: .whitespacesAndNewlines) +} +``` + +### Custom Separator Logic + +Different separators for different block types: + +```swift +import Markdown + +func customConversion(_ document: MarkdownDocument) -> String { + let ast = document.parsedContent + var result: [String] = [] + + for element in ast.children { + if element is Heading { + // Extra spacing around headings + result.append("\n" + element.plainText + "\n") + } else if element is CodeBlock { + // Code blocks with markers + result.append("--- Code ---\n" + element.plainText + "\n--- End Code ---") + } else if element is BlockQuote { + // Indent quotes + let quoted = element.plainText + .components(separatedBy: .newlines) + .map { "> \($0)" } + .joined(separator: "\n") + result.append(quoted) + } else { + result.append(element.plainText) + } + } + + return result.joined(separator: "\n\n") +} +``` + +## Best Practices + +### Choose Appropriate Options + +Match options to your use case: + +```swift +// For reading/display +let readable = document.toPlainText(options: .default) + +// For compact storage +let compact = document.toPlainText(options: .compact) + +// For one-line previews +let preview = document.toPlainText(options: .singleLine) + +// For analysis (preserve structure) +let analysis = document.toPlainText( + options: PlainTextOptions( + blockSeparator: "\n", + indentLists: true, + preserveCodeBlocks: false + ) +) +``` + +### Handle Edge Cases + +Deal with empty content: + +```swift +let plainText = document.toPlainText() + +if plainText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + print("Document has no content") +} else { + print("Content: \(plainText)") +} +``` + +### Performance Considerations + +Cache converted text when needed: + +```swift +class DocumentCache { + private var plainTextCache: [String: String] = [:] + + func getPlainText(for document: MarkdownDocument) -> String { + let key = document.content + + if let cached = plainTextCache[key] { + return cached + } + + let plainText = document.toPlainText() + plainTextCache[key] = plainText + return plainText + } +} +``` + +## See Also + +- ``PlainTextOptions`` +- ``PlainTextPresets`` +- ``MarkdownDocument/toPlainText(options:)`` +- diff --git a/Sources/MarkdownUtilities/Documentation.docc/Articles/FrontMatter/CRUDOperations.md b/Sources/MarkdownUtilities/Documentation.docc/Articles/FrontMatter/CRUDOperations.md new file mode 100644 index 0000000..b2e6a7a --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/Articles/FrontMatter/CRUDOperations.md @@ -0,0 +1,485 @@ +# Frontmatter CRUD Operations + +Complete guide to creating, reading, updating, and deleting frontmatter. + +## Overview + +The ``FrontMatter`` type provides a complete set of CRUD (Create, Read, Update, Delete) operations for managing YAML metadata in Markdown documents. This guide covers all operations with comprehensive examples and error handling patterns. + +## Reading Values + +### Basic Reading + +Get a value from frontmatter using `getValue(forKey:)`: + +```swift +let document = MarkdownDocument(content: markdown) + +// Get a string value +if let title = document.frontmatter.getValue(forKey: "title") as? String { + print("Title: \(title)") +} + +// Get a number +if let count = document.frontmatter.getValue(forKey: "count") as? Int { + print("Count: \(count)") +} + +// Get a boolean +if let published = document.frontmatter.getValue(forKey: "published") as? Bool { + print("Published: \(published)") +} + +// Get an array +if let tags = document.frontmatter.getValue(forKey: "tags") as? [String] { + print("Tags: \(tags)") +} +``` + +### Checking for Keys + +Use `hasKey(_:)` to check if a key exists: + +```swift +let frontmatter = document.frontmatter + +if frontmatter.hasKey("author") { + print("Document has an author") +} + +// Check before accessing +if frontmatter.hasKey("date") { + let date = frontmatter.getValue(forKey: "date") + // Process date +} +``` + +### Type-Safe Reading + +Always cast to the expected type and handle type mismatches: + +```swift +// Safe approach with guard +guard let title = document.frontmatter.getValue(forKey: "title") as? String else { + print("Title is missing or not a string") + return +} + +// Safe approach with if-let +if let tags = document.frontmatter.getValue(forKey: "tags") as? [String] { + print("Tags: \(tags.joined(separator: ", "))") +} else { + print("No tags found or invalid format") +} + +// With default values +let author = document.frontmatter.getValue(forKey: "author") as? String ?? "Unknown" +let wordCount = document.frontmatter.getValue(forKey: "wordCount") as? Int ?? 0 +``` + +### Reading Nested Values + +Access nested properties using dot notation: + +```swift +// Frontmatter: +// author: +// name: Jane Doe +// email: jane@example.com + +let authorName = document.frontmatter.getValue(forKey: "author.name") as? String +let authorEmail = document.frontmatter.getValue(forKey: "author.email") as? String + +if let name = authorName, let email = authorEmail { + print("\(name) <\(email)>") +} +``` + +## Writing Values + +### Setting Values + +Use `setValue(_:forKey:)` to add or update values: + +```swift +var document = MarkdownDocument(content: markdown) + +// Set a string +try document.frontmatter.setValue("My Title", forKey: "title") + +// Set a number +try document.frontmatter.setValue(42, forKey: "count") + +// Set a boolean +try document.frontmatter.setValue(true, forKey: "published") + +// Set an array +try document.frontmatter.setValue(["swift", "programming"], forKey: "tags") + +// Set a date +try document.frontmatter.setValue(Date(), forKey: "updated") + +// Get the modified document +let updatedMarkdown = document.content +``` + +> Important: `setValue(_:forKey:)` can throw errors. Always use try and handle potential failures. + +### Setting Multiple Values + +Set multiple values efficiently: + +```swift +var document = MarkdownDocument(content: markdown) + +do { + try document.frontmatter.setValue("New Title", forKey: "title") + try document.frontmatter.setValue("Jane Doe", forKey: "author") + try document.frontmatter.setValue(Date(), forKey: "updated") + try document.frontmatter.setValue(true, forKey: "published") + + // Save the modified document + let updatedContent = document.content + try updatedContent.write(to: fileURL, atomically: true, encoding: .utf8) + +} catch { + print("Error updating frontmatter: \(error)") +} +``` + +### Setting Nested Values + +Create nested structures: + +```swift +var document = MarkdownDocument(content: markdown) + +// Set nested author information +try document.frontmatter.setValue("Jane Doe", forKey: "author.name") +try document.frontmatter.setValue("jane@example.com", forKey: "author.email") +try document.frontmatter.setValue("@janedoe", forKey: "author.twitter") + +// Results in: +// author: +// name: Jane Doe +// email: jane@example.com +// twitter: "@janedoe" +``` + +### Type Considerations + +MarkdownUtilities supports standard Swift types: + +```swift +// Strings +try document.frontmatter.setValue("text", forKey: "key") + +// Numbers +try document.frontmatter.setValue(42, forKey: "count") +try document.frontmatter.setValue(3.14, forKey: "pi") + +// Booleans +try document.frontmatter.setValue(true, forKey: "flag") + +// Arrays +try document.frontmatter.setValue([1, 2, 3], forKey: "numbers") +try document.frontmatter.setValue(["a", "b"], forKey: "letters") + +// Dates (converted to ISO 8601) +try document.frontmatter.setValue(Date(), forKey: "timestamp") + +// Dictionaries +try document.frontmatter.setValue( + ["name": "Jane", "age": 30], + forKey: "person" +) +``` + +## Updating Values + +### Conditional Updates + +Update values only if they don't exist or meet certain criteria: + +```swift +var document = MarkdownDocument(content: markdown) + +// Set default value if missing +if !document.frontmatter.hasKey("draft") { + try document.frontmatter.setValue(true, forKey: "draft") +} + +// Update if different +let currentTitle = document.frontmatter.getValue(forKey: "title") as? String +if currentTitle != "New Title" { + try document.frontmatter.setValue("New Title", forKey: "title") +} + +// Update timestamp +try document.frontmatter.setValue(Date(), forKey: "modified") +``` + +### Batch Updates + +Update multiple documents: + +```swift +func updateAllDocuments(in directory: URL) throws { + let fileManager = FileManager.default + let files = try fileManager.contentsOfDirectory(at: directory, + includingPropertiesForKeys: nil) + + for fileURL in files where fileURL.pathExtension == "md" { + let content = try String(contentsOf: fileURL, encoding: .utf8) + var document = MarkdownDocument(content: content) + + // Update frontmatter + try document.frontmatter.setValue(Date(), forKey: "updated") + try document.frontmatter.setValue("batch-update", forKey: "processedBy") + + // Save + try document.content.write(to: fileURL, atomically: true, encoding: .utf8) + } +} +``` + +### Transforming Values + +Read, transform, and write back: + +```swift +var document = MarkdownDocument(content: markdown) + +// Append to existing array +if var tags = document.frontmatter.getValue(forKey: "tags") as? [String] { + tags.append("new-tag") + try document.frontmatter.setValue(tags, forKey: "tags") +} + +// Increment counter +if let count = document.frontmatter.getValue(forKey: "count") as? Int { + try document.frontmatter.setValue(count + 1, forKey: "count") +} + +// Convert to uppercase +if let title = document.frontmatter.getValue(forKey: "title") as? String { + try document.frontmatter.setValue(title.uppercased(), forKey: "title") +} +``` + +## Deleting Values + +### Removing Keys + +Use `removeValue(forKey:)` to delete a key: + +```swift +var document = MarkdownDocument(content: markdown) + +// Remove a single key +try document.frontmatter.removeValue(forKey: "draft") + +// Remove multiple keys +let keysToRemove = ["temp", "scratch", "debug"] +for key in keysToRemove { + if document.frontmatter.hasKey(key) { + try document.frontmatter.removeValue(forKey: key) + } +} +``` + +### Conditional Removal + +Remove keys based on conditions: + +```swift +var document = MarkdownDocument(content: markdown) + +// Remove if value is nil or empty +if let tags = document.frontmatter.getValue(forKey: "tags") as? [String], + tags.isEmpty { + try document.frontmatter.removeValue(forKey: "tags") +} + +// Remove deprecated fields +let deprecatedFields = ["old_field", "legacy_property"] +for field in deprecatedFields { + if document.frontmatter.hasKey(field) { + try document.frontmatter.removeValue(forKey: field) + } +} +``` + +## Renaming Keys + +### Basic Renaming + +Use `renameKey(from:to:)` to rename a key: + +```swift +var document = MarkdownDocument(content: markdown) + +// Rename a key +try document.frontmatter.renameKey(from: "old_name", to: "new_name") + +// The value is preserved, only the key changes +``` + +### Bulk Renaming + +Rename multiple keys: + +```swift +var document = MarkdownDocument(content: markdown) + +let renames = [ + "created_date": "created", + "modified_date": "modified", + "post_title": "title" +] + +for (oldKey, newKey) in renames { + if document.frontmatter.hasKey(oldKey) { + try document.frontmatter.renameKey(from: oldKey, to: newKey) + } +} +``` + +### Schema Migration + +Migrate from one schema to another: + +```swift +func migrateSchema(_ document: inout MarkdownDocument) throws { + // Rename keys for new schema + if document.frontmatter.hasKey("tags") { + try document.frontmatter.renameKey(from: "tags", to: "keywords") + } + + // Convert string to array + if let category = document.frontmatter.getValue(forKey: "category") as? String { + try document.frontmatter.setValue([category], forKey: "categories") + try document.frontmatter.removeValue(forKey: "category") + } + + // Add version field + try document.frontmatter.setValue(2, forKey: "schema_version") +} +``` + +## Error Handling + +### Handling Errors Gracefully + +CRUD operations can fail for various reasons: + +```swift +var document = MarkdownDocument(content: markdown) + +do { + try document.frontmatter.setValue("value", forKey: "key") +} catch { + print("Failed to set value: \(error.localizedDescription)") +} + +// More specific error handling +do { + try document.frontmatter.setValue(complexValue, forKey: "key") +} catch let error as FrontMatterError { + // Handle specific frontmatter errors + print("Frontmatter error: \(error)") +} catch { + // Handle other errors + print("Unexpected error: \(error)") +} +``` + +### Validation Before Writing + +Validate values before setting: + +```swift +func setTitle(_ title: String, for document: inout MarkdownDocument) throws { + // Validate + guard !title.trimmingCharacters(in: .whitespaces).isEmpty else { + throw ValidationError.emptyTitle + } + + guard title.count <= 200 else { + throw ValidationError.titleTooLong + } + + // Set if valid + try document.frontmatter.setValue(title, forKey: "title") +} +``` + +## Complete CRUD Example + +Here's a comprehensive example demonstrating all operations: + +```swift +import Foundation +import MarkdownUtilities + +// Read a Markdown file +let fileURL = URL(fileURLWithPath: "post.md") +let content = try String(contentsOf: fileURL, encoding: .utf8) +var document = MarkdownDocument(content: content) + +// READ operations +print("Current title:", document.frontmatter.getValue(forKey: "title") as? String ?? "None") + +if document.frontmatter.hasKey("tags") { + let tags = document.frontmatter.getValue(forKey: "tags") as? [String] ?? [] + print("Tags:", tags) +} + +// CREATE/UPDATE operations +try document.frontmatter.setValue("Updated Title", forKey: "title") +try document.frontmatter.setValue(Date(), forKey: "modified") + +// Add new field if missing +if !document.frontmatter.hasKey("author") { + try document.frontmatter.setValue("Jane Doe", forKey: "author") +} + +// Update existing array +if var tags = document.frontmatter.getValue(forKey: "tags") as? [String] { + tags.append("updated") + try document.frontmatter.setValue(tags, forKey: "tags") +} + +// RENAME operation +if document.frontmatter.hasKey("created_date") { + try document.frontmatter.renameKey(from: "created_date", to: "created") +} + +// DELETE operation +if document.frontmatter.hasKey("draft") { + let isDraft = document.frontmatter.getValue(forKey: "draft") as? Bool ?? false + if !isDraft { + try document.frontmatter.removeValue(forKey: "draft") + } +} + +// Write back to file +try document.content.write(to: fileURL, atomically: true, encoding: .utf8) +print("✓ Document updated successfully") +``` + +## Best Practices + +1. **Always handle errors**: Use do-catch blocks for all CRUD operations +2. **Validate input**: Check values before setting them +3. **Check key existence**: Use `hasKey(_:)` before accessing or removing +4. **Use type-safe casting**: Always cast `getValue` results to expected types +5. **Preserve data integrity**: Make backups before batch operations +6. **Document your schema**: Define expected frontmatter fields clearly + +## See Also + +- ``FrontMatter`` +- ``MarkdownDocument`` +- +- diff --git a/Sources/MarkdownUtilities/Documentation.docc/Articles/FrontMatter/FrontMatterOverview.md b/Sources/MarkdownUtilities/Documentation.docc/Articles/FrontMatter/FrontMatterOverview.md new file mode 100644 index 0000000..21846b4 --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/Articles/FrontMatter/FrontMatterOverview.md @@ -0,0 +1,368 @@ +# Frontmatter Overview + +Understanding YAML frontmatter in Markdown documents. + +## Overview + +Frontmatter is YAML-formatted metadata placed at the beginning of Markdown documents. It's widely used in static site generators like Hugo and Jekyll, note-taking apps like Obsidian, and content management systems to store structured data alongside Markdown content. + +## What is Frontmatter? + +Frontmatter consists of YAML data enclosed between triple-dash delimiters (`---`): + +```markdown +--- +title: My Blog Post +date: 2024-01-24 +author: Jane Doe +tags: [swift, programming, tutorial] +published: true +--- + +# My Blog Post + +The actual Markdown content starts here. +``` + +The frontmatter block must: +- Start at the very beginning of the file +- Be enclosed in `---` delimiters +- Contain valid YAML syntax +- Be separated from the body by a blank line (recommended) + +## Common Use Cases + +### Static Site Generators + +Hugo, Jekyll, and other static site generators use frontmatter for: + +```yaml +--- +title: "Understanding Swift Concurrency" +date: 2024-01-24T10:00:00Z +draft: false +categories: [programming, swift] +tags: [swift, async-await, concurrency] +author: Jane Doe +description: "A comprehensive guide to Swift concurrency" +--- +``` + +### Note-Taking Apps + +Obsidian and similar apps use frontmatter for metadata: + +```yaml +--- +title: Project Meeting Notes +created: 2024-01-24 +tags: [meetings, project-alpha] +status: active +attendees: [Alice, Bob, Charlie] +--- +``` + +### Documentation + +Technical documentation often includes: + +```yaml +--- +title: API Reference +version: 1.0.0 +category: reference +last-updated: 2024-01-24 +--- +``` + +## YAML Syntax Quick Reference + +### Strings + +```yaml +# Simple strings +title: My Title + +# Quoted strings (for special characters) +title: "Title: With Colon" +title: 'Single quotes work too' + +# Multi-line strings +description: | + This is a multi-line + description that preserves + line breaks. +``` + +### Numbers and Booleans + +```yaml +# Numbers +count: 42 +price: 19.99 + +# Booleans +published: true +draft: false +``` + +### Arrays + +```yaml +# Inline array +tags: [swift, programming, tutorial] + +# Block array +categories: + - Programming + - Swift + - Tutorial +``` + +### Objects (Nested Data) + +```yaml +# Nested object +author: + name: Jane Doe + email: jane@example.com + twitter: "@janedoe" + +# Access in Swift: +# let name = frontmatter.getValue(forKey: "author.name") +``` + +### Dates + +```yaml +# ISO 8601 format +date: 2024-01-24 +datetime: 2024-01-24T10:30:00Z + +# Alternative formats (interpreted as strings) +published: "January 24, 2024" +``` + +## Accessing Frontmatter in MarkdownUtilities + +### Basic Access + +```swift +let document = MarkdownDocument(content: markdown) + +// Get a string value +if let title = document.frontmatter.getValue(forKey: "title") as? String { + print("Title: \(title)") +} + +// Get a boolean +if let published = document.frontmatter.getValue(forKey: "published") as? Bool { + print("Published: \(published)") +} + +// Get an array +if let tags = document.frontmatter.getValue(forKey: "tags") as? [String] { + print("Tags: \(tags.joined(separator: ", "))") +} +``` + +### Type Safety + +Always cast values to the expected type: + +```swift +// Safe approach with optional binding +if let count = document.frontmatter.getValue(forKey: "count") as? Int { + print("Count: \(count)") +} else { + print("Count is missing or not an integer") +} + +// Check if key exists first +if document.frontmatter.hasKey("author") { + if let author = document.frontmatter.getValue(forKey: "author") as? String { + print("Author: \(author)") + } +} +``` + +## Integration with Popular Tools + +### Hugo + +Hugo expects specific frontmatter fields: + +```yaml +--- +title: "Post Title" +date: 2024-01-24T10:00:00Z +draft: false +tags: [tag1, tag2] +categories: [category1] +summary: "Brief description" +--- +``` + +MarkdownUtilities works seamlessly with Hugo's frontmatter format. + +### Jekyll + +Jekyll uses similar YAML frontmatter: + +```yaml +--- +layout: post +title: "Post Title" +date: 2024-01-24 10:00:00 +0000 +categories: [category1, category2] +tags: [tag1, tag2] +--- +``` + +### Obsidian + +Obsidian supports flexible frontmatter: + +```yaml +--- +tags: [note, important] +created: 2024-01-24 +modified: 2024-01-24 +status: active +--- +``` + +Obsidian also supports inline fields, but MarkdownUtilities focuses on standard YAML frontmatter. + +## Best Practices + +### Use Consistent Keys + +Standardize field names across your documents: + +```yaml +# Good: Consistent naming +title: "My Post" +created: 2024-01-24 +modified: 2024-01-24 + +# Avoid: Inconsistent naming +title: "My Post" +created_date: 2024-01-24 +date_modified: 2024-01-24 +``` + +### Choose Appropriate Types + +Use the right YAML type for each field: + +```yaml +# Good +count: 42 # Number +published: true # Boolean +tags: [a, b] # Array + +# Avoid +count: "42" # String (harder to process) +published: "true" # String (not a boolean) +tags: "a, b" # String (not an array) +``` + +### Document Your Schema + +Define a schema for your frontmatter: + +```swift +// Define expected fields +enum FrontmatterField: String { + case title + case date + case author + case tags + case published +} + +// Validate against schema +func validate(_ document: MarkdownDocument) -> Bool { + let required: [FrontmatterField] = [.title, .date] + + for field in required { + guard document.frontmatter.hasKey(field.rawValue) else { + return false + } + } + + return true +} +``` + +### Handle Missing Fields Gracefully + +Always provide defaults or handle missing fields: + +```swift +// Provide default values +let title = document.frontmatter.getValue(forKey: "title") as? String ?? "Untitled" +let tags = document.frontmatter.getValue(forKey: "tags") as? [String] ?? [] +let published = document.frontmatter.getValue(forKey: "published") as? Bool ?? false +``` + +## Common Patterns + +### Required vs Optional Fields + +```swift +struct PostMetadata { + // Required fields + let title: String + let date: Date + + // Optional fields + let author: String? + let tags: [String]? + + init?(from frontmatter: FrontMatter) { + // Required fields must exist + guard let title = frontmatter.getValue(forKey: "title") as? String, + let date = frontmatter.getValue(forKey: "date") as? Date else { + return nil + } + + self.title = title + self.date = date + self.author = frontmatter.getValue(forKey: "author") as? String + self.tags = frontmatter.getValue(forKey: "tags") as? [String] + } +} +``` + +### Nested Metadata + +```yaml +--- +author: + name: Jane Doe + email: jane@example.com + social: + twitter: "@janedoe" + github: "janedoe" +--- +``` + +```swift +// Access nested values using dot notation +let authorName = document.frontmatter.getValue(forKey: "author.name") as? String +let twitter = document.frontmatter.getValue(forKey: "author.social.twitter") as? String +``` + +## Next Steps + +Learn how to perform CRUD operations on frontmatter: + +- - Complete guide to reading, writing, and updating frontmatter + +## See Also + +- ``FrontMatter`` +- ``MarkdownDocument`` +- diff --git a/Sources/MarkdownUtilities/Documentation.docc/Articles/GettingStarted.md b/Sources/MarkdownUtilities/Documentation.docc/Articles/GettingStarted.md new file mode 100644 index 0000000..354f21d --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/Articles/GettingStarted.md @@ -0,0 +1,159 @@ +# Getting Started with MarkdownUtilities + +Learn how to integrate and use MarkdownUtilities in your Swift projects. + +## Overview + +MarkdownUtilities is a Swift library that makes it easy to work with Markdown documents. This guide will help you get started quickly with parsing Markdown files, accessing frontmatter, and performing common operations. + +## Installation + +Add MarkdownUtilities as a dependency to your Swift package: + +```swift +// Package.swift +let package = Package( + name: "YourPackage", + platforms: [ + .macOS(.v13), + .iOS(.v16) + ], + dependencies: [ + .package(url: "https://github.com/yourusername/md-utils.git", from: "1.0.0") + ], + targets: [ + .target( + name: "YourTarget", + dependencies: [ + .product(name: "MarkdownUtilities", package: "md-utils") + ] + ) + ] +) +``` + +## Your First Markdown Document + +Here's a simple example that demonstrates the core functionality: + +```swift +import MarkdownUtilities + +// Create a Markdown document with frontmatter +let markdown = """ +--- +title: My First Document +author: Jane Doe +tags: [swift, markdown] +--- + +# Welcome + +This is a **Markdown** document with frontmatter. +""" + +// Parse the document +let document = MarkdownDocument(content: markdown) + +// Access frontmatter +if let title = document.frontmatter.getValue(forKey: "title") as? String { + print("Title: \(title)") // Prints: Title: My First Document +} + +// Get the body content (without frontmatter) +let body = document.body +print(body) +// Prints: +// # Welcome +// +// This is a **Markdown** document with frontmatter. +``` + +## Common Operations + +### Reading Frontmatter + +Access frontmatter values using type-safe getters: + +```swift +let document = MarkdownDocument(content: markdown) + +// Get a string value +if let title = document.frontmatter.getValue(forKey: "title") as? String { + print("Title: \(title)") +} + +// Get an array value +if let tags = document.frontmatter.getValue(forKey: "tags") as? [String] { + print("Tags: \(tags.joined(separator: ", "))") +} + +// Check if a key exists +if document.frontmatter.hasKey("author") { + print("Document has an author") +} +``` + +### Modifying Frontmatter + +Update frontmatter values and render the modified document: + +```swift +var document = MarkdownDocument(content: markdown) + +// Set a new value +try document.frontmatter.setValue("Updated Title", forKey: "title") + +// Add a new field +try document.frontmatter.setValue(Date(), forKey: "updated") + +// Get the modified document +let updatedMarkdown = document.content +``` + +### Generating a Table of Contents + +Extract headings and create a TOC: + +```swift +let document = MarkdownDocument(content: markdown) + +// Generate TOC with default options +let toc = try document.generateTOC() + +// Access TOC entries +for entry in toc.entries { + let indent = String(repeating: " ", count: entry.level - 1) + print("\(indent)- \(entry.text)") +} +``` + +### Converting to Plain Text + +Strip Markdown formatting and extract plain text: + +```swift +let document = MarkdownDocument(content: markdown) + +// Convert with default options +let plainText = document.toPlainText() +print(plainText) + +// Convert with custom options +let compactText = document.toPlainText(options: .compact) +``` + +## Next Steps + +Now that you've learned the basics, explore these topics: + +- - Detailed integration instructions +- - Deep dive into the core type +- - Complete frontmatter reference +- - Advanced TOC generation + +## See Also + +- ``MarkdownDocument`` +- ``FrontMatter`` +- ``TableOfContents`` diff --git a/Sources/MarkdownUtilities/Documentation.docc/Articles/IntegrationGuide.md b/Sources/MarkdownUtilities/Documentation.docc/Articles/IntegrationGuide.md new file mode 100644 index 0000000..6125c0b --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/Articles/IntegrationGuide.md @@ -0,0 +1,282 @@ +# Integration Guide + +Detailed instructions for integrating MarkdownUtilities into your project. + +## Overview + +This guide covers everything you need to know about adding MarkdownUtilities to your Swift project, including platform requirements, dependency configuration, and common integration patterns. + +## Requirements + +MarkdownUtilities has the following requirements: + +- **Swift**: 6.2 or later +- **macOS**: 13.0 or later +- **iOS**: 16.0 or later +- **tvOS**: 16.0 or later +- **watchOS**: 9.0 or later +- **Package Manager**: Swift Package Manager (SPM) + +## Adding the Dependency + +### For Applications + +Add MarkdownUtilities to your app's Package.swift dependencies: + +```swift +// Package.swift +let package = Package( + name: "MyApp", + platforms: [ + .macOS(.v13), + .iOS(.v16) + ], + dependencies: [ + .package(url: "https://github.com/yourusername/md-utils.git", from: "1.0.0") + ], + targets: [ + .executableTarget( + name: "MyApp", + dependencies: [ + .product(name: "MarkdownUtilities", package: "md-utils") + ] + ) + ] +) +``` + +### For Libraries + +If you're building a library that uses MarkdownUtilities: + +```swift +// Package.swift +let package = Package( + name: "MyLibrary", + platforms: [ + .macOS(.v13), + .iOS(.v16) + ], + products: [ + .library(name: "MyLibrary", targets: ["MyLibrary"]) + ], + dependencies: [ + .package(url: "https://github.com/yourusername/md-utils.git", from: "1.0.0") + ], + targets: [ + .target( + name: "MyLibrary", + dependencies: [ + .product(name: "MarkdownUtilities", package: "md-utils") + ] + ) + ] +) +``` + +### For Xcode Projects + +1. Open your project in Xcode +2. Go to File → Add Package Dependencies... +3. Enter the repository URL: `https://github.com/yourusername/md-utils.git` +4. Choose the version requirements +5. Select "MarkdownUtilities" from the available products +6. Add it to your target + +## Importing the Module + +Import MarkdownUtilities in your Swift files: + +```swift +import MarkdownUtilities + +// Now you can use MarkdownDocument and other types +let document = MarkdownDocument(content: "# Hello") +``` + +## Common Integration Patterns + +### File-Based Processing + +Read Markdown files from disk and process them: + +```swift +import Foundation +import MarkdownUtilities + +func processMarkdownFile(at url: URL) throws { + // Read file content + let content = try String(contentsOf: url, encoding: .utf8) + + // Parse as Markdown document + var document = MarkdownDocument(content: content) + + // Modify frontmatter + try document.frontmatter.setValue(Date(), forKey: "processed") + + // Write back to file + try document.content.write(to: url, atomically: true, encoding: .utf8) +} +``` + +### Directory Scanning + +Process all Markdown files in a directory: + +```swift +import Foundation +import MarkdownUtilities + +func processDirectory(at url: URL) throws { + let fileManager = FileManager.default + + guard let enumerator = fileManager.enumerator( + at: url, + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles] + ) else { + return + } + + for case let fileURL as URL in enumerator { + // Check if it's a Markdown file + guard fileURL.pathExtension == "md" else { continue } + + // Process the file + let content = try String(contentsOf: fileURL, encoding: .utf8) + let document = MarkdownDocument(content: content) + + // Your processing logic here + print("Processing: \(fileURL.lastPathComponent)") + } +} +``` + +### Stream Processing + +Process Markdown content from streams or APIs: + +```swift +import MarkdownUtilities + +func processMarkdownFromAPI(content: String) -> String { + var document = MarkdownDocument(content: content) + + // Extract plain text for preview + let preview = String(document.toPlainText().prefix(200)) + + // Return preview + return preview + "..." +} +``` + +### Validation + +Validate Markdown documents for required frontmatter: + +```swift +import MarkdownUtilities + +struct ValidationError: Error { + let message: String +} + +func validateBlogPost(_ document: MarkdownDocument) throws { + // Check required fields + let requiredFields = ["title", "date", "author"] + + for field in requiredFields { + guard document.frontmatter.hasKey(field) else { + throw ValidationError(message: "Missing required field: \(field)") + } + } + + // Validate title is not empty + if let title = document.frontmatter.getValue(forKey: "title") as? String, + title.trimmingCharacters(in: .whitespaces).isEmpty { + throw ValidationError(message: "Title cannot be empty") + } + + print("✓ Document is valid") +} +``` + +### Batch Processing + +Process multiple documents with error handling: + +```swift +import MarkdownUtilities + +func batchProcess(files: [URL]) { + var successCount = 0 + var errors: [(URL, Error)] = [] + + for fileURL in files { + do { + let content = try String(contentsOf: fileURL, encoding: .utf8) + var document = MarkdownDocument(content: content) + + // Process document + try document.frontmatter.setValue(Date(), forKey: "updated") + + // Save + try document.content.write(to: fileURL, atomically: true, encoding: .utf8) + successCount += 1 + + } catch { + errors.append((fileURL, error)) + } + } + + print("Processed \(successCount) files successfully") + if !errors.isEmpty { + print("Errors: \(errors.count)") + for (url, error) in errors { + print(" - \(url.lastPathComponent): \(error)") + } + } +} +``` + +## Platform Considerations + +### macOS + +MarkdownUtilities works great for: +- Static site generators +- Documentation tools +- Content management systems +- Markdown editors + +### iOS/iPadOS + +Ideal for: +- Note-taking apps +- Markdown editors +- Blog writing apps +- Documentation viewers + +### tvOS and watchOS + +While supported, MarkdownUtilities is primarily designed for macOS and iOS. Consider the limited storage and processing capabilities of these platforms. + +## Performance Tips + +For optimal performance when processing large numbers of files: + +1. **Minimize parsing**: Cache ``MarkdownDocument`` instances when possible +2. **Batch operations**: Group file operations to reduce I/O overhead +3. **Selective processing**: Only parse the parts you need (frontmatter vs full AST) +4. **Use generators**: For TOC generation, use appropriate level filters + +## Next Steps + +- - Quick start guide +- - Learn about the core type +- - Master frontmatter operations + +## See Also + +- ``MarkdownDocument`` +- ``FrontMatter`` diff --git a/Sources/MarkdownUtilities/Documentation.docc/Articles/MarkdownDocument.md b/Sources/MarkdownUtilities/Documentation.docc/Articles/MarkdownDocument.md new file mode 100644 index 0000000..7a3d383 --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/Articles/MarkdownDocument.md @@ -0,0 +1,403 @@ +# Working with MarkdownDocument + +Deep dive into the core MarkdownDocument type. + +## Overview + +``MarkdownDocument`` is the foundation of MarkdownUtilities. It represents a complete Markdown document with optional YAML frontmatter, providing a rich API for parsing, manipulation, and rendering. + +## Document Structure + +A ``MarkdownDocument`` consists of two main parts: + +1. **Frontmatter**: Optional YAML metadata at the document's beginning +2. **Body**: The actual Markdown content + +```markdown +--- +title: My Document +date: 2024-01-24 +--- + +# Heading + +Content goes here. +``` + +## Creating Documents + +### From String Content + +The most common way to create a document: + +```swift +let markdown = """ +--- +title: My Document +--- + +# Hello World +""" + +let document = MarkdownDocument(content: markdown) +``` + +### From File + +Read a Markdown file from disk: + +```swift +import Foundation + +let fileURL = URL(fileURLWithPath: "/path/to/document.md") +let content = try String(contentsOf: fileURL, encoding: .utf8) +let document = MarkdownDocument(content: content) +``` + +### Without Frontmatter + +Documents don't require frontmatter: + +```swift +let markdown = """ +# Just Markdown + +No frontmatter here. +""" + +let document = MarkdownDocument(content: markdown) +// document.frontmatter will be empty but still accessible +``` + +## Accessing Content + +### Getting the Full Document + +Access the complete document as a string: + +```swift +let document = MarkdownDocument(content: markdown) +let fullContent = document.content +``` + +### Getting the Body + +Access just the Markdown content without frontmatter: + +```swift +let document = MarkdownDocument(content: markdown) +let body = document.body + +// body contains only the Markdown content: +// # Hello World +``` + +### Getting Frontmatter + +Access the frontmatter directly: + +```swift +let document = MarkdownDocument(content: markdown) +let frontmatter = document.frontmatter + +// Check if frontmatter exists +if frontmatter.hasKey("title") { + print("Document has a title") +} +``` + +## Parsing to AST + +Convert Markdown to an abstract syntax tree for advanced processing: + +```swift +import Markdown + +let document = MarkdownDocument(content: markdown) + +// Parse body to AST +let ast = document.parsedContent + +// Traverse the AST +for child in ast.children { + if let heading = child as? Heading { + print("Found heading: \(heading.plainText)") + } +} +``` + +The parsed content is a `Document` from the [swift-markdown](https://github.com/apple/swift-markdown) library, giving you full access to the AST for complex transformations. + +## Modifying Documents + +### Updating Frontmatter + +Modify frontmatter and get the updated document: + +```swift +var document = MarkdownDocument(content: markdown) + +// Update a field +try document.frontmatter.setValue("New Title", forKey: "title") + +// Add a new field +try document.frontmatter.setValue(["swift", "markdown"], forKey: "tags") + +// Get the modified document +let updatedContent = document.content +``` + +The `content` property automatically reconstructs the document with the modified frontmatter. + +### Updating Body Content + +Replace the body while preserving frontmatter: + +```swift +var document = MarkdownDocument(content: markdown) + +// Create new body content +let newBody = """ +# Updated Heading + +New content here. +""" + +// Reconstruct document with new body +let newContent = """ +\(document.frontmatter.rawContent) + +\(newBody) +""" + +document = MarkdownDocument(content: newContent) +``` + +## Round-Trip Editing + +Parse, modify, and render while preserving structure: + +```swift +// Original document +let original = """ +--- +title: Original +author: Jane +--- + +# Content + +Some text. +""" + +// Parse +var document = MarkdownDocument(content: original) + +// Modify +try document.frontmatter.setValue("Modified", forKey: "title") +try document.frontmatter.setValue(Date(), forKey: "updated") + +// Render back to string +let modified = document.content + +// Save to file +try modified.write(to: fileURL, atomically: true, encoding: .utf8) +``` + +## Advanced Operations + +### Generating Table of Contents + +Extract document structure: + +```swift +let document = MarkdownDocument(content: markdown) + +// Generate TOC with options +let options = TOCOptions( + minLevel: 2, + maxLevel: 4, + includePosition: true +) + +let toc = try document.generateTOC(options: options) + +// Process entries +for entry in toc.entries { + print("\(String(repeating: " ", count: entry.level - 1))\(entry.text)") +} +``` + +See for complete TOC documentation. + +### Converting to Plain Text + +Strip formatting for previews or search indexing: + +```swift +let document = MarkdownDocument(content: markdown) + +// Default conversion +let plainText = document.toPlainText() + +// Custom options +let options = PlainTextOptions( + blockSeparator: "\n\n", + indentLists: true, + preserveCodeBlocks: true +) +let customPlainText = document.toPlainText(options: options) +``` + +See for conversion options. + +### Extracting Headings + +Get all headings from a document: + +```swift +import Markdown + +let document = MarkdownDocument(content: markdown) +let ast = document.parsedContent + +var headings: [(level: Int, text: String)] = [] + +for element in ast.children { + if let heading = element as? Heading { + headings.append((heading.level, heading.plainText)) + } +} + +for (level, text) in headings { + let prefix = String(repeating: "#", count: level) + print("\(prefix) \(text)") +} +``` + +## Best Practices + +### Always Handle Errors + +Frontmatter operations can throw errors: + +```swift +do { + var document = MarkdownDocument(content: markdown) + try document.frontmatter.setValue("value", forKey: "key") + print("Success") +} catch { + print("Error: \(error)") +} +``` + +### Use Value Types + +``MarkdownDocument`` is a value type (struct), so modifications create new instances: + +```swift +let original = MarkdownDocument(content: markdown) +var modified = original // Creates a copy + +try modified.frontmatter.setValue("New", forKey: "title") + +// original is unchanged +// modified has the new title +``` + +### Cache Parsed Content + +If you're working extensively with the AST, cache the parsed content: + +```swift +let document = MarkdownDocument(content: markdown) +let ast = document.parsedContent // Cache this + +// Perform multiple AST operations on the cached ast +// instead of calling parsedContent repeatedly +``` + +### Validate Input + +Always validate frontmatter values match your schema: + +```swift +func validateDocument(_ document: MarkdownDocument) -> Bool { + // Check required fields + guard document.frontmatter.hasKey("title"), + document.frontmatter.hasKey("date") else { + return false + } + + // Check types + guard document.frontmatter.getValue(forKey: "title") is String else { + return false + } + + return true +} +``` + +## Common Patterns + +### Template Processing + +Create documents from templates: + +```swift +func createFromTemplate(title: String, author: String) -> MarkdownDocument { + let template = """ + --- + title: \(title) + author: \(author) + date: \(Date()) + draft: true + --- + + # \(title) + + Write your content here. + """ + + return MarkdownDocument(content: template) +} + +let newDoc = createFromTemplate(title: "My Post", author: "Jane Doe") +``` + +### Metadata Extraction + +Extract all metadata for indexing: + +```swift +struct DocumentMetadata { + let title: String? + let author: String? + let tags: [String]? + let wordCount: Int +} + +func extractMetadata(from document: MarkdownDocument) -> DocumentMetadata { + let title = document.frontmatter.getValue(forKey: "title") as? String + let author = document.frontmatter.getValue(forKey: "author") as? String + let tags = document.frontmatter.getValue(forKey: "tags") as? [String] + + let plainText = document.toPlainText() + let wordCount = plainText.split(separator: " ").count + + return DocumentMetadata( + title: title, + author: author, + tags: tags, + wordCount: wordCount + ) +} +``` + +## See Also + +- ``MarkdownDocument`` +- ``FrontMatter`` +- +- +- diff --git a/Sources/MarkdownUtilities/Documentation.docc/Articles/TableOfContents/GeneratingTOC.md b/Sources/MarkdownUtilities/Documentation.docc/Articles/TableOfContents/GeneratingTOC.md new file mode 100644 index 0000000..781cc5f --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/Articles/TableOfContents/GeneratingTOC.md @@ -0,0 +1,600 @@ +# Generating Table of Contents + +Complete guide to TOC generation with all configuration options. + +## Overview + +MarkdownUtilities provides flexible TOC generation through the ``MarkdownDocument/generateTOC(options:)`` method. This guide covers all configuration options, output formats, and advanced use cases. + +## Basic Generation + +### Default Options + +Generate a TOC with default settings: + +```swift +import MarkdownUtilities + +let markdown = """ +# Main Title + +## Section 1 + +### Subsection 1.1 + +## Section 2 + +### Subsection 2.1 + +### Subsection 2.2 +""" + +let document = MarkdownDocument(content: markdown) +let toc = try document.generateTOC() + +// Access entries +for entry in toc.entries { + print("Level \(entry.level): \(entry.text)") +} +``` + +Default behavior: +- Includes all heading levels (1-6) +- Preserves hierarchical structure +- Generates slugs for all headings +- Does not include position information + +### With Custom Options + +Configure TOC generation with ``TOCOptions``: + +```swift +let options = TOCOptions( + minLevel: 2, + maxLevel: 4, + includePosition: true, + generateSlugs: true, + flat: false +) + +let toc = try document.generateTOC(options: options) +``` + +## Configuration Options + +### Heading Level Filtering + +Control which heading levels are included: + +```swift +// Only include h2 and h3 +let options = TOCOptions(minLevel: 2, maxLevel: 3) +let toc = try document.generateTOC(options: options) + +// Only h1 (main sections) +let mainSections = try document.generateTOC( + options: TOCOptions(minLevel: 1, maxLevel: 1) +) + +// h2 through h4 (common for documentation) +let docTOC = try document.generateTOC( + options: TOCOptions(minLevel: 2, maxLevel: 4) +) +``` + +**Use cases:** +- Skip h1 when it's the page title +- Limit depth for cleaner navigation +- Extract specific heading levels for analysis + +### Slug Generation + +Control whether to generate URL-safe slugs: + +```swift +// With slugs (default) +let options = TOCOptions(generateSlugs: true) +let toc = try document.generateTOC(options: options) + +for entry in toc.entries { + if let slug = entry.slug { + print("Link: [\(entry.text)](#\(slug))") + } +} + +// Without slugs (faster for analysis) +let noSlugs = TOCOptions(generateSlugs: false) +let plainTOC = try document.generateTOC(options: noSlugs) +// entry.slug will be nil +``` + +**Slug examples:** + +| Heading | Generated Slug | +|---------|----------------| +| Getting Started | `getting-started` | +| API v2.0 | `api-v20` | +| FAQ & Support | `faq-support` | +| 2024 Updates | `2024-updates` | + +### Position Tracking + +Include source position information: + +```swift +let options = TOCOptions(includePosition: true) +let toc = try document.generateTOC(options: options) + +for entry in toc.entries { + if let position = entry.position { + print("\(entry.text) at line \(position.line), col \(position.column)") + } +} +``` + +**Position information includes:** +- Line number in source document +- Column offset +- Byte offset (for efficient seeking) + +**Use cases:** +- Editor integration (jump to heading) +- Source mapping +- Diff tools +- Error reporting + +### Flat vs Hierarchical + +Control the structure of returned entries: + +```swift +// Hierarchical (default) - preserves document structure +let hierarchical = try document.generateTOC() + +// Flat - all entries at same level +let flat = try document.generateTOC( + options: TOCOptions(flat: true) +) +``` + +> Note: Even in flat mode, each entry's `level` property still reflects its original heading level. + +## Working with TOC Entries + +### Entry Properties + +Each ``TOCEntry`` provides: + +```swift +let entry = toc.entries[0] + +// Heading level (1-6) +let level = entry.level + +// Plain text (Markdown formatting removed) +let text = entry.text + +// URL-safe slug (if generated) +let slug = entry.slug + +// Source position (if tracked) +let position = entry.position +``` + +### Iterating Entries + +Process all entries: + +```swift +let toc = try document.generateTOC() + +for entry in toc.entries { + // Process each entry + print("\(entry.level): \(entry.text)") +} +``` + +### Filtering Entries + +Filter by level or content: + +```swift +let toc = try document.generateTOC() + +// Get only h2 headings +let h2Entries = toc.entries.filter { $0.level == 2 } + +// Get entries containing specific text +let apiEntries = toc.entries.filter { + $0.text.lowercased().contains("api") +} + +// Get top-level sections +let topLevel = toc.entries.filter { $0.level == 1 } +``` + +### Building Nested Structures + +Convert flat entries to nested structure: + +```swift +struct TOCNode { + let entry: TOCEntry + var children: [TOCNode] = [] +} + +func buildHierarchy(from entries: [TOCEntry]) -> [TOCNode] { + var roots: [TOCNode] = [] + var stack: [TOCNode] = [] + + for entry in entries { + let node = TOCNode(entry: entry) + + // Pop stack until we find the parent level + while let last = stack.last, last.entry.level >= entry.level { + stack.removeLast() + } + + if let parent = stack.last { + var parentCopy = parent + parentCopy.children.append(node) + stack[stack.count - 1] = parentCopy + } else { + roots.append(node) + } + + stack.append(node) + } + + return roots +} +``` + +## Output Formats + +### Markdown List + +Generate Markdown TOC: + +```swift +func formatAsMarkdown(_ toc: TableOfContents) -> String { + var lines = ["## Table of Contents", ""] + + for entry in toc.entries { + let indent = String(repeating: " ", count: entry.level - 1) + let link = entry.slug.map { "[\(entry.text)](#\($0))" } ?? entry.text + lines.append("\(indent)- \(link)") + } + + return lines.joined(separator: "\n") +} + +let markdown = formatAsMarkdown(toc) +print(markdown) +``` + +**Output:** +```markdown +## Table of Contents + +- [Introduction](#introduction) + - [Getting Started](#getting-started) + - [Installation](#installation) + - [Usage](#usage) +``` + +### Plain Text + +Simple indented outline: + +```swift +func formatAsPlainText(_ toc: TableOfContents) -> String { + toc.entries.map { entry in + let indent = String(repeating: " ", count: entry.level - 1) + return "\(indent)\(entry.text)" + }.joined(separator: "\n") +} +``` + +### HTML + +Generate HTML navigation: + +```swift +func formatAsHTML(_ toc: TableOfContents) -> String { + var html = "" + return html +} +``` + +### JSON + +Export as JSON for APIs: + +```swift +import Foundation + +struct TOCEntryJSON: Codable { + let level: Int + let text: String + let slug: String? +} + +func formatAsJSON(_ toc: TableOfContents) throws -> String { + let entries = toc.entries.map { entry in + TOCEntryJSON( + level: entry.level, + text: entry.text, + slug: entry.slug + ) + } + + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let data = try encoder.encode(entries) + return String(data: data, encoding: .utf8) ?? "" +} +``` + +## Advanced Use Cases + +### Validating Document Structure + +Check for structural issues: + +```swift +func validateTOCStructure(_ toc: TableOfContents) throws { + // Ensure document starts with h1 + guard let first = toc.entries.first, first.level == 1 else { + throw ValidationError.missingH1 + } + + // Check for level skipping + for i in 1.. prev.level + 1 { + throw ValidationError.skippedLevel( + from: prev.level, + to: curr.level, + heading: curr.text + ) + } + } + + // Ensure reasonable depth + let maxLevel = toc.entries.map(\.level).max() ?? 0 + guard maxLevel <= 4 else { + throw ValidationError.tooDeep(maxLevel) + } +} +``` + +### Generating Section Numbers + +Add numbering to headings: + +```swift +func addSectionNumbers(to toc: TableOfContents) -> [(number: String, entry: TOCEntry)] { + var counters = [0, 0, 0, 0, 0, 0] // For levels 1-6 + var numbered: [(String, TOCEntry)] = [] + + for entry in toc.entries { + let level = entry.level - 1 + + // Increment current level + counters[level] += 1 + + // Reset deeper levels + for i in (level + 1).. 0 } + let numberString = numbers.map(String.init).joined(separator: ".") + + numbered.append((numberString, entry)) + } + + return numbered +} + +// Usage +let numbered = addSectionNumbers(to: toc) +for (number, entry) in numbered { + print("\(number). \(entry.text)") +} +// Output: +// 1. Introduction +// 1.1. Getting Started +// 1.1.1. Installation +// 2. Usage +``` + +### Creating Navigation Trees + +Build a navigable tree structure: + +```swift +class TOCTreeNode { + let entry: TOCEntry + var children: [TOCTreeNode] = [] + weak var parent: TOCTreeNode? + + init(entry: TOCEntry) { + self.entry = entry + } + + func addChild(_ child: TOCTreeNode) { + children.append(child) + child.parent = self + } + + // Get all ancestors + var ancestors: [TOCTreeNode] { + var nodes: [TOCTreeNode] = [] + var current = parent + while let node = current { + nodes.insert(node, at: 0) + current = node.parent + } + return nodes + } + + // Get breadcrumb path + var breadcrumb: String { + let path = ancestors.map(\.entry.text) + [entry.text] + return path.joined(separator: " > ") + } +} + +func buildNavigationTree(from toc: TableOfContents) -> [TOCTreeNode] { + var roots: [TOCTreeNode] = [] + var stack: [TOCTreeNode] = [] + + for entry in toc.entries { + let node = TOCTreeNode(entry: entry) + + // Find parent + while let last = stack.last, last.entry.level >= entry.level { + stack.removeLast() + } + + if let parent = stack.last { + parent.addChild(node) + } else { + roots.append(node) + } + + stack.append(node) + } + + return roots +} +``` + +### Extracting Specific Sections + +Get content for a specific section: + +```swift +func extractSection( + matching heading: String, + from document: MarkdownDocument +) throws -> String? { + let options = TOCOptions(includePosition: true) + let toc = try document.generateTOC(options: options) + + // Find the heading + guard let index = toc.entries.firstIndex(where: { $0.text == heading }), + let startPos = toc.entries[index].position else { + return nil + } + + // Find the next heading at the same or higher level + let currentLevel = toc.entries[index].level + let nextIndex = toc.entries[(index + 1)...] + .firstIndex(where: { $0.level <= currentLevel }) + + let endPos = nextIndex.flatMap { toc.entries[$0].position } + + // Extract content between positions + let lines = document.content.components(separatedBy: .newlines) + + if let endPos = endPos { + return lines[startPos.line.. TableOfContents { + let key = document.content + + if let cached = tocCache[key] { + return cached + } + + let toc = try document.generateTOC() + tocCache[key] = toc + return toc + } +} +``` + +### Minimal Options for Speed + +Use minimal options when performance matters: + +```swift +// Fast: No slugs, no positions +let fast = try document.generateTOC( + options: TOCOptions( + generateSlugs: false, + includePosition: false + ) +) + +// Slower: Full tracking +let detailed = try document.generateTOC( + options: TOCOptions( + generateSlugs: true, + includePosition: true + ) +) +``` + +## See Also + +- ``TableOfContents`` +- ``TOCEntry`` +- ``TOCOptions`` +- ``MarkdownDocument/generateTOC(options:)`` +- diff --git a/Sources/MarkdownUtilities/Documentation.docc/Articles/TableOfContents/TOCOverview.md b/Sources/MarkdownUtilities/Documentation.docc/Articles/TableOfContents/TOCOverview.md new file mode 100644 index 0000000..ec26523 --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/Articles/TableOfContents/TOCOverview.md @@ -0,0 +1,349 @@ +# Table of Contents Overview + +Understanding table of contents generation from Markdown documents. + +## Overview + +A table of contents (TOC) is a hierarchical navigation structure extracted from a document's headings. MarkdownUtilities provides powerful TOC generation capabilities that analyze your Markdown documents and create structured, navigable outlines. + +## What is a Table of Contents? + +A TOC is an organized list of headings that provides: + +- **Document Navigation**: Quick access to different sections +- **Content Overview**: High-level view of document structure +- **Accessibility**: Screen reader navigation support +- **SEO Benefits**: Improved content structure for search engines + +### Example + +Given this Markdown: + +```markdown +# Introduction + +Some content here. + +## Getting Started + +More content. + +### Prerequisites + +Details about prerequisites. + +### Installation + +Installation instructions. + +## Usage + +How to use the tool. +``` + +A TOC might look like: + +``` +- Introduction + - Getting Started + - Prerequisites + - Installation + - Usage +``` + +## Use Cases + +### Document Navigation + +Add clickable navigation to long documents: + +```markdown +## Table of Contents + +- [Introduction](#introduction) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) +- [Usage](#usage) +``` + +### Site Maps + +Generate site structure for static site generators: + +```swift +let document = MarkdownDocument(content: pageContent) +let toc = try document.generateTOC() + +// Generate sitemap.xml from toc.entries +``` + +### Document Outlines + +Create document outlines for editors or previews: + +```swift +let toc = try document.generateTOC() + +for entry in toc.entries { + let indent = String(repeating: " ", count: entry.level - 1) + print("\(indent)\(entry.level). \(entry.text)") +} +``` + +### Content Analysis + +Analyze document structure: + +```swift +let toc = try document.generateTOC() + +// Check heading depth +let maxDepth = toc.entries.map(\.level).max() ?? 0 +print("Maximum heading depth: \(maxDepth)") + +// Count headings per level +let level2Count = toc.entries.filter { $0.level == 2 }.count +print("Number of level 2 headings: \(level2Count)") +``` + +## Hierarchical vs Flat Structure + +### Hierarchical (Default) + +Preserves the document's heading structure: + +``` +1. Introduction + 1.1. Overview + 1.2. Goals +2. Implementation + 2.1. Architecture + 2.1.1. Components + 2.2. API Design +``` + +Generated with standard options: + +```swift +let toc = try document.generateTOC() +// toc.entries preserves hierarchy through level property +``` + +### Flat Structure + +Returns all headings at the same level: + +```swift +let options = TOCOptions(flat: true) +let toc = try document.generateTOC(options: options) + +// All entries have the same hierarchical position +// but retain their original level in the level property +``` + +## The TableOfContents Type + +``TableOfContents`` contains: + +```swift +public struct TableOfContents { + /// All TOC entries in document order + public let entries: [TOCEntry] + + /// The source document content + public let sourceDocument: String +} +``` + +### TOCEntry + +Each ``TOCEntry`` represents one heading: + +```swift +public struct TOCEntry { + /// Heading level (1-6) + public let level: Int + + /// Heading text (without Markdown formatting) + public let text: String + + /// URL-safe slug for linking + public let slug: String? + + /// Position in source document (if requested) + public let position: SourcePosition? +} +``` + +## Basic Generation Example + +```swift +import MarkdownUtilities + +let markdown = """ +# Introduction + +Welcome to the guide. + +## Getting Started + +Let's begin. + +### Prerequisites + +You'll need these tools. + +## Advanced Topics + +For experienced users. +""" + +let document = MarkdownDocument(content: markdown) +let toc = try document.generateTOC() + +// Print the TOC +for entry in toc.entries { + let indent = String(repeating: " ", count: entry.level - 1) + let bullet = entry.level == 1 ? "•" : "-" + print("\(indent)\(bullet) \(entry.text)") +} + +// Output: +// • Introduction +// - Getting Started +// - Prerequisites +// - Advanced Topics +``` + +## Slug Generation + +Slugs are URL-safe identifiers for linking to headings: + +```swift +let toc = try document.generateTOC() + +for entry in toc.entries { + if let slug = entry.slug { + print("[\(entry.text)](#\(slug))") + } +} + +// Output: +// [Introduction](#introduction) +// [Getting Started](#getting-started) +// [Prerequisites](#prerequisites) +``` + +Slugs are generated automatically by: +1. Converting to lowercase +2. Replacing spaces with hyphens +3. Removing special characters +4. Ensuring uniqueness + +### Example Slug Transformations + +| Heading Text | Generated Slug | +|-------------|----------------| +| `Getting Started` | `getting-started` | +| `API Reference` | `api-reference` | +| `FAQ & Support` | `faq-support` | +| `v2.0 Changes` | `v20-changes` | + +## Position Tracking + +Track heading positions in the source document: + +```swift +let options = TOCOptions(includePosition: true) +let toc = try document.generateTOC(options: options) + +for entry in toc.entries { + if let pos = entry.position { + print("\(entry.text) at line \(pos.line), column \(pos.column)") + } +} +``` + +This is useful for: +- **Editor Integration**: Jump to heading in source +- **Error Reporting**: Reference specific locations +- **Diff Tools**: Track heading changes + +## Common Patterns + +### Generating Markdown TOC + +```swift +func generateMarkdownTOC(from document: MarkdownDocument) throws -> String { + let toc = try document.generateTOC() + + var lines: [String] = ["## Table of Contents", ""] + + for entry in toc.entries { + let indent = String(repeating: " ", count: entry.level - 1) + let link = entry.slug.map { "[\(entry.text)](#\($0))" } ?? entry.text + lines.append("\(indent)- \(link)") + } + + return lines.joined(separator: "\n") +} +``` + +### Filtering by Level + +```swift +let toc = try document.generateTOC() + +// Get only major sections (level 1 and 2) +let majorSections = toc.entries.filter { $0.level <= 2 } + +// Get subsections (level 3+) +let subsections = toc.entries.filter { $0.level >= 3 } +``` + +### Validating Structure + +```swift +func validateDocumentStructure(_ document: MarkdownDocument) throws { + let toc = try document.generateTOC() + + // Check for missing level 1 heading + guard toc.entries.contains(where: { $0.level == 1 }) else { + throw ValidationError.missingMainHeading + } + + // Check for too many nesting levels + let maxLevel = toc.entries.map(\.level).max() ?? 0 + guard maxLevel <= 4 else { + throw ValidationError.tooManyLevels(maxLevel) + } + + // Check for skipped levels (e.g., h1 → h3) + for (index, entry) in toc.entries.enumerated() { + if index > 0 { + let prevLevel = toc.entries[index - 1].level + let levelJump = entry.level - prevLevel + guard levelJump <= 1 else { + throw ValidationError.skippedLevel( + from: prevLevel, + to: entry.level + ) + } + } + } +} +``` + +## Next Steps + +Learn how to generate and customize TOC output: + +- - Detailed generation guide with all options + +## See Also + +- ``TableOfContents`` +- ``TOCEntry`` +- ``TOCOptions`` +- ``MarkdownDocument/generateTOC(options:)`` diff --git a/Sources/MarkdownUtilities/Documentation.docc/MarkdownUtilities.md b/Sources/MarkdownUtilities/Documentation.docc/MarkdownUtilities.md new file mode 100644 index 0000000..df81554 --- /dev/null +++ b/Sources/MarkdownUtilities/Documentation.docc/MarkdownUtilities.md @@ -0,0 +1,50 @@ +# ``MarkdownUtilities`` + +A Swift library for parsing and manipulating Markdown files with YAML frontmatter support. + +## Overview + +MarkdownUtilities provides a comprehensive set of tools for working with Markdown documents in Swift. Whether you're building a static site generator, a documentation tool, or a content management system, MarkdownUtilities offers the functionality you need to parse, manipulate, and generate Markdown content. + +Key capabilities include: + +- **Markdown Parsing**: Convert Markdown text to an abstract syntax tree (AST) using swift-markdown +- **Frontmatter Management**: Full CRUD operations for YAML frontmatter +- **Table of Contents Generation**: Automatically generate TOCs from document headings +- **Format Conversion**: Convert Markdown to plain text with customizable formatting +- **Round-trip Editing**: Parse, modify, and render Markdown while preserving structure + +MarkdownUtilities is built with Swift 6.2 and supports macOS 13+, iOS 16+, and other Apple platforms. + +## Topics + +### Essentials + +- +- +- + +### Working with Frontmatter + +- +- + +### Table of Contents + +- +- + +### Format Conversion + +- + +### Core Types + +- ``MarkdownDocument`` +- ``FrontMatter`` +- ``TableOfContents`` +- ``TOCEntry`` + +### Plain Text Conversion + +- ``PlainTextOptions`` diff --git a/Sources/md-utils/Documentation.docc/Articles/Commands/BodyCommand.md b/Sources/md-utils/Documentation.docc/Articles/Commands/BodyCommand.md new file mode 100644 index 0000000..624c34b --- /dev/null +++ b/Sources/md-utils/Documentation.docc/Articles/Commands/BodyCommand.md @@ -0,0 +1,446 @@ +# body Command + +Extract Markdown body content without frontmatter. + +## Overview + +The `body` command extracts the body content from Markdown documents, removing the YAML frontmatter. This is useful for previewing content, converting to other formats, or analyzing document text without metadata. + +## Syntax + +```bash +md-utils body [options] +``` + +## Options + +### --format + +Output format for the body content. + +**Values:** +- `markdown` (default): Preserve Markdown formatting +- `plain-text`: Convert to plain text + +```bash +# Get body as Markdown (default) +md-utils body post.md + +# Explicit Markdown format +md-utils body --format markdown post.md + +# Convert to plain text +md-utils body --format plain-text post.md +``` + +## Examples + +### Basic Usage + +Extract body from a single file: + +```bash +# Create a sample file +cat > post.md << 'EOF' +--- +title: My Post +author: Jane Doe +--- + +# My Post + +This is the **content** of my post. + +## Section 1 + +Some text here. +EOF + +# Extract body as Markdown +md-utils body post.md +``` + +**Output:** +```markdown +# My Post + +This is the **content** of my post. + +## Section 1 + +Some text here. +``` + +### Plain Text Conversion + +Convert body to plain text: + +```bash +md-utils body --format plain-text post.md +``` + +**Output:** +``` +My Post + +This is the content of my post. + +Section 1 + +Some text here. +``` + +### Multiple Files + +Process multiple files: + +```bash +# Multiple explicit files +md-utils body post1.md post2.md post3.md + +# Using wildcards +md-utils body posts/*.md + +# Each file's output is separated +``` + +### Recursive Processing + +Extract bodies from all files in a directory tree: + +```bash +# Process all Markdown files recursively +md-utils --recursive body content/ + +# Output to directory (preserves structure) +md-utils --recursive body content/ --output bodies/ +``` + +## Common Use Cases + +### Preview Generation + +Generate previews without frontmatter: + +```bash +# Get first 5 lines of body for preview +md-utils body post.md | head -n 5 + +# Create preview file +md-utils body post.md --output preview.md +``` + +### Content Migration + +Extract content when migrating between systems: + +```bash +# Extract all post bodies +md-utils --recursive body content/posts/ --output migrated-content/ + +# Convert to plain text for import +md-utils -r body --format plain-text old-posts/ -o new-system/ +``` + +### Word Count Analysis + +Count words without counting frontmatter: + +```bash +# Count words in body only +md-utils body --format plain-text post.md | wc -w + +# Count for all posts +md-utils -r body --format plain-text posts/ | wc -w + +# Per-file word counts +for file in posts/*.md; do + count=$(md-utils body --format plain-text "$file" | wc -w) + echo "$file: $count words" +done +``` + +### Content Extraction for Search + +Extract plain text for search indexing: + +```bash +# Extract all post bodies as plain text +md-utils --recursive body --format plain-text content/ --output search-index/ + +# Pipe to search indexer +md-utils body --format plain-text post.md | search-indexer --add +``` + +### Email Generation + +Prepare content for email: + +```bash +# Get plain text version for email +CONTENT=$(md-utils body --format plain-text post.md) + +# Send via mail command +echo "$CONTENT" | mail -s "New Post" subscribers@example.com +``` + +### Combining with Other Tools + +```bash +# Extract body and pipe to other Markdown tools +md-utils body post.md | pandoc -f markdown -t html + +# Get body and search for pattern +md-utils body post.md | grep "important" + +# Count lines in body +md-utils body post.md | wc -l + +# Compare bodies of two files +diff <(md-utils body post1.md) <(md-utils body post2.md) +``` + +## Pipeline Usage + +The `body` command is designed for pipeline use: + +### Extract and Transform + +```bash +# Extract → transform → save +md-utils body post.md | sed 's/old/new/g' > transformed.md + +# Extract → convert → save +md-utils body post.md | pandoc -f markdown -t rst > post.rst +``` + +### Filter and Process + +```bash +# Extract bodies of posts tagged "swift" +for file in posts/*.md; do + if md-utils fm get --key tags "$file" | grep -q "swift"; then + md-utils body "$file" + fi +done +``` + +### Aggregate Content + +```bash +# Combine all post bodies +md-utils --recursive body posts/ > combined.md + +# Create a master document +echo "# All Posts" > master.md +md-utils -r body posts/ >> master.md +``` + +## Output Control + +### Standard Output + +By default, output goes to stdout: + +```bash +# Print to console +md-utils body post.md + +# Redirect to file +md-utils body post.md > body.md + +# Pipe to other command +md-utils body post.md | less +``` + +### File Output + +Save to a specific file: + +```bash +# Single file +md-utils body post.md --output body.md + +# Multiple files to directory +md-utils body posts/*.md --output bodies/ + +# Recursive to directory +md-utils -r body content/ --output extracted/ +``` + +## Format Comparison + +### Markdown Format + +Preserves all Markdown formatting: + +```bash +md-utils body --format markdown post.md +``` + +**Preserves:** +- Headings (`#`, `##`, etc.) +- **Bold** and *italic* +- Lists (ordered and unordered) +- Code blocks and inline code +- Links and images +- Blockquotes +- All other Markdown syntax + +### Plain Text Format + +Strips all formatting: + +```bash +md-utils body --format plain-text post.md +``` + +**Removes:** +- Heading markers (`#`) +- **Bold** and *italic* markers +- List markers (`-`, `*`, `1.`) +- Code block delimiters +- Link brackets and URLs +- Image syntax +- Blockquote markers + +**Preserves:** +- Text content +- Paragraph structure +- Basic spacing + +## Batch Processing Scripts + +### Process All Posts + +```bash +#!/bin/bash +# extract-all-bodies.sh + +INPUT_DIR="content/posts" +OUTPUT_DIR="extracted-bodies" + +mkdir -p "$OUTPUT_DIR" + +md-utils --recursive body "$INPUT_DIR" --output "$OUTPUT_DIR" + +echo "Extracted bodies from $INPUT_DIR to $OUTPUT_DIR" +``` + +### Convert to Plain Text + +```bash +#!/bin/bash +# convert-to-text.sh + +for file in posts/*.md; do + basename=$(basename "$file" .md) + md-utils body --format plain-text "$file" > "text/${basename}.txt" +done + +echo "Converted $(ls posts/*.md | wc -l) files" +``` + +### Selective Extraction + +```bash +#!/bin/bash +# extract-published.sh + +# Extract bodies only from published posts +for file in posts/*.md; do + draft=$(md-utils fm get --key draft "$file") + + if [ "$draft" != "true" ]; then + md-utils body "$file" > "published/$(basename "$file")" + fi +done +``` + +## Error Handling + +### Missing Frontmatter + +If a file has no frontmatter, the entire file is treated as body: + +```bash +# File without frontmatter +echo "# Just Content" > no-fm.md + +# Returns entire file +md-utils body no-fm.md +# Output: # Just Content +``` + +### Invalid Files + +```bash +# Non-existent file +md-utils body missing.md +# Error: File not found: missing.md + +# Empty file +touch empty.md +md-utils body empty.md +# Output: (empty) +``` + +### Mixed Results + +When processing multiple files, errors are reported but don't stop processing: + +```bash +md-utils body post1.md missing.md post2.md +# Outputs: body of post1.md +# Error: File not found: missing.md +# Outputs: body of post2.md +``` + +## Integration Examples + +### With Pandoc + +```bash +# Convert body to HTML +md-utils body post.md | pandoc -f markdown -t html -o post.html + +# Convert to PDF +md-utils body post.md | pandoc -f markdown -o post.pdf + +# Convert multiple formats +for file in posts/*.md; do + basename=$(basename "$file" .md) + md-utils body "$file" | pandoc -f markdown -t html -o "html/${basename}.html" + md-utils body "$file" | pandoc -f markdown -t docx -o "docx/${basename}.docx" +done +``` + +### With Static Site Generators + +```bash +# Extract body for custom processor +md-utils body post.md | custom-processor > processed.html + +# Combine with frontmatter for custom format +FRONTMATTER=$(md-utils fm dump --format json post.md) +BODY=$(md-utils body post.md) +echo "{\"metadata\": $FRONTMATTER, \"content\": \"$BODY\"}" | jq +``` + +### With Search Engines + +```bash +# Index content in search engine +md-utils --recursive body --format plain-text posts/ | \\ + while IFS= read -r file; do + # Index each file + curl -X POST -d "$file" https://search-api.example.com/index + done +``` + +## See Also + +- +- +- +- diff --git a/Sources/md-utils/Documentation.docc/Articles/Commands/FMDump.md b/Sources/md-utils/Documentation.docc/Articles/Commands/FMDump.md new file mode 100644 index 0000000..3ab6649 --- /dev/null +++ b/Sources/md-utils/Documentation.docc/Articles/Commands/FMDump.md @@ -0,0 +1,475 @@ +# fm dump Command + +Export frontmatter in various formats. + +## Overview + +The `fm dump` command exports the entire frontmatter block in multiple output formats. This is useful for migration, backup, processing with other tools, and converting between formats. + +## Syntax + +```bash +md-utils fm dump [options] +``` + +## Options + +### --format + +Output format for the frontmatter: + +**Values:** +- `json-pretty` (default): Formatted JSON with indentation +- `json`: Compact JSON (single line) +- `yaml`: YAML format +- `raw`: Raw YAML as it appears in file +- `plist`: Property list XML format + +```bash +# Pretty JSON (default) +md-utils fm dump post.md + +# Compact JSON +md-utils fm dump --format json post.md + +# YAML +md-utils fm dump --format yaml post.md + +# Raw YAML (preserves original formatting) +md-utils fm dump --format raw post.md + +# Property list +md-utils fm dump --format plist post.md +``` + +### --include-delimiters + +Include `---` delimiters in raw format: + +```bash +# Without delimiters (default) +md-utils fm dump --format raw post.md + +# With delimiters +md-utils fm dump --format raw --include-delimiters post.md +``` + +## Examples + +### JSON Pretty (Default) + +```bash +cat > post.md << 'EOF' +--- +title: My Post +author: Jane Doe +tags: [swift, programming] +published: true +count: 42 +--- +EOF + +md-utils fm dump post.md +``` + +**Output:** +```json +{ + "title": "My Post", + "author": "Jane Doe", + "tags": ["swift", "programming"], + "published": true, + "count": 42 +} +``` + +### Compact JSON + +```bash +md-utils fm dump --format json post.md +``` + +**Output:** +```json +{"title":"My Post","author":"Jane Doe","tags":["swift","programming"],"published":true,"count":42} +``` + +### YAML Format + +```bash +md-utils fm dump --format yaml post.md +``` + +**Output:** +```yaml +title: My Post +author: Jane Doe +tags: + - swift + - programming +published: true +count: 42 +``` + +### Raw Format + +```bash +md-utils fm dump --format raw post.md +``` + +**Output:** +```yaml +title: My Post +author: Jane Doe +tags: [swift, programming] +published: true +count: 42 +``` + +### Raw with Delimiters + +```bash +md-utils fm dump --format raw --include-delimiters post.md +``` + +**Output:** +```yaml +--- +title: My Post +author: Jane Doe +tags: [swift, programming] +published: true +count: 42 +--- +``` + +## Common Use Cases + +### Data Export + +Export all frontmatter to JSON: + +```bash +# Single file +md-utils fm dump --format json post.md > post-metadata.json + +# All posts +for file in posts/*.md; do + basename=$(basename "$file" .md) + md-utils fm dump --format json "$file" > "metadata/${basename}.json" +done + +# Combined export +echo "[" > all-metadata.json +first=true +for file in posts/*.md; do + if [ "$first" = false ]; then + echo "," >> all-metadata.json + fi + md-utils fm dump --format json "$file" >> all-metadata.json + first=false +done +echo "]" >> all-metadata.json +``` + +### Format Conversion + +Convert between formats: + +```bash +#!/bin/bash +# Convert YAML frontmatter to TOML + +file="$1" + +# Get as JSON +json=$(md-utils fm dump --format json "$file") + +# Convert JSON to TOML (using external tool) +toml=$(echo "$json" | json2toml) + +# Create new file +{ + echo "+++" + echo "$toml" + echo "+++" + md-utils body "$file" +} > "${file%.md}.toml.md" +``` + +### Migration + +Migrate frontmatter to database: + +```bash +#!/bin/bash +# Import to database + +for file in posts/*.md; do + json=$(md-utils fm dump --format json "$file") + + # Insert to database + echo "INSERT INTO posts (filename, metadata) VALUES " \\ + "('$file', '$json');" | sqlite3 posts.db +done +``` + +### Backup + +Backup all frontmatter: + +```bash +#!/bin/bash +# Backup frontmatter separately + +backup_dir="frontmatter-backup-$(date +%Y%m%d)" +mkdir -p "$backup_dir" + +md-utils -r fm dump --format yaml content/ | \\ + while IFS=: read -r file _; do + mkdir -p "$backup_dir/$(dirname "$file")" + md-utils fm dump --format yaml "$file" > "$backup_dir/${file%.md}.yaml" + done +``` + +### API Integration + +Send metadata to API: + +```bash +#!/bin/bash +# Sync metadata to API + +for file in posts/*.md; do + json=$(md-utils fm dump --format json "$file") + + # POST to API + curl -X POST \\ + -H "Content-Type: application/json" \\ + -d "$json" \\ + https://api.example.com/metadata +done +``` + +## Pipeline Usage + +### Process with jq + +```bash +# Extract specific fields +md-utils fm dump --format json post.md | jq '{title, author}' + +# Filter +md-utils fm dump --format json post.md | jq 'select(.published == true)' + +# Transform +md-utils fm dump --format json post.md | \\ + jq '{title, slug: (.title | gsub(" "; "-") | ascii_downcase)}' + +# Combine multiple files +for file in posts/*.md; do + md-utils fm dump --format json "$file" +done | jq -s '.' +``` + +### Analyze Metadata + +```bash +#!/bin/bash +# Analyze frontmatter across all posts + +echo "Metadata Analysis" +echo "=================" + +# Collect all metadata +all_metadata=$(for file in posts/*.md; do + md-utils fm dump --format json "$file" +done | jq -s '.') + +# Count posts by author +echo "" +echo "Posts by author:" +echo "$all_metadata" | jq -r '.[].author' | sort | uniq -c | sort -rn + +# Most common tags +echo "" +echo "Most common tags:" +echo "$all_metadata" | jq -r '.[].tags[]' | sort | uniq -c | sort -rn | head -10 + +# Average word count (if available) +echo "" +echo "Statistics:" +echo "$all_metadata" | jq 'map(.wordCount) | { + avg: (add / length), + min: min, + max: max +}' +``` + +### Validation + +```bash +#!/bin/bash +# Validate frontmatter against schema + +schema='{"type":"object","required":["title","date","author"]}' + +for file in posts/*.md; do + json=$(md-utils fm dump --format json "$file") + + # Validate with ajv-cli or similar + if ! echo "$json" | ajv validate -s <(echo "$schema"); then + echo "$file: Invalid frontmatter" + fi +done +``` + +## Format Comparison + +### JSON vs YAML + +**JSON** (structured, machine-readable): +- Easy to parse programmatically +- Works with jq and other JSON tools +- Compact representation +- Requires escaping for special characters + +**YAML** (human-readable): +- More readable for humans +- Native frontmatter format +- Supports comments +- Can be more compact for simple data + +### When to Use Each Format + +**json-pretty**: +- Human review and editing +- Debugging frontmatter issues +- Documentation + +**json**: +- API integration +- Database storage +- Processing with jq +- Minimal file size + +**yaml**: +- Migrating between Markdown systems +- Editing frontmatter externally +- Creating templates + +**raw**: +- Exact copy of original +- Preserving formatting +- Backup purposes + +**plist**: +- macOS/iOS integration +- Property list editors +- Apple ecosystem tools + +## Advanced Examples + +### Merge Metadata + +```bash +#!/bin/bash +# Merge external metadata into frontmatter + +file="post.md" +external_data="metadata.json" + +# Get current frontmatter +current=$(md-utils fm dump --format json "$file") + +# Merge with external data +merged=$(jq -s '.[0] * .[1]' <(echo "$current") "$external_data") + +# Update frontmatter (requires setting each field) +echo "$merged" | jq -r 'to_entries | .[] | "\\(.key)=\\(.value)"' | \\ + while IFS='=' read -r key value; do + md-utils fm set --key "$key" --value "$value" "$file" -i + done +``` + +### Diff Frontmatter + +```bash +#!/bin/bash +# Compare frontmatter between two files + +file1="post-v1.md" +file2="post-v2.md" + +diff -u \\ + <(md-utils fm dump --format yaml "$file1") \\ + <(md-utils fm dump --format yaml "$file2") +``` + +### Generate TypeScript Types + +```bash +#!/bin/bash +# Generate TypeScript interface from frontmatter + +echo "interface PostFrontmatter {" + +md-utils -r fm dump --format json posts/ | \\ + jq -s ' + map(to_entries | map({key: .key, type: (.value | type)})) | + flatten | + group_by(.key) | + map({key: .[0].key, type: (.[].type | unique | join(" | "))}) | + .[] | + " \(.key): \(.type);" + ' -r + +echo "}" +``` + +### Create Search Index + +```bash +#!/bin/bash +# Build search index from metadata + +echo "Building search index..." + +index_file="search-index.json" + +echo "[" > "$index_file" +first=true + +for file in posts/*.md; do + if [ "$first" = false ]; then + echo "," >> "$index_file" + fi + + # Get metadata + metadata=$(md-utils fm dump --format json "$file") + + # Get plain text content + content=$(md-utils body --format plain-text "$file" | head -c 500) + + # Combine + echo "{" >> "$index_file" + echo ' "file": "'"$file"'",' >> "$index_file" + echo ' "metadata":' "$metadata," >> "$index_file" + echo ' "preview": "'"$content"'"' >> "$index_file" + echo "}" >> "$index_file" + + first=false +done + +echo "]" >> "$index_file" + +echo "Search index created: $index_file" +``` + +## See Also + +- +- +- +- +- diff --git a/Sources/md-utils/Documentation.docc/Articles/Commands/FMGet.md b/Sources/md-utils/Documentation.docc/Articles/Commands/FMGet.md new file mode 100644 index 0000000..ff76574 --- /dev/null +++ b/Sources/md-utils/Documentation.docc/Articles/Commands/FMGet.md @@ -0,0 +1,459 @@ +# fm get Command + +Read frontmatter values from Markdown files. + +## Overview + +The `fm get` command retrieves values from YAML frontmatter. It supports accessing simple values, arrays, nested objects, and can output in multiple formats. + +## Syntax + +```bash +md-utils fm get --key [options] +``` + +## Required Options + +### --key + +The frontmatter key to retrieve: + +```bash +# Simple key +md-utils fm get --key title post.md + +# Nested key (dot notation) +md-utils fm get --key author.name post.md + +# Short form +md-utils fm get -k title post.md +``` + +## Optional Parameters + +### --format + +Output format for the value: + +- `auto` (default): Detect type and format appropriately +- `json`: JSON format +- `yaml`: YAML format +- `raw`: Raw string value + +```bash +# Auto-detect (default) +md-utils fm get --key tags post.md + +# JSON format +md-utils fm get --key tags --format json post.md + +# YAML format +md-utils fm get --key tags --format yaml post.md + +# Raw string +md-utils fm get --key title --format raw post.md +``` + +## Examples + +### Simple Values + +```bash +# Create sample file +cat > post.md << 'EOF' +--- +title: My Blog Post +author: Jane Doe +date: 2024-01-24 +published: true +count: 42 +--- + +# Content here +EOF + +# Get string +md-utils fm get --key title post.md +# Output: My Blog Post + +# Get boolean +md-utils fm get --key published post.md +# Output: true + +# Get number +md-utils fm get --key count post.md +# Output: 42 + +# Get date +md-utils fm get --key date post.md +# Output: 2024-01-24 +``` + +### Array Values + +```bash +cat > post.md << 'EOF' +--- +tags: [swift, programming, tutorial] +categories: + - Development + - Swift +--- +EOF + +# Get inline array +md-utils fm get --key tags post.md +# Output: ["swift", "programming", "tutorial"] + +# Get block array +md-utils fm get --key categories post.md +# Output: ["Development", "Swift"] + +# JSON format +md-utils fm get --key tags --format json post.md +# Output: ["swift","programming","tutorial"] +``` + +### Nested Values + +```bash +cat > post.md << 'EOF' +--- +author: + name: Jane Doe + email: jane@example.com + social: + twitter: "@janedoe" + github: "janedoe" +--- +EOF + +# Get nested value (dot notation) +md-utils fm get --key author.name post.md +# Output: Jane Doe + +# Get deeply nested value +md-utils fm get --key author.social.twitter post.md +# Output: @janedoe + +# Get entire object +md-utils fm get --key author --format json post.md +# Output: {"name":"Jane Doe","email":"jane@example.com",...} +``` + +## Multiple Files + +### Process Multiple Files + +```bash +# Multiple explicit files +md-utils fm get --key title post1.md post2.md post3.md + +# Using wildcards +md-utils fm get --key author posts/*.md + +# Output shows each file: +# post1.md: First Post +# post2.md: Second Post +# post3.md: Third Post +``` + +### Recursive Processing + +```bash +# Get all titles in directory tree +md-utils --recursive fm get --key title content/ + +# Filter results +md-utils -r fm get --key author posts/ | grep "Jane" + +# Count unique authors +md-utils -r fm get --key author posts/ | sort | uniq | wc -l +``` + +## Common Use Cases + +### Find Files by Value + +```bash +# Find posts by specific author +md-utils fm get --key author posts/*.md | grep -l "Jane Doe" + +# Find published posts +for file in posts/*.md; do + if [ "$(md-utils fm get --key published "$file")" = "true" ]; then + echo "$file" + fi +done + +# Find posts with specific tag +md-utils fm get --key tags posts/*.md | grep -l "swift" +``` + +### List All Values + +```bash +# List all titles +md-utils -r fm get --key title posts/ + +# List all tags (flatten arrays) +md-utils -r fm get --key tags posts/ --format json | \\ + jq -r '.[]' | sort | uniq + +# List all authors +md-utils -r fm get --key author posts/ | \\ + sed 's/.*: //' | sort | uniq +``` + +### Validation + +```bash +#!/bin/bash +# Check required fields + +required_fields=("title" "date" "author") + +for file in posts/*.md; do + missing=() + + for field in "${required_fields[@]}"; do + if ! md-utils fm get --key "$field" "$file" >/dev/null 2>&1; then + missing+=("$field") + fi + done + + if [ ${#missing[@]} -gt 0 ]; then + echo "$file missing: ${missing[*]}" + fi +done +``` + +### Content Analysis + +```bash +# Count posts per author +md-utils -r fm get --key author posts/ | \\ + sed 's/.*: //' | sort | uniq -c + +# Tag frequency +md-utils -r fm get --key tags posts/ --format json | \\ + jq -r '.[]' | sort | uniq -c | sort -rn + +# Posts by year +md-utils -r fm get --key date posts/ | \\ + sed 's/.*: //' | cut -d- -f1 | sort | uniq -c +``` + +## Pipeline Usage + +### Filter and Process + +```bash +# Get titles of published posts +for file in posts/*.md; do + if [ "$(md-utils fm get --key draft "$file")" != "true" ]; then + md-utils fm get --key title "$file" + fi +done + +# Process posts by category +md-utils fm get --key category posts/*.md | \\ + while IFS=: read -r file category; do + echo "Processing $category: $file" + done +``` + +### JSON Processing + +```bash +# Extract all metadata as JSON +for file in posts/*.md; do + echo "{" + echo " \"file\": \"$file\"," + echo " \"title\": \"$(md-utils fm get --key title "$file")\"," + echo " \"author\": \"$(md-utils fm get --key author "$file")\"," + echo " \"tags\": $(md-utils fm get --key tags "$file" --format json)" + echo "}" +done | jq -s '.' +``` + +### Reporting + +```bash +#!/bin/bash +# Generate content report + +echo "Content Report" +echo "==============" +echo "" + +echo "Total posts: $(ls posts/*.md | wc -l)" +echo "" + +echo "Posts by author:" +md-utils -r fm get --key author posts/ | \\ + sed 's/.*: //' | sort | uniq -c | sort -rn +echo "" + +echo "Most common tags:" +md-utils -r fm get --key tags posts/ --format json | \\ + jq -r '.[]' | sort | uniq -c | sort -rn | head -10 +``` + +## Output Formats + +### Auto Format (Default) + +Automatically detects type: + +```bash +md-utils fm get --key title post.md +# Output: My Title + +md-utils fm get --key tags post.md +# Output: ["tag1", "tag2"] + +md-utils fm get --key published post.md +# Output: true +``` + +### JSON Format + +Always outputs valid JSON: + +```bash +# String +md-utils fm get --key title --format json post.md +# Output: "My Title" + +# Array +md-utils fm get --key tags --format json post.md +# Output: ["tag1","tag2"] + +# Object +md-utils fm get --key author --format json post.md +# Output: {"name":"Jane","email":"jane@example.com"} +``` + +### YAML Format + +Outputs YAML: + +```bash +md-utils fm get --key author --format yaml post.md +# Output: +# name: Jane Doe +# email: jane@example.com +``` + +### Raw Format + +Plain string (no quotes): + +```bash +md-utils fm get --key title --format raw post.md +# Output: My Title +``` + +## Error Handling + +### Missing Keys + +```bash +# Key doesn't exist +md-utils fm get --key missing post.md +# Error: Key not found: missing + +# Check if key exists +if md-utils fm get --key author post.md >/dev/null 2>&1; then + echo "Author exists" +else + echo "No author" +fi +``` + +### Invalid Files + +```bash +# File not found +md-utils fm get --key title missing.md +# Error: File not found: missing.md + +# Invalid frontmatter +md-utils fm get --key title invalid.md +# Error: Failed to parse frontmatter +``` + +### Mixed Results + +When processing multiple files, errors don't stop processing: + +```bash +md-utils fm get --key title post1.md missing.md post2.md +# Output: post1.md: Title One +# Error: File not found: missing.md +# Output: post2.md: Title Two +``` + +## Advanced Examples + +### Conditional Processing + +```bash +#!/bin/bash +# Process based on frontmatter value + +for file in posts/*.md; do + category=$(md-utils fm get --key category "$file" 2>/dev/null) + + case "$category" in + "tutorial") + echo "Processing tutorial: $file" + # Custom processing + ;; + "news") + echo "Processing news: $file" + # Different processing + ;; + esac +done +``` + +### Data Export + +```bash +#!/bin/bash +# Export to CSV + +echo "file,title,author,date,tags" + +md-utils -r fm get --key title posts/ | while IFS=: read -r file title; do + author=$(md-utils fm get --key author "$file" 2>/dev/null || echo "Unknown") + date=$(md-utils fm get --key date "$file" 2>/dev/null || echo "") + tags=$(md-utils fm get --key tags --format json "$file" 2>/dev/null || echo "[]") + + echo "$file,$title,$author,$date,$tags" +done +``` + +### Migration + +```bash +#!/bin/bash +# Find posts using old field names + +echo "Posts needing migration:" + +for file in posts/*.md; do + if md-utils fm get --key old_field "$file" >/dev/null 2>&1; then + echo " $file" + fi +done +``` + +## See Also + +- +- +- +- diff --git a/Sources/md-utils/Documentation.docc/Articles/Commands/FMList.md b/Sources/md-utils/Documentation.docc/Articles/Commands/FMList.md new file mode 100644 index 0000000..6a9ca2f --- /dev/null +++ b/Sources/md-utils/Documentation.docc/Articles/Commands/FMList.md @@ -0,0 +1,414 @@ +# fm list Command + +List all frontmatter keys in Markdown files. + +## Overview + +The `fm list` command displays all keys present in a document's frontmatter. This is useful for discovering schema, validation, and exploring document structure. + +## Syntax + +```bash +md-utils fm list [options] +``` + +## Examples + +### Basic Usage + +List keys from a single file: + +```bash +# Create sample file +cat > post.md << 'EOF' +--- +title: My Post +author: Jane Doe +date: 2024-01-24 +tags: [swift, programming] +published: true +--- + +# Content +EOF + +# List all keys +md-utils fm list post.md +``` + +**Output:** +``` +title +author +date +tags +published +``` + +### Multiple Files + +```bash +# List keys from multiple files +md-utils fm list post1.md post2.md post3.md + +# Each file's output is shown separately +``` + +**Output:** +``` +post1.md: +title +author +tags + +post2.md: +title +date +published +``` + +### Recursive Processing + +```bash +# List keys from all files +md-utils --recursive fm list content/ + +# Unique keys across all files +md-utils -r fm list content/ | sort | uniq +``` + +## Common Use Cases + +### Schema Discovery + +Discover what fields are used: + +```bash +# Find all unique keys across posts +md-utils -r fm list posts/ | sort | uniq + +# Count occurrences of each key +md-utils -r fm list posts/ | sort | uniq -c | sort -rn +``` + +**Example output:** +``` + 150 title + 150 date + 148 author + 120 tags + 45 draft + 23 categories +``` + +### Validation + +Check for required fields: + +```bash +#!/bin/bash +# Validate required fields + +required=("title" "date" "author") + +for file in posts/*.md; do + keys=$(md-utils fm list "$file") + missing=() + + for field in "${required[@]}"; do + if ! echo "$keys" | grep -q "^${field}$"; then + missing+=("$field") + fi + done + + if [ ${#missing[@]} -gt 0 ]; then + echo "$file missing: ${missing[*]}" + fi +done +``` + +### Finding Inconsistencies + +Identify files with unusual fields: + +```bash +#!/bin/bash +# Find files with non-standard fields + +# Get common fields +common=$(md-utils -r fm list posts/ | sort | uniq -c | \\ + awk '$1 > 100 {print $2}') + +# Check each file +for file in posts/*.md; do + keys=$(md-utils fm list "$file") + + for key in $keys; do + if ! echo "$common" | grep -q "^${key}$"; then + echo "$file has unusual field: $key" + fi + done +done +``` + +### Schema Documentation + +Generate schema documentation: + +```bash +#!/bin/bash +# Document frontmatter schema + +echo "# Frontmatter Schema" +echo "" +echo "## Fields" +echo "" + +md-utils -r fm list posts/ | sort | uniq | while read -r key; do + count=$(md-utils -r fm list posts/ | grep -c "^${key}$") + total=$(ls posts/*.md | wc -l) + percentage=$((count * 100 / total)) + + echo "### $key" + echo "" + echo "- Used in: $count / $total files ($percentage%)" + + # Get sample value + for file in posts/*.md; do + if md-utils fm list "$file" | grep -q "^${key}$"; then + sample=$(md-utils fm get --key "$key" "$file" 2>/dev/null) + echo "- Example: \`$sample\`" + break + fi + done + + echo "" +done +``` + +## Pipeline Usage + +### Filter by Field Presence + +```bash +# Find files with specific field +for file in posts/*.md; do + if md-utils fm list "$file" | grep -q "^draft$"; then + echo "$file has draft field" + fi +done + +# Find files missing a field +for file in posts/*.md; do + if ! md-utils fm list "$file" | grep -q "^author$"; then + echo "$file missing author" + fi +done +``` + +### Compare Schemas + +```bash +#!/bin/bash +# Compare two files' schemas + +file1="post1.md" +file2="post2.md" + +keys1=$(md-utils fm list "$file1" | sort) +keys2=$(md-utils fm list "$file2" | sort) + +echo "Only in $file1:" +comm -23 <(echo "$keys1") <(echo "$keys2") + +echo "" +echo "Only in $file2:" +comm -13 <(echo "$keys1") <(echo "$keys2") + +echo "" +echo "In both:" +comm -12 <(echo "$keys1") <(echo "$keys2") +``` + +### Field Coverage Report + +```bash +#!/bin/bash +# Generate field coverage report + +echo "Field Coverage Report" +echo "====================" +echo "" + +total_files=$(ls posts/*.md | wc -l) + +md-utils -r fm list posts/ | sort | uniq | while read -r key; do + count=$(for file in posts/*.md; do + md-utils fm list "$file" | grep -q "^${key}$" && echo 1 + done | wc -l) + + percentage=$((count * 100 / total_files)) + + printf "%-20s %3d / %3d (%3d%%)\n" "$key:" "$count" "$total_files" "$percentage" +done | sort -t'(' -k2 -rn +``` + +## Advanced Examples + +### Required vs Optional Fields + +```bash +#!/bin/bash +# Categorize fields by usage + +total=$(ls posts/*.md | wc -l) +threshold=90 # 90% = required + +echo "Required fields (>$threshold% coverage):" +md-utils -r fm list posts/ | sort | uniq -c | while read -r count key; do + percentage=$((count * 100 / total)) + if [ $percentage -gt $threshold ]; then + echo " $key ($percentage%)" + fi +done + +echo "" +echo "Optional fields (<=$threshold% coverage):" +md-utils -r fm list posts/ | sort | uniq -c | while read -r count key; do + percentage=$((count * 100 / total)) + if [ $percentage -le $threshold ]; then + echo " $key ($percentage%)" + fi +done +``` + +### Migration Planning + +```bash +#!/bin/bash +# Find deprecated fields + +deprecated=("old_field" "legacy_field" "deprecated_field") + +echo "Files using deprecated fields:" +for file in posts/*.md; do + keys=$(md-utils fm list "$file") + found=() + + for field in "${deprecated[@]}"; do + if echo "$keys" | grep -q "^${field}$"; then + found+=("$field") + fi + done + + if [ ${#found[@]} -gt 0 ]; then + echo " $file: ${found[*]}" + fi +done +``` + +### Schema Diff + +```bash +#!/bin/bash +# Compare schemas between directories + +echo "Schema differences between directories:" +echo "" + +schema1=$(md-utils -r fm list dir1/ | sort | uniq) +schema2=$(md-utils -r fm list dir2/ | sort | uniq) + +echo "Only in dir1/:" +comm -23 <(echo "$schema1") <(echo "$schema2") + +echo "" +echo "Only in dir2/:" +comm -13 <(echo "$schema1") <(echo "$schema2") + +echo "" +echo "In both:" +comm -12 <(echo "$schema1") <(echo "$schema2") +``` + +## Integration Examples + +### With JSON Processing + +```bash +#!/bin/bash +# Generate JSON schema + +echo "{" +echo ' "fields": {' + +md-utils -r fm list posts/ | sort | uniq | while read -r key; do + # Get sample value to infer type + for file in posts/*.md; do + if value=$(md-utils fm get --key "$key" "$file" --format json 2>/dev/null); then + # Infer type from JSON + if echo "$value" | jq -e 'type == "string"' >/dev/null 2>&1; then + type="string" + elif echo "$value" | jq -e 'type == "number"' >/dev/null 2>&1; then + type="number" + elif echo "$value" | jq -e 'type == "boolean"' >/dev/null 2>&1; then + type="boolean" + elif echo "$value" | jq -e 'type == "array"' >/dev/null 2>&1; then + type="array" + else + type="unknown" + fi + + echo " \"$key\": {\"type\": \"$type\"}," + break + fi + done +done + +echo " }" +echo "}" +``` + +### Quality Checks + +```bash +#!/bin/bash +# Quality check for frontmatter completeness + +echo "Frontmatter Quality Report" +echo "==========================" +echo "" + +total=$(ls posts/*.md | wc -l) +required=("title" "date" "author") + +# Check required fields +echo "Required Fields Coverage:" +for field in "${required[@]}"; do + count=0 + for file in posts/*.md; do + if md-utils fm list "$file" | grep -q "^${field}$"; then + ((count++)) + fi + done + + percentage=$((count * 100 / total)) + echo " $field: $count/$total ($percentage%)" + + if [ $percentage -lt 100 ]; then + echo " Missing in:" + for file in posts/*.md; do + if ! md-utils fm list "$file" | grep -q "^${field}$"; then + echo " - $file" + fi + done + fi +done +``` + +## See Also + +- +- +- +- diff --git a/Sources/md-utils/Documentation.docc/Articles/Commands/FMSet.md b/Sources/md-utils/Documentation.docc/Articles/Commands/FMSet.md new file mode 100644 index 0000000..db38aa8 --- /dev/null +++ b/Sources/md-utils/Documentation.docc/Articles/Commands/FMSet.md @@ -0,0 +1,447 @@ +# fm set Command + +Set frontmatter values in Markdown files. + +## Overview + +The `fm set` command creates or updates frontmatter values. It supports setting simple values, arrays, nested objects, and can modify multiple files atomically. + +## Syntax + +```bash +md-utils fm set --key --value [options] +``` + +## Required Options + +### --key + +The frontmatter key to set: + +```bash +# Simple key +md-utils fm set --key title --value "New Title" post.md + +# Nested key (dot notation) +md-utils fm set --key author.name --value "Jane" post.md + +# Short form +md-utils fm set -k title -v "New Title" post.md +``` + +### --value + +The value to set: + +```bash +# String +md-utils fm set --key author --value "Jane Doe" post.md + +# Boolean (use lowercase) +md-utils fm set --key published --value "true" post.md +md-utils fm set --key draft --value "false" post.md + +# Number +md-utils fm set --key count --value "42" post.md + +# Date (ISO format) +md-utils fm set --key date --value "2024-01-24" post.md + +# Array (JSON format) +md-utils fm set --key tags --value '["swift","programming"]' post.md +``` + +## Output Control + +By default, outputs modified content to stdout. Use global `--in-place` option to modify files directly: + +```bash +# Output to stdout (preview) +md-utils fm set --key author --value "Jane" post.md + +# Modify file in place +md-utils fm set --key author --value "Jane" post.md --in-place + +# Short form +md-utils fm set -k author -v "Jane" post.md -i +``` + +## Examples + +### Setting Simple Values + +```bash +# Set string +md-utils fm set --key title --value "My Post" post.md --in-place + +# Set boolean +md-utils fm set --key published --value "true" post.md -i + +# Set number +md-utils fm set --key wordCount --value "1500" post.md -i + +# Set date +md-utils fm set --key updated --value "$(date -I)" post.md -i +``` + +### Setting Arrays + +```bash +# Set array (JSON format) +md-utils fm set --key tags --value '["swift", "programming"]' post.md -i + +# Set single-item array +md-utils fm set --key categories --value '["tutorial"]' post.md -i + +# Empty array +md-utils fm set --key tags --value '[]' post.md -i +``` + +### Setting Nested Values + +```bash +# Set nested value (creates structure if needed) +md-utils fm set --key author.name --value "Jane Doe" post.md -i +md-utils fm set --key author.email --value "jane@example.com" post.md -i + +# Results in: +# author: +# name: Jane Doe +# email: jane@example.com + +# Deeply nested +md-utils fm set --key author.social.twitter --value "@janedoe" post.md -i +``` + +## Multiple Files + +### Batch Updates + +```bash +# Update multiple files +md-utils fm set --key author --value "Jane Doe" \\ + post1.md post2.md post3.md --in-place + +# Using wildcards +md-utils fm set --key updated --value "$(date -I)" \\ + posts/*.md -i + +# Recursive processing +md-utils --recursive fm set --key modified --value "$(date -I)" \\ + content/ --in-place +``` + +## Common Use Cases + +### Publishing Workflow + +```bash +# Mark as published +md-utils fm set --key draft --value "false" post.md -i + +# Set publish date +md-utils fm set --key publishDate --value "$(date -I)" post.md -i + +# Publish all drafts in directory +md-utils -r fm set --key draft --value "false" drafts/ -i +``` + +### Adding Metadata + +```bash +# Add author to posts missing it +for file in posts/*.md; do + if ! md-utils fm get --key author "$file" >/dev/null 2>&1; then + md-utils fm set --key author --value "Default Author" "$file" -i + fi +done + +# Add timestamp to all files +md-utils -r fm set --key processedAt --value "$(date -I)" content/ -i +``` + +### Updating Tags + +```bash +# Add tag to all posts +for file in posts/*.md; do + tags=$(md-utils fm get --key tags "$file" --format json 2>/dev/null || echo '[]') + new_tags=$(echo "$tags" | jq '. + ["new-tag"] | unique') + md-utils fm set --key tags --value "$new_tags" "$file" -i +done + +# Replace tags +md-utils fm set --key tags --value '["swift", "tutorial"]' post.md -i +``` + +### Schema Migration + +```bash +#!/bin/bash +# Migrate from old field to new field + +for file in posts/*.md; do + # Get old value + old_value=$(md-utils fm get --key old_field "$file" 2>/dev/null) + + if [ -n "$old_value" ]; then + # Set new field + md-utils fm set --key new_field --value "$old_value" "$file" -i + + # Remove old field + md-utils fm remove --key old_field "$file" -i + fi +done +``` + +### Conditional Updates + +```bash +#!/bin/bash +# Update only if condition met + +for file in posts/*.md; do + draft=$(md-utils fm get --key draft "$file" 2>/dev/null) + + # Only update non-drafts + if [ "$draft" != "true" ]; then + md-utils fm set --key lastChecked --value "$(date -I)" "$file" -i + fi +done +``` + +## Pipeline Usage + +### Preview Before Apply + +```bash +# Preview changes +md-utils fm set --key author --value "Jane" posts/*.md + +# Review output, then apply +md-utils fm set --key author --value "Jane" posts/*.md -i +``` + +### Batch Processing with Logging + +```bash +#!/bin/bash +# Update with logging + +log_file="updates.log" + +for file in posts/*.md; do + echo "Updating $file" >> "$log_file" + + if md-utils fm set --key updated --value "$(date -I)" "$file" -i; then + echo " Success" >> "$log_file" + else + echo " Failed" >> "$log_file" + fi +done +``` + +### Complex Workflows + +```bash +#!/bin/bash +# Multi-step update workflow + +for file in posts/*.md; do + # Get current date + date=$(md-utils fm get --key date "$file" 2>/dev/null) + + # Set publish date if missing + if ! md-utils fm get --key publishDate "$file" >/dev/null 2>&1; then + md-utils fm set --key publishDate --value "$date" "$file" -i + fi + + # Update modified timestamp + md-utils fm set --key modified --value "$(date -I)" "$file" -i + + # Add version + md-utils fm set --key version --value "2" "$file" -i +done +``` + +## Value Types + +### Strings + +```bash +# Simple string +md-utils fm set --key title --value "My Title" post.md -i + +# String with spaces (use quotes) +md-utils fm set --key description --value "A long description here" post.md -i + +# String with special characters +md-utils fm set --key note --value "Quote: \"example\"" post.md -i +``` + +### Booleans + +Use lowercase string representation: + +```bash +# True +md-utils fm set --key published --value "true" post.md -i + +# False +md-utils fm set --key draft --value "false" post.md -i +``` + +### Numbers + +```bash +# Integer +md-utils fm set --key count --value "42" post.md -i + +# Float +md-utils fm set --key rating --value "4.5" post.md -i +``` + +### Arrays + +Use JSON array format: + +```bash +# String array +md-utils fm set --key tags --value '["swift", "programming"]' post.md -i + +# Number array +md-utils fm set --key scores --value '[1, 2, 3, 4, 5]' post.md -i + +# Mixed array (not recommended) +md-utils fm set --key mixed --value '["string", 42, true]' post.md -i +``` + +### Objects + +Use JSON object format: + +```bash +# Simple object +md-utils fm set --key author --value '{"name": "Jane", "email": "jane@example.com"}' post.md -i + +# Nested object +md-utils fm set --key metadata --value '{"version": 1, "status": "published"}' post.md -i +``` + +## Error Handling + +### Safe Updates + +```bash +# Backup before updating +cp post.md post.md.bak +md-utils fm set --key title --value "New Title" post.md -i + +# Or use version control +git add post.md +md-utils fm set --key title --value "New Title" post.md -i +git diff # Review +git commit -m "Update title" +``` + +### Validation + +```bash +#!/bin/bash +# Validate before setting + +key="title" +value="New Title" +file="post.md" + +# Check if file exists +if [ ! -f "$file" ]; then + echo "Error: File not found" + exit 1 +fi + +# Check if value is not empty +if [ -z "$value" ]; then + echo "Error: Value cannot be empty" + exit 1 +fi + +# Set value +md-utils fm set --key "$key" --value "$value" "$file" -i +``` + +### Error Recovery + +```bash +#!/bin/bash +# Update with error handling + +for file in posts/*.md; do + if ! md-utils fm set --key updated --value "$(date -I)" "$file" -i 2>/dev/null; then + echo "Failed to update $file" + # Log or handle error + fi +done +``` + +## Advanced Examples + +### Computed Values + +```bash +#!/bin/bash +# Set computed values + +file="post.md" + +# Count words and set +word_count=$(md-utils body --format plain-text "$file" | wc -w) +md-utils fm set --key wordCount --value "$word_count" "$file" -i + +# Calculate reading time (250 words/minute) +reading_time=$(( (word_count + 249) / 250 )) +md-utils fm set --key readingTime --value "$reading_time" "$file" -i +``` + +### Dynamic Metadata + +```bash +#!/bin/bash +# Add dynamic metadata + +for file in posts/*.md; do + # File info + size=$(stat -f%z "$file") + md-utils fm set --key fileSize --value "$size" "$file" -i + + # Checksum + checksum=$(md5 -q "$file") + md-utils fm set --key checksum --value "$checksum" "$file" -i + + # Last modified + mtime=$(stat -f%Sm -t"%Y-%m-%d" "$file") + md-utils fm set --key lastModified --value "$mtime" "$file" -i +done +``` + +### Bulk Updates from CSV + +```bash +#!/bin/bash +# Update from CSV file + +# CSV format: filename,title,author,tags +while IFS=, read -r filename title author tags; do + md-utils fm set --key title --value "$title" "$filename" -i + md-utils fm set --key author --value "$author" "$filename" -i + md-utils fm set --key tags --value "$tags" "$filename" -i +done < updates.csv +``` + +## See Also + +- +- +- +- +- diff --git a/Sources/md-utils/Documentation.docc/Articles/Commands/TOCCommand.md b/Sources/md-utils/Documentation.docc/Articles/Commands/TOCCommand.md new file mode 100644 index 0000000..ee43370 --- /dev/null +++ b/Sources/md-utils/Documentation.docc/Articles/Commands/TOCCommand.md @@ -0,0 +1,506 @@ +# toc Command + +Generate table of contents from Markdown headings. + +## Overview + +The `toc` command analyzes Markdown documents and generates a table of contents from their heading structure. It supports multiple output formats and provides options to control heading levels, formatting, and structure. + +## Syntax + +```bash +md-utils toc [options] +``` + +## Options + +### --format + +Output format for the table of contents. + +**Values:** +- `md-bullet-links` (default): Markdown bullet list with anchor links +- `plain`: Plain text hierarchical outline +- `json`: JSON array of heading objects +- `html`: HTML navigation list + +```bash +# Default: Markdown with links +md-utils toc post.md + +# Plain text outline +md-utils toc --format plain post.md + +# JSON for processing +md-utils toc --format json post.md + +# HTML navigation +md-utils toc --format html post.md +``` + +### --min-level + +Minimum heading level to include (1-6): + +```bash +# Skip h1, start with h2 +md-utils toc --min-level 2 post.md + +# Only h3 and deeper +md-utils toc --min-level 3 post.md +``` + +**Default**: 1 + +### --max-level + +Maximum heading level to include (1-6): + +```bash +# Only h1 and h2 +md-utils toc --max-level 2 post.md + +# Up to h4 +md-utils toc --max-level 4 post.md +``` + +**Default**: 6 + +### --flat + +Output flat list instead of hierarchical structure: + +```bash +# Hierarchical (default) +md-utils toc post.md + +# Flat list +md-utils toc --flat post.md +``` + +### --no-slugs + +Don't generate URL-safe slug identifiers: + +```bash +# With slugs (default, for links) +md-utils toc post.md + +# Without slugs (faster, no links) +md-utils toc --no-slugs post.md +``` + +## Examples + +### Basic TOC Generation + +Generate a TOC from a document: + +```bash +# Create sample document +cat > doc.md << 'EOF' +# Introduction + +Welcome to the guide. + +## Getting Started + +### Prerequisites + +### Installation + +## Usage + +### Basic Commands + +### Advanced Features + +## Troubleshooting +EOF + +# Generate default TOC +md-utils toc doc.md +``` + +**Output:** +```markdown +- [Introduction](#introduction) + - [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Usage](#usage) + - [Basic Commands](#basic-commands) + - [Advanced Features](#advanced-features) + - [Troubleshooting](#troubleshooting) +``` + +### Format Examples + +#### Markdown with Links (default) + +```bash +md-utils toc --format md-bullet-links doc.md +``` +```markdown +- [Introduction](#introduction) + - [Getting Started](#getting-started) + - [Installation](#installation) +``` + +#### Plain Text + +```bash +md-utils toc --format plain doc.md +``` +``` +Introduction + Getting Started + Installation +``` + +#### JSON + +```bash +md-utils toc --format json doc.md +``` +```json +[ + { + "level": 1, + "text": "Introduction", + "slug": "introduction" + }, + { + "level": 2, + "text": "Getting Started", + "slug": "getting-started" + }, + { + "level": 3, + "text": "Installation", + "slug": "installation" + } +] +``` + +#### HTML + +```bash +md-utils toc --format html doc.md +``` +```html + +``` + +### Level Filtering + +#### Skip h1 (Common Pattern) + +```bash +# Skip main title, show sections +md-utils toc --min-level 2 doc.md +``` +```markdown +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) +- [Usage](#usage) +``` + +#### Limit Depth + +```bash +# Show only h2-h3 +md-utils toc --min-level 2 --max-level 3 doc.md +``` + +#### Top-Level Only + +```bash +# Only h1 headings +md-utils toc --max-level 1 doc.md +``` + +## Common Use Cases + +### Documentation Navigation + +Add TOC to documentation files: + +```bash +# Generate TOC +md-utils toc README.md > toc.md + +# Insert into document +cat toc.md content.md > README-with-toc.md +``` + +### Site Map Generation + +Create site navigation from all pages: + +```bash +# Generate TOCs for all docs +md-utils --recursive toc docs/ --output toc/ + +# Combine into site map +md-utils -r toc --format plain docs/ > sitemap.txt +``` + +### Content Structure Analysis + +Analyze document structure: + +```bash +# Get structure as JSON +md-utils toc --format json doc.md | jq + +# Check heading depth +md-utils toc --format json doc.md | jq 'map(.level) | max' + +# Count headings per level +md-utils toc --format json doc.md | \\ + jq 'group_by(.level) | map({level: .[0].level, count: length})' +``` + +### Hugo/Jekyll Integration + +Generate navigation for static sites: + +```bash +# Generate TOC for each post +for file in content/posts/*.md; do + toc_file="layouts/toc/$(basename "$file")" + md-utils toc --format html --min-level 2 "$file" > "$toc_file" +done + +# Include in templates +``` + +### Validation + +Check document structure: + +```bash +#!/bin/bash +# validate-structure.sh + +file="$1" + +# Check if document has h1 +h1_count=$(md-utils toc --format json "$file" | jq '[.[] | select(.level == 1)] | length') + +if [ "$h1_count" -eq 0 ]; then + echo "Warning: $file missing h1 heading" +fi + +# Check max depth +max_depth=$(md-utils toc --format json "$file" | jq 'map(.level) | max') + +if [ "$max_depth" -gt 4 ]; then + echo "Warning: $file has deep nesting (level $max_depth)" +fi +``` + +## Pipeline Usage + +### Combine with Other Commands + +```bash +# Get TOC + body +echo "## Table of Contents" > output.md +md-utils toc post.md >> output.md +echo "" >> output.md +md-utils body post.md >> output.md + +# Filter specific sections +md-utils toc --format plain post.md | grep "API" + +# Process TOC with jq +md-utils toc --format json post.md | \\ + jq '[.[] | select(.level <= 3)]' +``` + +### Generate Multiple Formats + +```bash +#!/bin/bash +# generate-tocs.sh + +file="$1" +basename=$(basename "$file" .md) + +# Generate all formats +md-utils toc "$file" > "toc/${basename}-md.md" +md-utils toc --format plain "$file" > "toc/${basename}-plain.txt" +md-utils toc --format json "$file" > "toc/${basename}.json" +md-utils toc --format html "$file" > "toc/${basename}.html" +``` + +### Batch Processing + +```bash +# Generate TOCs for all docs +md-utils --recursive toc docs/ --output toc-files/ + +# Process with custom formatting +md-utils -r toc --format json docs/ | \\ + jq -s 'flatten | group_by(.text) | map({heading: .[0].text, count: length})' +``` + +## Advanced Examples + +### Custom TOC Insertion + +Insert TOC into document: + +```bash +#!/bin/bash +# insert-toc.sh + +file="$1" + +# Generate TOC +toc=$(md-utils toc --min-level 2 "$file") + +# Create new file with TOC +{ + # Copy frontmatter and title + md-utils fm dump --format raw --include-delimiters "$file" + md-utils toc --max-level 1 "$file" + + echo "" + echo "## Table of Contents" + echo "$toc" + echo "" + + # Copy body (skip h1) + md-utils body "$file" | tail -n +3 +} > "${file%.md}-with-toc.md" +``` + +### Section Extraction + +Extract specific sections based on TOC: + +```bash +# Get all "API" sections +md-utils toc --format json doc.md | \\ + jq -r '.[] | select(.text | contains("API")) | .slug' + +# Find section starting positions +md-utils toc --format json doc.md | \\ + jq '.[] | {text: .text, slug: .slug}' +``` + +### Multi-File Navigation + +Create master TOC for multiple files: + +```bash +#!/bin/bash +# master-toc.sh + +echo "# Master Table of Contents" +echo "" + +for file in docs/*.md; do + echo "## $(basename "$file" .md)" + md-utils toc --min-level 2 --format plain "$file" | sed 's/^/ /' + echo "" +done +``` + +## Format Details + +### md-bullet-links + +- Markdown bullet lists +- Anchor links to headings +- Hierarchical indentation +- GitHub-compatible + +### plain + +- Plain text only +- Hierarchical indentation +- No links or formatting +- Easy to parse + +### json + +- Array of heading objects +- Each object: `{level, text, slug}` +- Easy to process programmatically +- Flat array (hierarchy in level property) + +### html + +- HTML5 `