File size: 10,441 Bytes
8c62aab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c725543
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import markdown
import aiofiles
import os
import uuid
from datetime import datetime
import json
import re
from typing import List, Optional

app = FastAPI(title="HTML/Markdown Preview API", version="1.0.0")

# 挂载静态文件目录
app.mount("/static", StaticFiles(directory="static"), name="static")

class HTMLRequest(BaseModel):
    html_content: str

class MarkdownRequest(BaseModel):
    markdown_content: str

class PreviewResponse(BaseModel):
    url: str
    message: str

class FileInfo(BaseModel):
    filename: str
    title: Optional[str] = None
    url: str
    created_time: str

class FileListResponse(BaseModel):
    files: List[FileInfo]
    total: int

def generate_filename(extension: str = ".html") -> str:
    """生成唯一的文件名"""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    unique_id = str(uuid.uuid4())[:8]
    return f"{timestamp}_{unique_id}{extension}"

async def save_html_file(content: str, filename: str) -> str:
    """保存HTML内容到文件"""
    file_path = os.path.join("static", filename)
    async with aiofiles.open(file_path, 'w', encoding='utf-8') as f:
        await f.write(content)
    return filename

def extract_title_from_html(html_content: str) -> Optional[str]:
    """从HTML内容中提取标题"""
    # 尝试从title标签提取
    title_match = re.search(r'<title[^>]*>(.*?)</title>', html_content, re.IGNORECASE | re.DOTALL)
    if title_match:
        title = title_match.group(1).strip()
        if title:
            return title
    
    # 尝试从h1标签提取
    h1_match = re.search(r'<h1[^>]*>(.*?)</h1>', html_content, re.IGNORECASE | re.DOTALL)
    if h1_match:
        title = h1_match.group(1).strip()
        if title:
            return title
    
    return None

async def get_html_files_info() -> List[FileInfo]:
    """获取static目录中所有HTML文件的信息"""
    files_info = []
    static_dir = "static"
    
    if not os.path.exists(static_dir):
        return files_info
    
    for filename in os.listdir(static_dir):
        if filename.endswith('.html'):
            file_path = os.path.join(static_dir, filename)
            
            # 获取文件创建时间
            stat = os.stat(file_path)
            created_time = datetime.fromtimestamp(stat.st_ctime).strftime("%Y-%m-%d %H:%M:%S")
            
            # 尝试提取标题
            try:
                async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
                    content = await f.read()
                    title = extract_title_from_html(content)
            except:
                title = None
            
            files_info.append(FileInfo(
                filename=filename,
                title=title,
                url=f"/static/{filename}",
                created_time=created_time
            ))
    
    # 按创建时间倒序排列
    files_info.sort(key=lambda x: x.created_time, reverse=True)
    return files_info

def markdown_to_html(markdown_content: str) -> str:
    """将Markdown转换为HTML"""
    # 配置markdown扩展
    extensions = [
        'markdown.extensions.extra',
        'markdown.extensions.codehilite',
        'markdown.extensions.toc',
        'markdown.extensions.tables',
        'markdown.extensions.fenced_code'
    ]
    
    # 转换markdown为HTML
    html_content = markdown.markdown(markdown_content, extensions=extensions)
    
    # 包装在完整的HTML文档中
    full_html = f"""

<!DOCTYPE html>

<html lang="zh-CN">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Markdown Preview</title>

    <style>

        body {{

            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;

            line-height: 1.6;

            max-width: 800px;

            margin: 0 auto;

            padding: 20px;

            color: #333;

        }}

        h1, h2, h3, h4, h5, h6 {{

            margin-top: 1.5em;

            margin-bottom: 0.5em;

        }}

        code {{

            background-color: #f4f4f4;

            padding: 2px 4px;

            border-radius: 3px;

            font-family: 'Consolas', 'Monaco', monospace;

        }}

        pre {{

            background-color: #f4f4f4;

            padding: 10px;

            border-radius: 5px;

            overflow-x: auto;

        }}

        pre code {{

            background-color: transparent;

            padding: 0;

        }}

        blockquote {{

            border-left: 4px solid #ddd;

            margin: 0;

            padding-left: 20px;

            color: #666;

        }}

        table {{

            border-collapse: collapse;

            width: 100%;

            margin: 1em 0;

        }}

        th, td {{

            border: 1px solid #ddd;

            padding: 8px;

            text-align: left;

        }}

        th {{

            background-color: #f2f2f2;

        }}

        img {{

            max-width: 100%;

            height: auto;

        }}

    </style>

</head>

<body>

    {html_content}

</body>

</html>

    """
    return full_html

