add cargo

This commit is contained in:
2025-06-04 23:53:05 +09:00
parent e191cb376c
commit 02dd69840d
16 changed files with 1473 additions and 0 deletions

22
src/commands/build.rs Normal file
View File

@ -0,0 +1,22 @@
use anyhow::Result;
use colored::Colorize;
use std::path::PathBuf;
use crate::generator::Generator;
use crate::config::Config;
pub async fn execute(path: PathBuf) -> Result<()> {
println!("{}", "Building blog...".green());
// Load configuration
let config = Config::load(&path)?;
// Create generator
let generator = Generator::new(path, config)?;
// Build the site
generator.build().await?;
println!("{}", "Build completed successfully!".green().bold());
Ok(())
}

21
src/commands/clean.rs Normal file
View File

@ -0,0 +1,21 @@
use anyhow::Result;
use colored::Colorize;
use std::fs;
use std::path::Path;
pub async fn execute() -> Result<()> {
println!("{}", "Cleaning build artifacts...".yellow());
let public_dir = Path::new("public");
if public_dir.exists() {
fs::remove_dir_all(public_dir)?;
println!("{} public directory", "Removed".cyan());
} else {
println!("{}", "No build artifacts to clean");
}
println!("{}", "Clean completed!".green().bold());
Ok(())
}

216
src/commands/init.rs Normal file
View File

