diff --git a/.gitignore b/.gitignore index ec1aff1..20d1120 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ vendor *.lock .vscode cache -content blog \ No newline at end of file diff --git a/cache.go b/cache.go index f23be2a..a7c063e 100644 --- a/cache.go +++ b/cache.go @@ -10,12 +10,9 @@ import ( ) var ( - memoryCache = make(map[string][]byte) - cacheMutex sync.RWMutex - cacheDir = "cache" - - // TestMode disables caching when set to true - TestMode bool + cacheDir = "cache" + cacheMutex sync.Mutex + TestMode bool ) // InitCache ensures the cache directory exists @@ -31,8 +28,8 @@ func ClearCache() { InitCache() } -// getCacheFilename generates the hashed filename preserving the extension -func getCacheFilename(key string) string { +// GetCacheFilename generates the hashed filename preserving the extension +func GetCacheFilename(key string) string { hash := md5.Sum([]byte(key)) ext := filepath.Ext(key) // Default to .html if no extension (e.g. for processed markdown) @@ -41,49 +38,3 @@ func getCacheFilename(key string) string { } return filepath.Join(cacheDir, hex.EncodeToString(hash[:])+ext) } - -// CheckCache looks for content in memory, then on disk. -func CheckCache(key string) ([]byte, bool) { - if TestMode { - return nil, false - } - - cacheMutex.RLock() - - // 1. Check Memory - if data, found := memoryCache[key]; found { - cacheMutex.RUnlock() - return data, true - } - cacheMutex.RUnlock() - - // 2. Check Disk - filePath := getCacheFilename(key) - data, err := os.ReadFile(filePath) - if err == nil { - // Populate memory for next time - cacheMutex.Lock() - memoryCache[key] = data - cacheMutex.Unlock() - return data, true - } - - return nil, false -} - -// StoreCache saves content to memory and disk. -func StoreCache(key string, data []byte) error { - if TestMode { - return nil - } - - cacheMutex.Lock() - defer cacheMutex.Unlock() - - // 1. Save to memory - memoryCache[key] = data - - // 2. Save to disk - filePath := getCacheFilename(key) - return os.WriteFile(filePath, data, 0644) -} diff --git a/content/default.css b/content/default.css new file mode 100644 index 0000000..c49940e --- /dev/null +++ b/content/default.css @@ -0,0 +1,237 @@ +/* content/default.css */ + +/* + The "27bslash6" Aesthetic: + - Stark Black & White + - Helvetica / Arial + - Narrow, centered reading column + - Zero border radius, zero shadows +*/ + +:root { + --bg-color: #ffffff; + --text-color: #000000; + --link-color: #cc0000; /* That specific "angry" red he sometimes uses, or just black */ + --meta-color: #666666; + --font-stack: "Helvetica Neue", Helvetica, Arial, sans-serif; + --max-width: 680px; /* The classic narrow column */ +} + +* { + box-sizing: border-box; +} + +body { + background-color: var(--bg-color); + color: var(--text-color); + font-family: var(--font-stack); + font-size: 15px; /* Slightly smaller, crisp text */ + line-height: 1.5; + margin: 0; + padding: 2rem 1rem; +} + +/* The main document container */ +body > * { + max-width: var(--max-width); + margin-left: auto; + margin-right: auto; + display: block; +} + +/* Typography */ +h1, h2, h3 { + font-weight: bold; + letter-spacing: -0.5px; + margin-top: 2.5rem; + margin-bottom: 1rem; +} + +h1 { + font-size: 24px; + border-bottom: 1px solid #000; + padding-bottom: 10px; + margin-top: 0; +} + +h2 { + font-size: 18px; +} + +p { + margin-bottom: 1.2rem; + text-align: justify; /* Gives it that "formal letter" look */ +} + +/* Links - Stark and obvious */ +a { + color: var(--text-color); + text-decoration: underline; + text-decoration-thickness: 1px; +} + +a:hover { + color: var(--link-color); + text-decoration: none; +} + +/* Code blocks - Raw and industrial */ +pre { + background: #f0f0f0; + border: 1px solid #ccc; + padding: 15px; + font-family: "Monaco", "Menlo", "Consolas", monospace; + font-size: 12px; + overflow-x: auto; + margin: 1.5rem 0; +} + +code { + font-family: "Monaco", "Menlo", "Consolas", monospace; + background: #f0f0f0; + padding: 2px 4px; + font-size: 90%; +} + +/* Blockquotes - The "Email Reply" look */ +blockquote { + margin: 1.5rem 0; + padding-left: 15px; + border-left: 4px solid #000; + color: var(--meta-color); + font-style: italic; +} + +/* Images - fit the column */ +img { + max-width: 100%; + height: auto; + display: block; + margin: 2rem auto; + border: 1px solid #000; /* Optional: adds to the brutalist feel */ +} + +/* Lists */ +ul, ol { + padding-left: 20px; + margin-bottom: 1.5rem; +} + +li { + margin-bottom: 0.5rem; +} + +/* + Specific Components +*/ + +/* Top Banner - The "System Notice" Style +*/ +.topbanner { + background-color: #f4f4f4; /* Very light grey, distinct from white body */ + border: 1px solid #000; /* Crisp definition */ + border-left-width: 6px; /* Thick accent, matches your sidebar theme */ + padding: 1.5rem 2rem; /* Generous padding for readability */ + margin-bottom: 3rem; /* Push the H1 down */ + font-size: 14px; /* Slightly smaller than body text */ +} + +/* Ensure text inside the banner doesn't inherit the 'justify' from global p */ +.topbanner p { + text-align: left; + margin-bottom: 0.5rem; +} + +.topbanner p:last-child { + margin-bottom: 0; +} + +/* Optional: Make links inside the banner aggressive */ +.topbanner a { + font-weight: bold; + border-bottom: 2px solid var(--link-color); + text-decoration: none; +} + +.topbanner a:hover { + background-color: var(--link-color); + color: white; +} + +/* The Site Headline (Top of Index) */ +.site-headline { + margin-bottom: 3rem; + text-align: left; +} + +.site-headline h1 { + font-size: 32px; + border: none; + padding: 0; + margin: 0; + letter-spacing: -1px; +} + +/* Sidebar Styles */ +.sidebar { + float: right; + width: 30%; + min-width: 250px; + margin-left: 2rem; + margin-bottom: 1rem; + padding: 1rem; + background-color: #f9f9f9; + border-left: 4px solid #333; +} + +/* The Latest Posts List */ +.latest-posts { + margin-top: 3rem; + border-top: 4px solid #000; + padding-top: 1rem; +} + +.latest-posts h2 { + font-size: 14px; + text-transform: uppercase; + color: var(--meta-color); + margin-bottom: 1rem; +} + +.latest-posts ul { + list-style: none; + padding: 0; +} + +.latest-posts li { + display: flex; + justify-content: space-between; + border-bottom: 1px solid #eee; + padding: 8px 0; +} + +.latest-posts .date { + color: var(--meta-color); + font-size: 12px; + margin-right: 1rem; + min-width: 100px; +} + +.latest-posts a { + text-decoration: none; + font-weight: bold; +} + +.latest-posts a:hover { + text-decoration: underline; + color: var(--link-color); +} + +/* Directory Link */ +.directory-link { + margin: 2rem 0; + text-align: right; + font-size: 12px; + text-transform: uppercase; + font-weight: bold; +} \ No newline at end of file diff --git a/content/entries/entry1.md b/content/entries/entry1.md new file mode 100644 index 0000000..930f533 --- /dev/null +++ b/content/entries/entry1.md @@ -0,0 +1 @@ +# I'm quite fond of lyrics. Particicularly, I like interpol lyrics. \ No newline at end of file diff --git a/content/index.md b/content/index.md new file mode 100644 index 0000000..1077833 --- /dev/null +++ b/content/index.md @@ -0,0 +1,11 @@ +_-_- +_-_- Else-If.org - Blog +_-_- + +# Welcome to the Else-If.org Blog directory! + +I'll come up with more to say soon! + +__p.s. my name is Jess P.S ;D__ + +||| \ No newline at end of file diff --git a/content/index.yml b/content/index.yml new file mode 100644 index 0000000..aef2a77 --- /dev/null +++ b/content/index.yml @@ -0,0 +1 @@ +title: Blog Homepage diff --git a/go.mod b/go.mod index f79cdfb..4f8d212 100644 --- a/go.mod +++ b/go.mod @@ -8,5 +8,6 @@ require go.abhg.dev/goldmark/frontmatter v0.3.0 require ( github.com/BurntSushi/toml v1.6.0 // indirect + github.com/tendstofortytwo/goldmark-customtag v0.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3716b1c..0b623c1 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tendstofortytwo/goldmark-customtag v0.0.1 h1:c3Wnzi98gE7BPaqio/UOQVGGJU0EMdgKwjQHo9Y7NKQ= +github.com/tendstofortytwo/goldmark-customtag v0.0.1/go.mod h1:9KHP0WeHIafExF+Zu4CIDfzgfMo/FaEKXUO7NFmCyos= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.abhg.dev/goldmark/frontmatter v0.3.0 h1:ZOrMkeyyYzhlbenFNmOXyGFx1dFE8TgBWAgZfs9D5RA= diff --git a/parse.go b/parse.go index 98096ad..0ca004a 100644 --- a/parse.go +++ b/parse.go @@ -4,7 +4,9 @@ package main import ( "bytes" "fmt" + "os" "path/filepath" + "regexp" "strings" "git.else-if.org/jess/blog/templates" @@ -15,104 +17,146 @@ import ( "go.abhg.dev/goldmark/frontmatter" ) -// GetContent is the primary entry point for retrieving parsed content. -func GetContent(file ContentFile) ([]byte, error) { - // 1. Ask Cache - if data, found := CheckCache(file.OriginalPath); found { - return data, nil - } +// GetContentPath ensures the content is cached and returns the path to the cached file. +func GetContentPath(file ContentFile) (string, error) { + cachePath := GetCacheFilename(file.OriginalPath) - // 2. Read Raw - raw, err := ReadRaw(file.OriginalPath) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } + // 1. Check Cache Freshness (if not in TestMode) + if !TestMode { + cacheInfo, errCache := os.Stat(cachePath) + origInfo, errOrig := os.Stat(file.OriginalPath) - // If not markdown (e.g. css, images), return raw bytes immediately - if !file.IsMarkdown { - _ = StoreCache(file.OriginalPath, raw) - return raw, nil - } - - // 3. Configure Goldmark - // We use the Extender pattern which is the standard way to add frontmatter support. - // We also enable Unsafe mode to allow raw HTML tags (like ) to pass through - // so we can replace them later. - md := goldmark.New( - goldmark.WithExtensions( - extension.GFM, - &frontmatter.Extender{}, - ), - goldmark.WithRendererOptions( - html.WithUnsafe(), - ), - ) - - // 4. Parse Markdown - var buf bytes.Buffer - ctx := parser.NewContext() - - if err := md.Convert(raw, &buf, parser.WithContext(ctx)); err != nil { - return nil, fmt.Errorf("failed to parse markdown: %w", err) - } - - // 5. Extract Frontmatter - // We retrieve the metadata using the context after parsing - var meta templates.PageMetadata - d := frontmatter.Get(ctx) - if d != nil { - if err := d.Decode(&meta); err != nil { - // If decoding fails, we just proceed without metadata - fmt.Printf("Warning: failed to decode frontmatter for %s: %v\n", file.OriginalPath, err) + if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) { + return cachePath, nil } } - // If title is missing, try to grab the filename - if meta.Title == "" { - base := filepath.Base(file.OriginalPath) - meta.Title = strings.TrimSuffix(base, filepath.Ext(base)) + // 2. Regenerate Content + cacheMutex.Lock() + defer cacheMutex.Unlock() + + if !TestMode { + cacheInfo, errCache := os.Stat(cachePath) + origInfo, errOrig := os.Stat(file.OriginalPath) + if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) { + return cachePath, nil + } } - // 6. Special Handling for Index (Dynamic Components) - htmlContent := buf.String() + // Read Raw File + raw, err := ReadRaw(file.OriginalPath) + if err != nil { + return "", fmt.Errorf("failed to read file: %w", err) + } - if file.RoutePath == "/" { - // Generate the list of posts - var posts []templates.PostSnippet - for _, f := range AllContent { - if f.IsMarkdown && f.RoutePath != "/" { - name := filepath.Base(f.OriginalPath) - title := strings.TrimSuffix(name, filepath.Ext(name)) + var dataToWrite []byte - posts = append(posts, templates.PostSnippet{ - Title: title, - URL: f.RoutePath, - Date: f.ModTime, + if !file.IsMarkdown { + dataToWrite = raw + } else { + // Configure Goldmark with the custom tag extension + md := goldmark.New( + goldmark.WithExtensions( + extension.GFM, + &frontmatter.Extender{}, + templates.SidebarTag, // ||| -> + templates.TopBanner, + ), + goldmark.WithRendererOptions( + html.WithUnsafe(), + ), + ) + + var buf bytes.Buffer + ctx := parser.NewContext() + if err := md.Convert(raw, &buf, parser.WithContext(ctx)); err != nil { + return "", fmt.Errorf("failed to parse markdown: %w", err) + } + + // Extract Frontmatter + var meta templates.PageMetadata + d := frontmatter.Get(ctx) + if d != nil { + if err := d.Decode(&meta); err != nil { + fmt.Printf("Warning: failed to decode frontmatter for %s: %v\n", file.OriginalPath, err) + } + } + + if meta.Title == "" { + base := filepath.Base(file.OriginalPath) + meta.Title = strings.TrimSuffix(base, filepath.Ext(base)) + } + + htmlContent := buf.String() + + // 3. Post-Process for Index (Dynamic Content Injection) + if file.RoutePath == "/" { + // Generate posts data + var posts []templates.PostSnippet + for _, f := range AllContent { + if f.IsMarkdown && f.RoutePath != "/" { + name := filepath.Base(f.OriginalPath) + title := strings.TrimSuffix(name, filepath.Ext(name)) + + posts = append(posts, templates.PostSnippet{ + Title: title, + URL: f.RoutePath, + Date: f.ModTime, + }) + } + } + + /** + Add More Custom tags here. Add a customTag in tags.go as well, and the implementation in a template file. + */ + + // ||| SideBar + sidebarRegex := regexp.MustCompile(`(?s).*?`) + + if sidebarRegex.MatchString(htmlContent) { + // Render the dynamic content + latestPostsHTML := templates.RenderLatestPosts(posts) + dirLink := templates.RenderDirectoryLink() + + // Wrap in