add local TailwindCSS, fix side bar folder view, add more function to Markdown editor and allow .markdown files
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user