记录2025年5月17日博客界面更新
更换新界面是因为先前的界面虽然比更早之前的界面好看一点,颜色更统一点,但是有些组件的设计还是不尽如人意.
第一代
简单修改 Fthemes的Limousine
第二代
更换许多组件的样式,使用更多自行编写的外部css文件,更换主题颜色
在这一阶段还有一些更细致的更新,比如我写博文的工具换成了Typora,同时引入了Blogger的"博文摘要", 用户不需要在加载主页时加载主页要显示的所有文章全量内容,同时现在可以在一页中显示更多的博文卡片.
移动视图的主页中使用了自定义的卡片组件展示文章,调整了字体大小
另外移动视图的部分没有用到原主题Limousine的内容,因此在移动视图的页面下没有展示主题来源信息
在归档方面使用了自己编写的组件,可以缓存记录.
第三代
除了必须的blogger组件以外,全部换用自编写的组件和自定义的css
该风格渐渐固定,之后的更新重点会放在调整字符显示大小,一些小控件的样式等内容上.
页面内部侧边栏使用目录
移除了下部的3个列表组件, 博客低语放在主页面的侧边栏.
取消独立的labels组件,放到了主页面侧边栏,未来也会单开一个独立labels页面.
blogroll暂时取消,未来会在所有页面底部做一个Blogroll展示卡片
我是如何写blog的
我的工作流是 Typora导出为html, 通过Typora自定义命令,将导出的html通过自己写的golang脚本转成可以直接上传到Blogger的html部分,并复制到剪贴板同时删除临时的html文件.
我的golang脚本如下, typora以 html为模板,执行导出后命令bodyclip.exe ${outputPath}
x// filepath: [main.go](http://_vscodecontentref_/0)
package main
import (
"bodyclip/utils"
"bytes"
"fmt"
"os"
"regexp"
"strings"
"unicode"
"github.com/atotto/clipboard"
"golang.org/x/net/html"
)
const SummaryLength = 260
func main() {
// 检查是否传入命令行参数(HTML 文件路径)
if len(os.Args) < 2 {
fmt.Println("go-BodyClip")
fmt.Println("Usage: go run main.go <html_file_path> [-t]")
os.Exit(1)
}
filePath := os.Args[1]
is_translate := false
// 尝试获取translate参数
if len(os.Args) > 2 {
if os.Args[2] == "-t" || os.Args[2] == "--translate" {
is_translate = true
}
}
// 检查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
fmt.Printf("Error: 文件 '%s' 不存在!\n", filePath)
os.Exit(1)
}
// 读取文件内容
content, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Error: 读取文件失败: %v\n", err)
os.Exit(1)
}
// 解析 HTML 内容
doc, err := html.Parse(bytes.NewReader(content))
if err != nil {
fmt.Printf("Error: 解析 HTML 失败: %v\n", err)
os.Exit(1)
}
// 从解析树中找到 <body> 节点
bodyNode := findBody(doc)
if bodyNode == nil {
fmt.Println("Error: 未找到 <body> 标签!")
os.Exit(1)
}
// 处理内容,根据规则插入 <!-- more -->
bodyContent := processBodyContent(bodyNode)
// 如果需要翻译,先进行翻译
if is_translate {
translated, err := utils.HandleTranslate(bodyNode)
if err != nil {
fmt.Printf("Error: 翻译失败: %v\n", err)
os.Exit(1)
}
bodyContent = translated
}
// 将内容复制到剪贴板
if err := clipboard.WriteAll(bodyContent); err != nil {
fmt.Printf("Error: 复制到剪贴板失败: %v\n", err)
} else {
fmt.Println("已将处理后的内容复制到剪贴板。")
}
// 删除原始 HTML 文件
if err := os.Remove(filePath); err != nil {
fmt.Printf("警告: 无法删除文件 '%s': %v\n", filePath, err)
} else {
fmt.Printf("文件 '%s' 已成功删除。\n", filePath)
}
}
// findBody 递归查找 HTML 解析树中的 <body> 节点
func findBody(n *html.Node) *html.Node {
if n.Type == html.ElementNode && n.Data == "body" {
return n
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if res := findBody(c); res != nil {
return res
}
}
return nil
}
// 查找第一个 hr 标签
func findFirstHr(n *html.Node) *html.Node {
if n.Type == html.ElementNode && n.Data == "hr" {
return n
}
// 递归查找子节点
for c := n.FirstChild; c != nil; c = c.NextSibling {
if hr := findFirstHr(c); hr != nil {
return hr
}
}
return nil
}
// 计算节点的文本内容长度 - 更准确的实现
func calculateTextLength(n *html.Node) int {
if n.Type == html.TextNode {
// 解码HTML实体
text := html.UnescapeString(n.Data)
// 规范化空白字符
text = normalizeWhitespace(text)
return len([]rune(text)) // 使用rune计数以正确处理中文等多字节字符
}
// 跳过不显示文本内容的元素
if isNonTextElement(n.Data) {
return 0
}
length := 0
for c := n.FirstChild; c != nil; c = c.NextSibling {
length += calculateTextLength(c)
}
return length
}
// 规范化空白字符
func normalizeWhitespace(s string) string {
// 将连续的空白字符替换为单个空格
var result strings.Builder
var lastWasSpace bool
for _, r := range s {
if unicode.IsSpace(r) {
if !lastWasSpace {
result.WriteRune(' ')
lastWasSpace = true
}
} else {
result.WriteRune(r)
lastWasSpace = false
}
}
return strings.TrimSpace(result.String())
}
// 判断是否是不应该计算文本的元素
func isNonTextElement(tag string) bool {
nonTextElements := map[string]bool{
"script": true,
"style": true,
"head": true,
"meta": true,
"link": true,
"title": true,
}
return nonTextElements[tag]
}
// 处理 body 内容并插入 <!-- more -->
func processBodyContent(bodyNode *html.Node) string {
// 将整个 body 转为 HTML 字符串
var bodyBuf bytes.Buffer
for c := bodyNode.FirstChild; c != nil; c = c.NextSibling {
html.Render(&bodyBuf, c)
}
bodyContent := bodyBuf.String()
// 正则表达式匹配 hr 标签 - 包括自闭合和非自闭合形式
hrRegex := regexp.MustCompile(`<hr[^>]*>`)
// 查找第一个匹配项
loc := hrRegex.FindStringIndex(bodyContent)
if loc != nil {
// hr 标签位置
hrStart, hrEnd := loc[0], loc[1]
_ = hrEnd
// 在 hr 标签前插入 <!-- more -->
result := bodyContent[:hrStart] +
"\n\n<!-- more -->\n\n" +
bodyContent[hrStart:]
return result
} else {
// 如果没有 hr 标签,根据文本长度插入 <!-- more -->
return insertMoreByTextLength(bodyNode)
}
}
// 在 hr 标签前插入 <!-- more -->
func insertMoreBeforeHr(bodyNode, hrNode *html.Node) string {
var buf bytes.Buffer
var foundHr bool
// 递归处理每个节点,采用更清晰的分段处理方式
var processNode func(n *html.Node)
processNode = func(n *html.Node) {
// 如果节点是 hr 标签
if n == hrNode {
// 在 hr 前插入 <!-- more -->
buf.WriteString("\n\n<!-- more -->\n\n")
html.Render(&buf, n)
foundHr = true
return
}
if n.Type == html.ElementNode {
// 渲染开始标签
buf.WriteString("<" + n.Data)
for _, attr := range n.Attr {
buf.WriteString(fmt.Sprintf(" %s=\"%s\"", attr.Key, attr.Val))
}
if n.FirstChild == nil && isVoidElement(n.Data) {
buf.WriteString(" />")
return
}
buf.WriteString(">")
// 处理所有子节点
for c := n.FirstChild; c != nil; c = c.NextSibling {
if !foundHr {
processNode(c)
// 如果处理子节点时找到了 hr,不再处理其余子节点
if foundHr {
break
}
} else {
// hr 之后的内容直接渲染,不再查找 hr
html.Render(&buf, c)
}
}
// 渲染结束标签
buf.WriteString("</" + n.Data + ">")
} else {
html.Render(&buf, n)
}
}
// 处理所有子节点
for child := bodyNode.FirstChild; child != nil; child = child.NextSibling {
if !foundHr {
processNode(child)
} else {
// hr 之后的内容直接渲染
html.Render(&buf, child)
}
}
return buf.String()
}
// 根据文本长度插入 <!-- more -->
func insertMoreByTextLength(bodyNode *html.Node) string {
var buf bytes.Buffer
textCount := 0
inserted := false
// 递归计算文本长度并在合适位置插入 <!-- more -->
var traverseAndInsert func(n *html.Node) int
traverseAndInsert = func(n *html.Node) int {
if inserted {
html.Render(&buf, n)
return 0
}
if n.Type == html.TextNode {
// 使用新的文本长度计算方法
text := html.UnescapeString(n.Data)
text = normalizeWhitespace(text)
textLen := len([]rune(text))
html.Render(&buf, n)
return textLen
}
// 跳过不显示文本内容的元素
if isNonTextElement(n.Data) {
html.Render(&buf, n)
return 0
}
if n.Type == html.ElementNode {
// 渲染开始标签
buf.WriteString("<" + n.Data)
for _, attr := range n.Attr {
buf.WriteString(fmt.Sprintf(" %s=\"%s\"", attr.Key, attr.Val))
}
if n.FirstChild == nil && isVoidElement(n.Data) {
buf.WriteString(" />")
return 0
}
buf.WriteString(">")
// 处理子节点
nodeTextLen := 0
for c := n.FirstChild; c != nil; c = c.NextSibling {
nodeTextLen += traverseAndInsert(c)
}
// 渲染结束标签
buf.WriteString("</" + n.Data + ">")
// 检查是否应该在此节点后插入 <!-- more -->
if !inserted && textCount <= SummaryLength && textCount+nodeTextLen > SummaryLength {
buf.WriteString("\n\n<!-- more -->\n\n")
inserted = true
}
textCount += nodeTextLen
return nodeTextLen
}
html.Render(&buf, n)
return 0
}
// 处理所有子节点
for child := bodyNode.FirstChild; child != nil; child = child.NextSibling {
traverseAndInsert(child)
}
return buf.String()
}
// 判断是否是自闭合元素
func isVoidElement(tag string) bool {
voidElements := map[string]bool{
"area": true, "base": true, "br": true, "col": true,
"embed": true, "hr": true, "img": true, "input": true,
"link": true, "meta": true, "param": true, "source": true,
"track": true, "wbr": true,
}
return voidElements[tag]
}
0 评论:
发表评论