Knowledge base of ~80+ markdown files across 14 domains (00-13), Logseq graph, hardware design files (KiCAD), infrastructure configs, and talas-wiki static site. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
172 lines
5.2 KiB
Go
172 lines
5.2 KiB
Go
package wiki
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
func (idx *Index) ReadRaw(urlPath string) (string, error) {
|
|
absPath := filepath.Join(idx.docsRoot, urlPath+".md")
|
|
absPath = filepath.Clean(absPath)
|
|
if !strings.HasPrefix(absPath, filepath.Clean(idx.docsRoot)) {
|
|
return "", os.ErrPermission
|
|
}
|
|
content, err := os.ReadFile(absPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(content), nil
|
|
}
|
|
|
|
func (idx *Index) SavePage(urlPath string, content string) error {
|
|
absPath := filepath.Join(idx.docsRoot, urlPath+".md")
|
|
absPath = filepath.Clean(absPath)
|
|
if !strings.HasPrefix(absPath, filepath.Clean(idx.docsRoot)) {
|
|
return os.ErrPermission
|
|
}
|
|
content = strings.ReplaceAll(content, "\r\n", "\n")
|
|
if !strings.HasSuffix(content, "\n") {
|
|
content += "\n"
|
|
}
|
|
return os.WriteFile(absPath, []byte(content), 0644)
|
|
}
|
|
|
|
func (idx *Index) CreatePage(urlPath string, content string) error {
|
|
absPath := filepath.Join(idx.docsRoot, urlPath+".md")
|
|
absPath = filepath.Clean(absPath)
|
|
if !strings.HasPrefix(absPath, filepath.Clean(idx.docsRoot)) {
|
|
return os.ErrPermission
|
|
}
|
|
if _, err := os.Stat(absPath); err == nil {
|
|
return fmt.Errorf("page already exists: %s", urlPath)
|
|
}
|
|
dir := filepath.Dir(absPath)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
content = strings.ReplaceAll(content, "\r\n", "\n")
|
|
if !strings.HasSuffix(content, "\n") {
|
|
content += "\n"
|
|
}
|
|
return os.WriteFile(absPath, []byte(content), 0644)
|
|
}
|
|
|
|
func (idx *Index) SaveUpload(relDir string, filename string, file io.Reader) (string, error) {
|
|
absDir := filepath.Join(idx.docsRoot, relDir)
|
|
absDir = filepath.Clean(absDir)
|
|
if !strings.HasPrefix(absDir, filepath.Clean(idx.docsRoot)) {
|
|
return "", os.ErrPermission
|
|
}
|
|
if err := os.MkdirAll(absDir, 0755); err != nil {
|
|
return "", err
|
|
}
|
|
filename = filepath.Base(filename)
|
|
filename = strings.ReplaceAll(filename, "..", "")
|
|
absPath := filepath.Join(absDir, filename)
|
|
out, err := os.Create(absPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer out.Close()
|
|
if _, err := io.Copy(out, file); err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(relDir, filename), nil
|
|
}
|
|
|
|
// RenamePage moves a page and updates all wikilinks pointing to it.
|
|
func (idx *Index) RenamePage(oldPath, newPath string) (int, error) {
|
|
oldAbs := filepath.Clean(filepath.Join(idx.docsRoot, oldPath+".md"))
|
|
newAbs := filepath.Clean(filepath.Join(idx.docsRoot, newPath+".md"))
|
|
root := filepath.Clean(idx.docsRoot)
|
|
|
|
if !strings.HasPrefix(oldAbs, root) || !strings.HasPrefix(newAbs, root) {
|
|
return 0, os.ErrPermission
|
|
}
|
|
if _, err := os.Stat(oldAbs); os.IsNotExist(err) {
|
|
return 0, fmt.Errorf("source page not found: %s", oldPath)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(newAbs), 0755); err != nil {
|
|
return 0, err
|
|
}
|
|
if err := os.Rename(oldAbs, newAbs); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Update wikilinks in all pages
|
|
oldName := filepath.Base(oldPath)
|
|
newName := filepath.Base(newPath)
|
|
updated := 0
|
|
|
|
idx.mu.RLock()
|
|
pages := make([]*Page, len(idx.allPages))
|
|
copy(pages, idx.allPages)
|
|
idx.mu.RUnlock()
|
|
|
|
for _, page := range pages {
|
|
if page.URLPath == oldPath {
|
|
continue
|
|
}
|
|
content, err := os.ReadFile(page.AbsPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
original := string(content)
|
|
modified := original
|
|
|
|
// Replace [[oldPath]] → [[newPath]]
|
|
modified = strings.ReplaceAll(modified, "[["+oldPath+"]]", "[["+newPath+"]]")
|
|
modified = strings.ReplaceAll(modified, "[["+oldPath+"|", "[["+newPath+"|")
|
|
|
|
// Replace [[oldName]] → [[newName]] (name-only links)
|
|
if oldName != newName {
|
|
nameRe := regexp.MustCompile(`\[\[` + regexp.QuoteMeta(oldName) + `(\||\]\])`)
|
|
modified = nameRe.ReplaceAllStringFunc(modified, func(m string) string {
|
|
return strings.Replace(m, oldName, newName, 1)
|
|
})
|
|
}
|
|
|
|
if modified != original {
|
|
os.WriteFile(page.AbsPath, []byte(modified), 0644)
|
|
updated++
|
|
}
|
|
}
|
|
|
|
return updated, nil
|
|
}
|
|
|
|
// FixBrokenLink replaces a broken wikilink target in a source file.
|
|
func (idx *Index) FixBrokenLink(sourcePath, oldTarget, newTarget string) error {
|
|
absPath := filepath.Clean(filepath.Join(idx.docsRoot, sourcePath+".md"))
|
|
if !strings.HasPrefix(absPath, filepath.Clean(idx.docsRoot)) {
|
|
return os.ErrPermission
|
|
}
|
|
content, err := os.ReadFile(absPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
modified := strings.ReplaceAll(string(content), "[["+oldTarget+"]]", "[["+newTarget+"]]")
|
|
modified = strings.ReplaceAll(modified, "[["+oldTarget+"|", "[["+newTarget+"|")
|
|
return os.WriteFile(absPath, []byte(modified), 0644)
|
|
}
|
|
|
|
// Page templates
|
|
|
|
type PageTemplate struct {
|
|
ID string
|
|
Name string
|
|
Content string
|
|
}
|
|
|
|
func GetPageTemplates() []PageTemplate {
|
|
return []PageTemplate{
|
|
{ID: "standard", Name: "Standard", Content: "# {{TITLE}}\n\n"},
|
|
{ID: "readme", Name: "README", Content: "# {{TITLE}}\n\n## Vue d'ensemble\n\n\n\n## Structure\n\n\n\n## Notes\n\n"},
|
|
{ID: "fiche_produit", Name: "Fiche Produit", Content: "# {{TITLE}}\n\n## Description\n\n\n\n## Specifications techniques\n\n| Parametre | Valeur |\n|-----------|--------|\n| | |\n\n## Prix cible\n\n\n\n## Sourcing composants\n\n\n\n## Notes\n\n"},
|
|
{ID: "rapport", Name: "Rapport de Session", Content: "# Rapport — {{TITLE}}\n\n## Date\n\n\n\n## Sujets abordes\n\n- \n\n## Decisions prises\n\n- \n\n## Actions a suivre\n\n- [ ] \n\n## Notes\n\n"},
|
|
}
|
|
}
|