fix: unignore shared/workspace package; add missing file to git
This commit is contained in:
parent
ea532659b0
commit
692d243157
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ bin/
|
||||
# Workspace — conteúdo pessoal dos posts
|
||||
workspace/
|
||||
workspace-test/
|
||||
!shared/workspace/
|
||||
|
||||
# Go build cache
|
||||
*.test
|
||||
|
||||
200
shared/workspace/workspace.go
Normal file
200
shared/workspace/workspace.go
Normal file
@ -0,0 +1,200 @@
|
||||
package workspace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// ─── Path helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
// PostPath returns the post root directory: <workspace>/<categoria>/<slug>
|
||||
func PostPath(workspaceRoot, categoria, slug string) string {
|
||||
return filepath.Join(workspaceRoot, categoria, slug)
|
||||
}
|
||||
|
||||
// InputPath returns the input/ subdirectory of a post.
|
||||
func InputPath(postPath string) string {
|
||||
return filepath.Join(postPath, "input")
|
||||
}
|
||||
|
||||
// WorkPath returns the work/ subdirectory of a post.
|
||||
func WorkPath(postPath string) string {
|
||||
return filepath.Join(postPath, "work")
|
||||
}
|
||||
|
||||
// OutputPath returns the output/ subdirectory of a post.
|
||||
func OutputPath(postPath string) string {
|
||||
return filepath.Join(postPath, "output")
|
||||
}
|
||||
|
||||
// ArtRawPath returns the work/art-raw/ subdirectory.
|
||||
func ArtRawPath(postPath string) string {
|
||||
return filepath.Join(postPath, "work", "art-raw")
|
||||
}
|
||||
|
||||
// InputTextoPath returns the path to input/texto.md.
|
||||
func InputTextoPath(postPath string) string {
|
||||
return filepath.Join(InputPath(postPath), "texto.md")
|
||||
}
|
||||
|
||||
// OutputPostPath returns the path to output/post.md.
|
||||
func OutputPostPath(postPath string) string {
|
||||
return filepath.Join(OutputPath(postPath), "post.md")
|
||||
}
|
||||
|
||||
// ArtPromptsPath returns work/art-prompts.json.
|
||||
func ArtPromptsPath(postPath string) string {
|
||||
return filepath.Join(WorkPath(postPath), "art-prompts.json")
|
||||
}
|
||||
|
||||
// InboxSugestoes returns the inbox sugestões file path.
|
||||
func InboxSugestoes(workspaceRoot string) string {
|
||||
return filepath.Join(workspaceRoot, "_inbox", "_sugestoes.md")
|
||||
}
|
||||
|
||||
// VersionedFile returns the path for a versioned file like redator-v1.md.
|
||||
func VersionedFile(postPath, prefix string, n int) string {
|
||||
return filepath.Join(WorkPath(postPath), fmt.Sprintf("%s-v%d.md", prefix, n))
|
||||
}
|
||||
|
||||
// ─── Discovery ────────────────────────────────────────────────────────────────
|
||||
|
||||
// FindVariantsByBase returns all postPaths whose slug is exactly baseSlug OR
|
||||
// starts with baseSlug+"--" (recycled variants like "rag-csharp--porque").
|
||||
// Only paths that have a work/ subdirectory are included.
|
||||
func FindVariantsByBase(workspaceRoot, baseSlug string) []string {
|
||||
var found []string
|
||||
cats, err := os.ReadDir(workspaceRoot)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
prefix := baseSlug + "--"
|
||||
for _, cat := range cats {
|
||||
if !cat.IsDir() || strings.HasPrefix(cat.Name(), "_") || strings.HasPrefix(cat.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
catPath := filepath.Join(workspaceRoot, cat.Name())
|
||||
slugs, err := os.ReadDir(catPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, s := range slugs {
|
||||
if !s.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := s.Name()
|
||||
if name == baseSlug || strings.HasPrefix(name, prefix) {
|
||||
candidate := filepath.Join(catPath, name)
|
||||
if _, err := os.Stat(WorkPath(candidate)); err == nil {
|
||||
found = append(found, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// FindPostBySlug searches all category subdirectories for a post with the given
|
||||
// slug. Returns the postPath on success.
|
||||
func FindPostBySlug(workspaceRoot, slug string) (string, error) {
|
||||
entries, err := os.ReadDir(workspaceRoot)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ler workspace %s: %w", workspaceRoot, err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || strings.HasPrefix(e.Name(), "_") || strings.HasPrefix(e.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
candidate := PostPath(workspaceRoot, e.Name(), slug)
|
||||
if _, err := os.Stat(WorkPath(candidate)); err == nil {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("slug %q não encontrado em %s", slug, workspaceRoot)
|
||||
}
|
||||
|
||||
// LatestVersionFile scans work/ for files matching <prefix>-vN.md and returns
|
||||
// the path and version number of the highest version, or ("", 0) if none.
|
||||
func LatestVersionFile(postPath, prefix string) (string, int) {
|
||||
maxN := 0
|
||||
for n := 1; n <= 99; n++ {
|
||||
path := VersionedFile(postPath, prefix, n)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
maxN = n
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if maxN == 0 {
|
||||
return "", 0
|
||||
}
|
||||
return VersionedFile(postPath, prefix, maxN), maxN
|
||||
}
|
||||
|
||||
// ─── Directory setup ──────────────────────────────────────────────────────────
|
||||
|
||||
// EnsureDirs creates input/, work/, output/, and work/art-raw/ for a post.
|
||||
func EnsureDirs(postPath string) error {
|
||||
dirs := []string{
|
||||
InputPath(postPath),
|
||||
WorkPath(postPath),
|
||||
OutputPath(postPath),
|
||||
ArtRawPath(postPath),
|
||||
}
|
||||
for _, d := range dirs {
|
||||
if err := os.MkdirAll(d, 0755); err != nil {
|
||||
return fmt.Errorf("criar %s: %w", d, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ─── Slug ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// SlugFromTitle converts a human-readable title to a lowercase ASCII slug.
|
||||
// "Como criar RAG com C#: guia completo" → "como-criar-rag-com-csharp-guia-completo"
|
||||
func SlugFromTitle(title string) string {
|
||||
// Replace common non-ASCII sequences before lowercasing
|
||||
replacements := map[string]string{
|
||||
"C#": "csharp",
|
||||
"C++": "cpp",
|
||||
".NET": "dotnet",
|
||||
"@": "at",
|
||||
}
|
||||
for old, newVal := range replacements {
|
||||
title = strings.ReplaceAll(title, old, newVal)
|
||||
}
|
||||
|
||||
title = strings.ToLower(title)
|
||||
|
||||
var result []rune
|
||||
for _, r := range title {
|
||||
switch {
|
||||
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
||||
result = append(result, r)
|
||||
case r == ' ' || r == '-' || r == '_' || r == ':' || r == '/':
|
||||
if len(result) > 0 && result[len(result)-1] != '-' {
|
||||
result = append(result, '-')
|
||||
}
|
||||
}
|
||||
// All other chars (accents, punctuation) dropped
|
||||
}
|
||||
|
||||
slug := strings.Trim(string(result), "-")
|
||||
|
||||
// Normalize accented chars with simple replacements
|
||||
slug = strings.NewReplacer(
|
||||
"ã", "a", "á", "a", "â", "a", "à", "a",
|
||||
"é", "e", "ê", "e", "è", "e",
|
||||
"í", "i", "î", "i",
|
||||
"ó", "o", "ô", "o", "õ", "o",
|
||||
"ú", "u", "û", "u",
|
||||
"ç", "c",
|
||||
"ñ", "n",
|
||||
).Replace(slug)
|
||||
|
||||
return slug
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user