add cargo
This commit is contained in:
22
src/commands/build.rs
Normal file
22
src/commands/build.rs
Normal 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
21
src/commands/clean.rs
Normal 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
216
src/commands/init.rs
Normal 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>© 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
5
src/commands/mod.rs
Normal 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
48
src/commands/new.rs
Normal 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
77
src/commands/serve.rs
Normal 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))
|
||||
}
|
Reference in New Issue
Block a user