@ -0,0 +1,216 @@
use anyhow::Result;
use colored::Colorize;
use std::fs;
use std::path::PathBuf;
pub async fn execute(path: PathBuf) -> Result<()> {
println!("{}", "Initializing new blog...".green());
// Create directory structure
let dirs = vec![
"content",
"content/posts",
"templates",
"static",
"static/css",
"static/js",
"static/images",
"public",
];
for dir in dirs {
let dir_path = path.join(dir);
fs::create_dir_all(&dir_path)?;
println!(" {} {}", "Created".cyan(), dir_path.display());
}
// Create default config
let config_content = r#"[site]
title = "My Blog"
description = "A blog powered by ailog"
base_url = "https://example.com"
language = "ja"
[build]
highlight_code = true
minify = false
[ai]
enabled = false
auto_translate = false
comment_moderation = false
"#;
fs::write(path.join("config.toml"), config_content)?;
println!(" {} config.toml", "Created".cyan());
// Create default template
let base_template = r#"<!DOCTYPE html>
<html lang="{{ config.language }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ config.title }}{% endblock %}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1><a href="/">{{ config.title }}</a></h1>
<p>{{ config.description }}</p>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>&copy; 2025 {{ config.title }}</p>
</footer>
</body>
</html>"#;
fs::write(path.join("templates/base.html"), base_template)?;
println!(" {} templates/base.html", "Created".cyan());
let index_template = r#"{% extends "base.html" %}
{% block content %}
<h2>Recent Posts</h2>
<ul class="post-list">
{% for post in posts %}
<li>
<a href="{{ post.url }}">{{ post.title }}</a>
<time>{{ post.date }}</time>
</li>
{% endfor %}
</ul>
{% endblock %}"#;
fs::write(path.join("templates/index.html"), index_template)?;
println!(" {} templates/index.html", "Created".cyan());
let post_template = r#"{% extends "base.html" %}
{% block title %}{{ post.title }} - {{ config.title }}{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<time>{{ post.date }}</time>
<div class="content">
{{ post.content | safe }}
</div>
</article>
{% endblock %}"#;
fs::write(path.join("templates/post.html"), post_template)?;
println!(" {} templates/post.html", "Created".cyan());
// Create default CSS
let css_content = r#"body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
header {
margin-bottom: 40px;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
header h1 {
margin: 0;
}
header h1 a {
color: #333;
text-decoration: none;
}
.post-list {
list-style: none;
padding: 0;
}
.post-list li {
margin-bottom: 15px;
}
.post-list time {
color: #666;
font-size: 0.9em;
margin-left: 10px;
}
article time {
color: #666;
display: block;
margin-bottom: 20px;
}
pre {
background-color: #f4f4f4;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
}
code {
background-color: #f4f4f4;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
}"#;
fs::write(path.join("static/css/style.css"), css_content)?;
println!(" {} static/css/style.css", "Created".cyan());
// Create sample post
let sample_post = r#"---
title: "Welcome to ailog"
date: 2025-01-06
tags: ["welcome", "ailog"]
---
# Welcome to ailog
This is your first post powered by **ailog** - a static blog generator with AI features.
## Features
- Fast static site generation
- Markdown support with frontmatter
- AI-powered features (coming soon)
- atproto integration for comments
## Getting Started
Create new posts with:
```bash
ailog new "My New Post"
```
Build your blog with:
```bash
ailog build
```
Happy blogging!"#;
fs::write(path.join("content/posts/welcome.md"), sample_post)?;
println!(" {} content/posts/welcome.md", "Created".cyan());
println!("\n{}", "Blog initialized successfully!".green().bold());
println!("\nNext steps:");
println!(" 1. cd {}", path.display());
println!(" 2. ailog build");
println!(" 3. ailog serve");
Ok(())
}

5
src/commands/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod init;
pub mod build;
pub mod new;
pub mod serve;
pub mod clean;

48
src/commands/new.rs Normal file
View File

@ -0,0 +1,48 @@
use anyhow::Result;
use chrono::Local;
use colored::Colorize;
use std::fs;
use std::path::PathBuf;
pub async fn execute(title: String, format: String) -> Result<()> {
println!("{} {}", "Creating new post:".green(), title);
let date = Local::now();
let filename = format!(
"{}-{}.{}",
date.format("%Y-%m-%d"),
title.to_lowercase().replace(' ', "-"),
format
);
let content = format!(
r#"---
title: "{}"
date: {}
tags: []
draft: false
---
# {}
Write your content here...
"#,
title,
date.format("%Y-%m-%d"),
title
);
let post_path = PathBuf::from("content/posts").join(&filename);
// Ensure directory exists
if let Some(parent) = post_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&post_path, content)?;
println!("{} {}", "Created:".cyan(), post_path.display());
println!("\nYou can now edit your post at: {}", post_path.display());
Ok(())
}

77
src/commands/serve.rs Normal file
View File

@ -0,0 +1,77 @@
use anyhow::Result;
use colored::Colorize;
use std::path::PathBuf;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
pub async fn execute(port: u16) -> Result<()> {
let addr = format!("127.0.0.1:{}", port);
let listener = TcpListener::bind(&addr).await?;
println!("{}", "Starting development server...".green());
println!("Serving at: {}", format!("http://{}", addr).blue().underline());
println!("Press Ctrl+C to stop\n");
loop {
let (stream, _) = listener.accept().await?;
tokio::spawn(handle_connection(stream));
}
}
async fn handle_connection(mut stream: TcpStream) -> Result<()> {
let mut buffer = [0; 1024];
stream.read(&mut buffer).await?;
let request = String::from_utf8_lossy(&buffer[..]);
let path = parse_request_path(&request);
let (status, content_type, content) = match serve_file(&path).await {
Ok((ct, data)) => ("200 OK", ct, data),
Err(_) => ("404 NOT FOUND", "text/html", b"<h1>404 - Not Found</h1>".to_vec()),
};
let response = format!(
"HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\n\r\n",
status,
content_type,
content.len()
);
stream.write_all(response.as_bytes()).await?;
stream.write_all(&content).await?;
stream.flush().await?;
Ok(())
}
fn parse_request_path(request: &str) -> String {
request
.lines()
.next()
.and_then(|line| line.split_whitespace().nth(1))
.unwrap_or("/")
.to_string()
}
async fn serve_file(path: &str) -> Result<(&'static str, Vec<u8>)> {
let file_path = if path == "/" {
PathBuf::from("public/index.html")
} else {
PathBuf::from("public").join(path.trim_start_matches('/'))
};
let content_type = match file_path.extension().and_then(|ext| ext.to_str()) {
Some("html") => "text/html",
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("json") => "application/json",
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("gif") => "image/gif",
Some("svg") => "image/svg+xml",
_ => "text/plain",
};
let content = tokio::fs::read(file_path).await?;
Ok((content_type, content))
}