@app.post("/api/html/preview", response_model=PreviewResponse)
async def preview_html(request: HTMLRequest):
    """

    接收HTML代码,返回在线访问链接

    """
    try:
        # 生成唯一文件名
        filename = generate_filename(".html")
        
        # 保存HTML文件
        await save_html_file(request.html_content, filename)
        
        # 构建访问URL
        url = f"/static/{filename}"
        
        return PreviewResponse(
            url=url,
            message=f"HTML预览已创建,可通过链接访问"
        )
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"创建HTML预览失败: {str(e)}")

@app.post("/api/markdown/preview", response_model=PreviewResponse)
async def preview_markdown(request: MarkdownRequest):
    """

    接收Markdown代码,返回渲染后的HTML在线访问链接

    """
    try:
        # 将Markdown转换为HTML
        html_content = markdown_to_html(request.markdown_content)
        
        # 生成唯一文件名
        filename = generate_filename(".html")
        
        # 保存HTML文件
        await save_html_file(html_content, filename)
        
        # 构建访问URL
        url = f"/static/{filename}"
        
        return PreviewResponse(
            url=url,
            message=f"Markdown预览已创建,可通过链接访问"
        )
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"创建Markdown预览失败: {str(e)}")

@app.get("/api/files", response_model=FileListResponse)
async def get_files():
    """

    获取所有HTML文件列表

    """
    try:
        files = await get_html_files_info()
        return FileListResponse(
            files=files,
            total=len(files)
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"获取文件列表失败: {str(e)}")

@app.delete("/api/files/{filename}")
async def delete_file(filename: str):
    """

    删除指定的HTML文件

    """
    try:
        # 安全检查:确保文件名不包含路径遍历字符
        if '..' in filename or '/' in filename or '\\' in filename:
            raise HTTPException(status_code=400, detail="无效的文件名")
        
        file_path = os.path.join("static", filename)
        
        if not os.path.exists(file_path):
            raise HTTPException(status_code=404, detail="文件不存在")
        
        os.remove(file_path)
        
        return {"message": f"文件 {filename} 已删除"}
        
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"删除文件失败: {str(e)}")

@app.get("/", response_class=HTMLResponse)
async def root():
    """

    返回前端管理页面

    """
    try:
        async with aiofiles.open("templates/index.html", 'r', encoding='utf-8') as f:
            content = await f.read()
        return HTMLResponse(content=content)
    except Exception as e:
        return HTMLResponse(content=f"<h1>错误</h1><p>无法加载页面: {str(e)}</p>")

@app.get("/api/docs")
async def api_docs():
    """

    API文档,返回使用说明

    """
    return {
        "message": "HTML/Markdown Preview API",
        "version": "1.0.0",
        "endpoints": {
            "html_preview": {
                "url": "/api/html/preview",
                "method": "POST",
                "description": "接收HTML代码,返回在线访问链接",
                "request_body": {
                    "html_content": "string - HTML代码内容"
                }
            },
            "markdown_preview": {
                "url": "/api/markdown/preview",
                "method": "POST",
                "description": "接收Markdown代码,返回渲染后的HTML在线访问链接",
                "request_body": {
                    "markdown_content": "string - Markdown代码内容"
                }
            },
            "files_list": {
                "url": "/api/files",
                "method": "GET",
                "description": "获取所有HTML文件列表"
            },
            "delete_file": {
                "url": "/api/files/{filename}",
                "method": "DELETE",
                "description": "删除指定的HTML文件"
            }
        },
        "example_usage": {
            "html": "curl -X POST http://localhost:8000/api/html/preview -H 'Content-Type: application/json' -d '{\"html_content\": \"<h1>Hello World</h1>\"}'",
            "markdown": "curl -X POST http://localhost:8000/api/markdown/preview -H 'Content-Type: application/json' -d '{\"markdown_content\": \"# Hello World\"}'"
        }
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)