feat: 站点单页、时间线导入与行级 BBCode 短代码
首页右栏接入单页入口与公告置顶/全部页;Markdown 编辑器支持可视化时间线与 Git 导入;隐藏块改为 [hide]/[timeline] 命名闭合,并修复单页/公告 GORM 更新只改 updated_at 的问题。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
158
backend/markdown/legacy_rewrite.go
Normal file
158
backend/markdown/legacy_rewrite.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RewriteLegacyDirectives 将旧式 :::hide / :::timeline / 裸 ::: 改写为行级 BBCode。
|
||||
// 幂等:已是 BBCode 的内容原样返回(若无旧标记)。代码围栏内不改写。
|
||||
func RewriteLegacyDirectives(content string) string {
|
||||
if content == "" {
|
||||
return content
|
||||
}
|
||||
if !strings.Contains(content, ":::") {
|
||||
return content
|
||||
}
|
||||
lines := splitLines(content)
|
||||
var out []string
|
||||
inCode := false
|
||||
i := 0
|
||||
for i < len(lines) {
|
||||
trimmed := strings.TrimSpace(lines[i])
|
||||
if strings.HasPrefix(trimmed, "```") {
|
||||
inCode = !inCode
|
||||
out = append(out, lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if inCode {
|
||||
out = append(out, lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if kind, pts, pwd, locked, ok := parseLegacyHideOpen(trimmed); ok {
|
||||
bodyLines := make([]string, 0)
|
||||
j := i + 1
|
||||
innerCode := false
|
||||
found := false
|
||||
for j < len(lines) {
|
||||
inner := strings.TrimSpace(lines[j])
|
||||
if strings.HasPrefix(inner, "```") {
|
||||
innerCode = !innerCode
|
||||
bodyLines = append(bodyLines, lines[j])
|
||||
j++
|
||||
continue
|
||||
}
|
||||
if innerCode {
|
||||
bodyLines = append(bodyLines, lines[j])
|
||||
j++
|
||||
continue
|
||||
}
|
||||
if inner == ":::" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
bodyLines = append(bodyLines, lines[j])
|
||||
j++
|
||||
}
|
||||
out = append(out, openMarkerLine(kind, pts, pwd, locked))
|
||||
out = append(out, bodyLines...)
|
||||
if found {
|
||||
out = append(out, "[/hide]")
|
||||
i = j + 1
|
||||
} else {
|
||||
// 未闭合:仍写出已转换开标记与正文,避免丢内容
|
||||
i = j
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if trimmed == ":::timeline" || strings.HasPrefix(trimmed, ":::timeline ") {
|
||||
bodyLines := make([]string, 0)
|
||||
j := i + 1
|
||||
innerCode := false
|
||||
found := false
|
||||
for j < len(lines) {
|
||||
inner := strings.TrimSpace(lines[j])
|
||||
if strings.HasPrefix(inner, "```") {
|
||||
innerCode = !innerCode
|
||||
bodyLines = append(bodyLines, lines[j])
|
||||
j++
|
||||
continue
|
||||
}
|
||||
if innerCode {
|
||||
bodyLines = append(bodyLines, lines[j])
|
||||
j++
|
||||
continue
|
||||
}
|
||||
if inner == ":::" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
bodyLines = append(bodyLines, lines[j])
|
||||
j++
|
||||
}
|
||||
out = append(out, "[timeline]")
|
||||
out = append(out, bodyLines...)
|
||||
if found {
|
||||
out = append(out, "[/timeline]")
|
||||
i = j + 1
|
||||
} else {
|
||||
i = j
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, lines[i])
|
||||
i++
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
// parseLegacyHideOpen 解析旧式 :::hide <kind> [arg] [locked]
|
||||
func parseLegacyHideOpen(trimmed string) (kind string, points int, password string, locked bool, ok bool) {
|
||||
if !strings.HasPrefix(trimmed, ":::hide") {
|
||||
return "", 0, "", false, false
|
||||
}
|
||||
rest := strings.TrimSpace(trimmed[len(":::hide"):])
|
||||
if rest == "" {
|
||||
return "", 0, "", false, false
|
||||
}
|
||||
parts := strings.Fields(rest)
|
||||
if len(parts) == 0 {
|
||||
return "", 0, "", false, false
|
||||
}
|
||||
kind = parts[0]
|
||||
switch kind {
|
||||
case HideKindLogin, HideKindReply, HideKindPoints, HideKindPassword:
|
||||
default:
|
||||
return "", 0, "", false, false
|
||||
}
|
||||
idx := 1
|
||||
if kind == HideKindPoints {
|
||||
if idx >= len(parts) {
|
||||
return "", 0, "", false, false
|
||||
}
|
||||
n, err := strconv.Atoi(parts[idx])
|
||||
if err != nil {
|
||||
return "", 0, "", false, false
|
||||
}
|
||||
points = n
|
||||
idx++
|
||||
} else if kind == HideKindPassword {
|
||||
if idx < len(parts) && parts[idx] != "locked" {
|
||||
password = parts[idx]
|
||||
idx++
|
||||
}
|
||||
}
|
||||
for ; idx < len(parts); idx++ {
|
||||
if parts[idx] == "locked" {
|
||||
locked = true
|
||||
} else {
|
||||
return "", 0, "", false, false
|
||||
}
|
||||
}
|
||||
return kind, points, password, locked, true
|
||||
}
|
||||
Reference in New Issue
Block a user