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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ docci
dist/
.goreleaser/
build/

# used in the readme.md for testing
example.html
styles.css
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Your documentation is now your test suite! 🎯 *(pronounced "doc-ee", short for

A CI tool that brings your markdown docs to life by executing code blocks in sequence. Run processes in the background, handle environment variables, add delays, verify outputs, and modify files - all through simple markdown tags. Perfect for ensuring your docs stay accurate and your examples actually work! 📚

## 🌟 Projects Using Docci

Several projects have adopted Docci to ensure their documentation stays accurate and executable:

- **[Spawn](https://github.com/rollchains/spawn)** - Rapid Cosmos blockchain development framework
- **[WAVS](https://github.com/Lay3rLabs/wavs-middleware)** - Eigenlayer AVS WebAssembly development
- **[OndoChain](https://ondo.finance/ondo-chain/)** - Institutional RWA Cosmos-EVM blockchain

## 🏃‍♂️ Quick Start

Find sample workspaces in the [`examples/` directory](./examples/).
Expand All @@ -28,7 +36,7 @@ task install # go install ./*.go
# docci_Linux_x86_64, docci_Linux_arm64, docci_Darwin_x86_64, docci_Darwin_arm64
- name: Install Docci Readme Test Tool
run: |
VERSION=v0.9.0
VERSION=v0.9.2
BINARY=docci_Linux_x86_64.tar.gz
curl -fsSL "https://github.com/Reecepbcups/docci/releases/download/${VERSION}/${BINARY}" | sudo tar -xzC /usr/local/bin
sudo chmod +x /usr/local/bin/docci
Expand Down Expand Up @@ -65,6 +73,14 @@ docci version
* 🖥️ `docci-os=mac|linux`: Run the command only on it's the specified OS
* 🔄 `docci-replace-text="old;new"`: Replace text in the code block before execution (including env variables!)

### 📄 File Tags
* 📝 `docci-file`: The file name to operate on
* 🔄 `docci-reset-file`: Reset the file to its original content
* 🚫 `docci-if-file-not-exists`: Only run if a file does not exist
* ➕ `docci-line-insert=N`: Insert content at line N
* ✏️ `docci-line-replace=N`: Replace content at line N
* 📋 `docci-line-replace=N-M`: Replace content from line N to M


### 💡 Code Block Tag Examples (Operations)

Expand Down Expand Up @@ -139,3 +155,47 @@ Cleanup demo server if running in the background:
curl http://localhost:3000/kill
```
````

### 💡 Files Code Block Tag Examples

Create a new file from content: 📝

<!-- yes, the typo is meant to be here -->
````html
```html docci-file=example.html docci-reset-file
<html>
<head>
<title>My Titlee</title>
</head>
</html>
```
````

Replace the typo'ed line:

````html
```html docci-file=example.html docci-line-replace=3
<title>My Title</title>
```
````

Add new content

````html
```html docci-file=example.html docci-line-insert=4
<body>
<h1>My Header</h1>
<p>1 paragraph</p>
<p>2 paragraph</p>
</body>
```
````

Replace multiple lines

````html
```html docci-file=example.html docci-line-replace=7-9
<p>First paragraph</p>
<p>Second paragraph</p>
```
````
7 changes: 7 additions & 0 deletions examples/docci_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"files": [
"base.md",
"file-operations.md",
"if-not-installed-test.md"
]
}
97 changes: 97 additions & 0 deletions examples/file-operations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# File Operations Test

This example demonstrates the new file operation tags in docci.

## Create a new HTML file

```html docci-file="example.html" docci-reset-file
<!DOCTYPE html>
<html>
<head>
<title>My Titlee</title>
</head>
<body>
<h1>Welcome</h1>
</body>
</html>
```

## Verify the file was created

```bash docci-output-contains="<!DOCTYPE html>"
cat example.html
```

## Fix the typo in the title (line 4)

```html docci-file="example.html" docci-line-replace="4"
<title>My Title</title>
```

## Verify the typo was fixed

```bash docci-output-contains="My Title"
grep "title" example.html
```

## Insert content after the h1 tag (line 7)

```html docci-file="example.html" docci-line-insert="7"
<p>This is a paragraph</p>
<p>This is another paragraph</p>
```

## Verify the paragraphs were added

```bash docci-output-contains="This is a paragraph"
cat example.html
```

## Create a CSS file

```css docci-file="styles.css"
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
}

h1 {
color: #333;
}
```

## Add more styles at the end

```css docci-file="styles.css" docci-line-insert="10"

p {
line-height: 1.6;
color: #666;
}
```

## Replace the h1 color (line 8)

```css docci-file="styles.css" docci-line-replace="8"
color: #0066cc;
```

## Verify the final CSS

```bash docci-output-contains="color: #0066cc"
cat styles.css
```

## Test conditional file creation

```bash docci-if-file-not-exists="example.html"
echo "This should not run because example.html exists"
```

## Clean up

```bash
rm -f example.html styles.css
echo "Test files cleaned up"
```
80 changes: 76 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"fmt"
"os"
"os/exec"
Expand All @@ -23,6 +24,11 @@ var (
keepRunning bool
)

// DocciConfig represents the JSON configuration file format
type DocciConfig struct {
Files []string `json:"files"`
}

var rootCmd = &cobra.Command{
Use: "docci",
Short: "Execute and validate code blocks in markdown files",
Expand All @@ -33,11 +39,26 @@ It helps ensure your documentation examples are always accurate and working.`,
}

var runCmd = &cobra.Command{
Use: "run <markdown-files>",
Use: "run <markdown-files|config.json>",
Short: "Execute code blocks in markdown file(s)",
Long: `Execute all code blocks marked with 'exec' in markdown file(s).
The command will run the blocks in sequence and validate any expected outputs.
Multiple files can be specified separated by commas.`,

You can specify files in three ways:
1. Single file: docci run file.md
2. Multiple files (comma-separated): docci run file1.md,file2.md,file3.md
3. JSON config file: docci run config.json

When using a JSON config file, create a file with this format:
{
"files": [
"file1.md",
"subdir/file2.md",
"file3.md"
]
}

File paths in the JSON config are resolved relative to the config file's location.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// Initialize logging based on flags
Expand Down Expand Up @@ -340,9 +361,60 @@ func runCleanupCommands(commands []string) {
log.Info("Cleanup complete")
}

// parseFileList parses comma separated file paths
// parseFileList parses comma separated file paths or JSON config file
func parseFileList(input string) []string {
// Split by comma
// Check if input is a JSON file
if strings.HasSuffix(strings.ToLower(input), ".json") {
// Try to read and parse as JSON config
configData, err := os.ReadFile(input)
if err == nil {
var config DocciConfig
if jsonErr := json.Unmarshal(configData, &config); jsonErr == nil && len(config.Files) > 0 {
// Successfully parsed JSON config
// Get the directory containing the JSON file
configDir := filepath.Dir(input)

// Get git root for fallback
var gitRoot string
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
if output, err := cmd.Output(); err == nil {
gitRoot = strings.TrimSpace(string(output))
}

// Resolve relative file paths
var resolvedFiles []string
for _, file := range config.Files {
// If the file path is absolute, use it as-is
if filepath.IsAbs(file) {
resolvedFiles = append(resolvedFiles, file)
} else {
// First try relative to the JSON config directory
resolvedPath := filepath.Join(configDir, file)
if _, err := os.Stat(resolvedPath); err == nil {
resolvedFiles = append(resolvedFiles, resolvedPath)
} else if gitRoot != "" {
// If not found, try relative to git root
rootPath := filepath.Join(gitRoot, file)
if _, err := os.Stat(rootPath); err == nil {
resolvedFiles = append(resolvedFiles, rootPath)
} else {
// If still not found, use the original resolved path
// (will fail later with proper error message)
resolvedFiles = append(resolvedFiles, resolvedPath)
}
} else {
// No git root available, use original resolved path
resolvedFiles = append(resolvedFiles, resolvedPath)
}
}
}
return resolvedFiles
}
}
// If we can't parse as JSON config, treat it as a regular file
}

// Original comma-separated logic
if !strings.Contains(input, ",") {
// Single file
return []string{strings.TrimSpace(input)}
Expand Down
Loading