add local TailwindCSS, fix side bar folder view, add more function to Markdown editor and allow .markdown files

This commit is contained in:
nahakubuilde
2025-08-28 07:29:51 +01:00
parent 090d491dd6
commit f364a4b6db
15 changed files with 714 additions and 79 deletions

View File

@@ -96,8 +96,8 @@ func (h *Handlers) CreateNoteHandler(c *gin.Context) {
title += ".md"
ext = "md"
} else {
// Has extension: allow if md or in allowed file extensions
allowed := ext == "md"
// Has extension: allow if md/markdown or in allowed file extensions
allowed := (ext == "md" || ext == "markdown")
if !allowed {
for _, a := range h.config.AllowedFileExtensions {
if strings.EqualFold(a, ext) {
@@ -143,7 +143,7 @@ func (h *Handlers) CreateNoteHandler(c *gin.Context) {
// Redirect based on extension
redirect := h.config.URLPrefix + "/note/" + notePath
if strings.ToLower(ext) != "md" {
if e := strings.ToLower(ext); e != "md" && e != "markdown" {
redirect = h.config.URLPrefix + "/view_text/" + notePath
}
@@ -158,11 +158,12 @@ func (h *Handlers) CreateNoteHandler(c *gin.Context) {
func (h *Handlers) EditNotePageHandler(c *gin.Context) {
notePath := strings.TrimPrefix(c.Param("path"), "/")
if !strings.HasSuffix(notePath, ".md") {
lp := strings.ToLower(notePath)
if !(strings.HasSuffix(lp, ".md") || strings.HasSuffix(lp, ".markdown")) {
c.HTML(http.StatusBadRequest, "error", gin.H{
"error": "Invalid note path",
"app_name": h.config.AppName,
"message": "Note path must end with .md",
"message": "Note path must end with .md or .markdown",
"ContentTemplate": "error_content",
"ScriptsTemplate": "error_scripts",
"Page": "error",
@@ -236,7 +237,13 @@ func (h *Handlers) EditNotePageHandler(c *gin.Context) {
return
}
title := strings.TrimSuffix(filepath.Base(notePath), ".md")
base := filepath.Base(notePath)
var title string
if strings.HasSuffix(strings.ToLower(base), ".markdown") {
title = strings.TrimSuffix(base, ".markdown")
} else {
title = strings.TrimSuffix(base, ".md")
}
folderPath := filepath.Dir(notePath)
if folderPath == "." {
folderPath = ""
@@ -264,7 +271,8 @@ func (h *Handlers) EditNoteHandler(c *gin.Context) {
notePath := strings.TrimPrefix(c.Param("path"), "/")
content := c.PostForm("content")
if !strings.HasSuffix(notePath, ".md") {
lp := strings.ToLower(notePath)
if !(strings.HasSuffix(lp, ".md") || strings.HasSuffix(lp, ".markdown")) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid note path"})
return
}

View File

@@ -1079,11 +1079,12 @@ func (h *Handlers) FolderHandler(c *gin.Context) {
func (h *Handlers) NoteHandler(c *gin.Context) {
notePath := strings.TrimPrefix(c.Param("path"), "/")
if !strings.HasSuffix(notePath, ".md") {
// Allow both .md and .markdown files
if !(strings.HasSuffix(strings.ToLower(notePath), ".md") || strings.HasSuffix(strings.ToLower(notePath), ".markdown")) {
c.HTML(http.StatusBadRequest, "error", gin.H{
"error": "Invalid note path",
"app_name": h.config.AppName,
"message": "Note path must end with .md",
"message": "Note path must end with .md or .markdown",
"ContentTemplate": "error_content",
"ScriptsTemplate": "error_scripts",
"Page": "error",
@@ -1143,15 +1144,48 @@ func (h *Handlers) NoteHandler(c *gin.Context) {
fullPath := filepath.Join(h.config.NotesDir, notePath)
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
c.HTML(http.StatusNotFound, "error", gin.H{
"error": "Note not found",
"app_name": h.config.AppName,
"message": "The requested note does not exist",
"ContentTemplate": "error_content",
"ScriptsTemplate": "error_scripts",
"Page": "error",
})
return
// Fallback: try the alternate markdown extension
base := filepath.Base(notePath)
dir := filepath.Dir(notePath)
lower := strings.ToLower(base)
var alt string
if strings.HasSuffix(lower, ".md") {
alt = base[:len(base)-len(".md")] + ".markdown"
} else if strings.HasSuffix(lower, ".markdown") {
alt = base[:len(base)-len(".markdown")] + ".md"
}
if alt != "" {
altRel := alt
if dir != "." && dir != "" {
altRel = filepath.Join(dir, alt)
}
altFull := filepath.Join(h.config.NotesDir, altRel)
if _, err2 := os.Stat(altFull); err2 == nil {
// Use the alternate path
notePath = altRel
fullPath = altFull
} else {
c.HTML(http.StatusNotFound, "error", gin.H{
"error": "Note not found",
"app_name": h.config.AppName,
"message": "The requested note does not exist",
"ContentTemplate": "error_content",
"ScriptsTemplate": "error_scripts",
"Page": "error",
})
return
}
} else {
c.HTML(http.StatusNotFound, "error", gin.H{
"error": "Note not found",
"app_name": h.config.AppName,
"message": "The requested note does not exist",
"ContentTemplate": "error_content",
"ScriptsTemplate": "error_scripts",
"Page": "error",
})
return
}
}
content, err := os.ReadFile(fullPath)
@@ -1193,7 +1227,16 @@ func (h *Handlers) NoteHandler(c *gin.Context) {
return
}
title := strings.TrimSuffix(filepath.Base(notePath), ".md")
base := filepath.Base(notePath)
lower := strings.ToLower(base)
var title string
if strings.HasSuffix(lower, ".markdown") {
title = base[:len(base)-len(".markdown")]
} else if strings.HasSuffix(lower, ".md") {
title = base[:len(base)-len(".md")]
} else {
title = strings.TrimSuffix(base, filepath.Ext(base))
}
folderPath := filepath.Dir(notePath)
if folderPath == "." {
folderPath = ""
@@ -1679,7 +1722,7 @@ func (h *Handlers) SearchHandler(c *gin.Context) {
// Skip disallowed files
ext := strings.ToLower(filepath.Ext(relPath))
isMD := ext == ".md"
isMD := ext == ".md" || ext == ".markdown"
if !isMD && !models.IsAllowedFile(relPath, h.config.AllowedFileExtensions) {
return nil
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
emoji "github.com/yuin/goldmark-emoji"
"gobsidian/internal/config"
)
@@ -22,6 +23,38 @@ type Renderer struct {
config *config.Config
}
// processMermaidFences wraps fenced code blocks marked as "mermaid" into <div class="mermaid">...</div>
func (r *Renderer) processMermaidFences(content string) string {
// ```mermaid\n...\n```
re := regexp.MustCompile("(?s)```mermaid\\s*(.*?)\\s*```")
return re.ReplaceAllString(content, "<div class=\"mermaid\">$1</div>")
}
// processMediaEmbeds turns a bare URL on its own line into an embed element
// Supports: audio (mp3|wav|ogg), video (mp4|webm|ogg), pdf
func (r *Renderer) processMediaEmbeds(content string) string {
lines := strings.Split(content, "\n")
mediaRe := regexp.MustCompile(`^(https?://\S+\.(?:mp3|wav|ogg|mp4|webm|ogg|pdf))$`)
for i, ln := range lines {
trimmed := strings.TrimSpace(ln)
m := mediaRe.FindStringSubmatch(trimmed)
if len(m) == 0 {
continue
}
url := m[1]
switch {
case strings.HasSuffix(strings.ToLower(url), ".mp3") || strings.HasSuffix(strings.ToLower(url), ".wav") || strings.HasSuffix(strings.ToLower(url), ".ogg"):
lines[i] = fmt.Sprintf("<audio controls preload=\"metadata\" src=\"%s\"></audio>", url)
case strings.HasSuffix(strings.ToLower(url), ".mp4") || strings.HasSuffix(strings.ToLower(url), ".webm") || strings.HasSuffix(strings.ToLower(url), ".ogg"):
lines[i] = fmt.Sprintf("<video controls preload=\"metadata\" style=\"max-width:100%%\" src=\"%s\"></video>", url)
case strings.HasSuffix(strings.ToLower(url), ".pdf"):
lines[i] = fmt.Sprintf("<iframe src=\"%s\" style=\"width:100%%;height:70vh;border:1px solid #374151;border-radius:8px\"></iframe>", url)
}
}
return strings.Join(lines, "\n")
}
func NewRenderer(cfg *config.Config) *Renderer {
md := goldmark.New(
goldmark.WithExtensions(
@@ -29,6 +62,11 @@ func NewRenderer(cfg *config.Config) *Renderer {
extension.Table,
extension.Strikethrough,
extension.TaskList,
extension.Footnote,
extension.DefinitionList,
extension.Linkify,
extension.Typographer,
emoji.Emoji,
highlighting.NewHighlighting(
highlighting.WithStyle("github-dark"),
highlighting.WithFormatOptions(
@@ -61,6 +99,12 @@ func (r *Renderer) RenderMarkdown(content string, notePath string) (string, erro
// Process Obsidian links
content = r.processObsidianLinks(content)
// Convert Mermaid fenced code blocks to <div class="mermaid"> for client rendering
content = r.processMermaidFences(content)
// Convert bare media links on their own line to embedded players (audio/video/pdf)
content = r.processMediaEmbeds(content)
var buf bytes.Buffer
if err := r.md.Convert([]byte(content), &buf); err != nil {
return "", err

View File

@@ -45,7 +45,7 @@ type ImageStorageInfo struct {
func GetFileType(extension string, allowedImageExts, allowedFileExts []string) FileType {
ext := strings.ToLower(strings.TrimPrefix(extension, "."))
if ext == "md" {
if ext == "md" || ext == "markdown" {
return FileTypeMarkdown
}

View File

@@ -136,7 +136,15 @@ func GetFolderContents(folderPath string, cfg *config.Config) ([]models.FileInfo
// Set display name based on file type
if fileInfo.Type == models.FileTypeMarkdown {
fileInfo.DisplayName = strings.TrimSuffix(entry.Name(), ".md")
name := entry.Name()
lower := strings.ToLower(name)
if strings.HasSuffix(lower, ".markdown") {
fileInfo.DisplayName = name[:len(name)-len(".markdown")]
} else if strings.HasSuffix(lower, ".md") {
fileInfo.DisplayName = name[:len(name)-len(".md")]
} else {
fileInfo.DisplayName = strings.TrimSuffix(name, filepath.Ext(name))
}
} else {
fileInfo.DisplayName = entry.Name()
}