mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
WIP: pressa generator in Ruby and Phlex
This commit is contained in:
parent
23e62f4a49
commit
b0588fed20
53 changed files with 2957 additions and 0 deletions
1
pressa/.ruby-version
Normal file
1
pressa/.ruby-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.4.1
|
||||
18
pressa/Gemfile
Normal file
18
pressa/Gemfile
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
ruby '~> 3.4.0'
|
||||
|
||||
gem 'phlex', '~> 2.3'
|
||||
gem 'kramdown', '~> 2.5'
|
||||
gem 'kramdown-parser-gfm', '~> 1.1'
|
||||
gem 'rouge', '~> 4.6'
|
||||
gem 'dry-struct', '~> 1.8'
|
||||
gem 'builder', '~> 3.3'
|
||||
gem 'bake', '~> 0.20'
|
||||
|
||||
group :development, :test do
|
||||
gem 'rspec', '~> 3.13'
|
||||
gem 'guard', '~> 2.18'
|
||||
gem 'guard-rspec', '~> 4.7'
|
||||
gem 'standard', '~> 1.43'
|
||||
end
|
||||
197
pressa/Gemfile.lock
Normal file
197
pressa/Gemfile.lock
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.3)
|
||||
bake (0.24.1)
|
||||
bigdecimal
|
||||
samovar (~> 2.1)
|
||||
bigdecimal (3.3.1)
|
||||
builder (3.3.0)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.5)
|
||||
console (1.34.2)
|
||||
fiber-annotation
|
||||
fiber-local (~> 1.1)
|
||||
json
|
||||
diff-lcs (1.6.2)
|
||||
dry-core (1.1.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
logger
|
||||
zeitwerk (~> 2.6)
|
||||
dry-inflector (1.2.0)
|
||||
dry-logic (1.6.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.1)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-struct (1.8.0)
|
||||
dry-core (~> 1.1)
|
||||
dry-types (~> 1.8, >= 1.8.2)
|
||||
ice_nine (~> 0.11)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-types (1.8.3)
|
||||
bigdecimal (~> 3.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-inflector (~> 1.0)
|
||||
dry-logic (~> 1.4)
|
||||
zeitwerk (~> 2.6)
|
||||
ffi (1.17.2)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-aarch64-linux-musl)
|
||||
ffi (1.17.2-arm-linux-gnu)
|
||||
ffi (1.17.2-arm-linux-musl)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86-linux-gnu)
|
||||
ffi (1.17.2-x86-linux-musl)
|
||||
ffi (1.17.2-x86_64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
ffi (1.17.2-x86_64-linux-musl)
|
||||
fiber-annotation (0.2.0)
|
||||
fiber-local (1.1.0)
|
||||
fiber-storage
|
||||
fiber-storage (1.0.1)
|
||||
formatador (1.2.3)
|
||||
reline
|
||||
guard (2.19.1)
|
||||
formatador (>= 0.2.4)
|
||||
listen (>= 2.7, < 4.0)
|
||||
logger (~> 1.6)
|
||||
lumberjack (>= 1.0.12, < 2.0)
|
||||
nenv (~> 0.1)
|
||||
notiffany (~> 0.0)
|
||||
ostruct (~> 0.6)
|
||||
pry (>= 0.13.0)
|
||||
shellany (~> 0.0)
|
||||
thor (>= 0.18.1)
|
||||
guard-compat (1.2.1)
|
||||
guard-rspec (4.7.3)
|
||||
guard (~> 2.1)
|
||||
guard-compat (~> 1.1)
|
||||
rspec (>= 2.99.0, < 4.0)
|
||||
ice_nine (0.11.2)
|
||||
io-console (0.8.1)
|
||||
json (2.16.0)
|
||||
kramdown (2.5.1)
|
||||
rexml (>= 3.3.9)
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
logger (1.7.0)
|
||||
lumberjack (1.4.2)
|
||||
mapping (1.1.3)
|
||||
method_source (1.1.0)
|
||||
nenv (0.3.0)
|
||||
notiffany (0.1.3)
|
||||
nenv (~> 0.1)
|
||||
shellany (~> 0.0)
|
||||
ostruct (0.6.3)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
phlex (2.3.1)
|
||||
zeitwerk (~> 2.7)
|
||||
prism (1.6.0)
|
||||
pry (0.15.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
racc (1.8.1)
|
||||
rainbow (3.1.1)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.4.4)
|
||||
rouge (4.6.1)
|
||||
rspec (3.13.2)
|
||||
rspec-core (~> 3.13.0)
|
||||
rspec-expectations (~> 3.13.0)
|
||||
rspec-mocks (~> 3.13.0)
|
||||
rspec-core (3.13.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.7)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-support (3.13.6)
|
||||
rubocop (1.80.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.46.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.48.0)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.25.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
samovar (2.4.1)
|
||||
console (~> 1.0)
|
||||
mapping (~> 1.0)
|
||||
shellany (0.0.1)
|
||||
standard (1.51.1)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.0)
|
||||
rubocop (~> 1.80.2)
|
||||
standard-custom (~> 1.0.0)
|
||||
standard-performance (~> 1.8)
|
||||
standard-custom (1.0.2)
|
||||
lint_roller (~> 1.0)
|
||||
rubocop (~> 1.50)
|
||||
standard-performance (1.8.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop-performance (~> 1.25.0)
|
||||
thor (1.4.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux-gnu
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86-linux-gnu
|
||||
x86-linux-musl
|
||||
x86_64-darwin
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
bake (~> 0.20)
|
||||
builder (~> 3.3)
|
||||
dry-struct (~> 1.8)
|
||||
guard (~> 2.18)
|
||||
guard-rspec (~> 4.7)
|
||||
kramdown (~> 2.5)
|
||||
kramdown-parser-gfm (~> 1.1)
|
||||
phlex (~> 2.3)
|
||||
rouge (~> 4.6)
|
||||
rspec (~> 3.13)
|
||||
standard (~> 1.43)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.1p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.2
|
||||
191
pressa/README.md
Normal file
191
pressa/README.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Pressa
|
||||
|
||||
A Ruby-based static site generator using Phlex for HTML generation. Built to replace the Swift-based generator for samhuri.net.
|
||||
|
||||
## Features
|
||||
|
||||
- **Plugin-based architecture** - Extensible system with PostsPlugin and ProjectsPlugin
|
||||
- **Hierarchical post organization** - Posts organized by year/month
|
||||
- **Markdown processing** - Kramdown with Rouge for syntax highlighting
|
||||
- **Multiple output formats** - Individual posts, homepage, archives, year/month indexes
|
||||
- **RSS & JSON feeds** - Both feeds with 30 most recent posts
|
||||
- **Link posts** - Support for posts linking to external URLs
|
||||
- **Phlex templates** - Type-safe HTML generation
|
||||
- **dry-struct models** - Immutable data structures
|
||||
|
||||
## Requirements
|
||||
|
||||
- Ruby 3.4.1+
|
||||
- Bundler
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bundle install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Development build (localhost:8000)
|
||||
bundle exec bake debug
|
||||
|
||||
# Start local server
|
||||
bundle exec bake serve
|
||||
|
||||
# Build for staging
|
||||
bundle exec bake beta
|
||||
bundle exec bake publish_beta # build + deploy
|
||||
|
||||
# Build for production
|
||||
bundle exec bake release
|
||||
bundle exec bake publish # build + deploy
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all specs
|
||||
bundle exec bake test
|
||||
|
||||
# Run specs with Guard (auto-run on file changes)
|
||||
bundle exec bake guard
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
# Check code style
|
||||
bundle exec bake lint
|
||||
|
||||
# Auto-fix style issues
|
||||
bundle exec bake lint_fix
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Build site
|
||||
bin/pressa SOURCE TARGET [URL]
|
||||
|
||||
# Example
|
||||
bin/pressa . www https://samhuri.net
|
||||
```
|
||||
|
||||
### Migration Tools
|
||||
|
||||
```bash
|
||||
# Convert front-matter from custom format to YAML
|
||||
bin/convert-frontmatter posts/**/*.md
|
||||
|
||||
# Validate output comparison between Swift and Ruby
|
||||
bin/validate-output www-swift www-ruby
|
||||
```
|
||||
|
||||
See [SYNTAX_HIGHLIGHTING.md](SYNTAX_HIGHLIGHTING.md) for details on Rouge syntax highlighting.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
pressa/
|
||||
├── lib/
|
||||
│ ├── pressa.rb # Main entry point
|
||||
│ ├── site_generator.rb # Orchestrator
|
||||
│ ├── site.rb # Site model (dry-struct)
|
||||
│ ├── plugin.rb # Plugin base class
|
||||
│ ├── posts/ # PostsPlugin
|
||||
│ │ ├── plugin.rb
|
||||
│ │ ├── repo.rb # Read/parse posts
|
||||
│ │ ├── writer.rb # Write HTML
|
||||
│ │ ├── metadata.rb # YAML front-matter parsing
|
||||
│ │ ├── models.rb # Post, PostsByYear, etc.
|
||||
│ │ ├── json_feed.rb
|
||||
│ │ └── rss_feed.rb
|
||||
│ ├── projects/ # ProjectsPlugin
|
||||
│ │ ├── plugin.rb
|
||||
│ │ └── models.rb
|
||||
│ ├── views/ # Phlex templates
|
||||
│ │ ├── layout.rb # Base layout
|
||||
│ │ ├── post_view.rb
|
||||
│ │ ├── recent_posts_view.rb
|
||||
│ │ ├── archive_view.rb
|
||||
│ │ └── ...
|
||||
│ └── utils/
|
||||
│ ├── file_writer.rb
|
||||
│ └── markdown_renderer.rb
|
||||
├── bin/
|
||||
│ └── pressa # CLI executable
|
||||
├── spec/ # RSpec tests
|
||||
└── bake.rb # Build tasks
|
||||
```
|
||||
|
||||
## Content Structure
|
||||
|
||||
### Posts
|
||||
|
||||
Posts must be in `/posts/YYYY/MM/` with YAML front-matter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
Title: Post Title
|
||||
Author: Author Name
|
||||
Date: 11th November, 2025
|
||||
Timestamp: 2025-11-11T14:00:00-08:00
|
||||
Tags: Ruby, Phlex # Optional
|
||||
Link: https://example.net # Optional (for link posts)
|
||||
Scripts: highlight.js # Optional
|
||||
Styles: code.css # Optional
|
||||
---
|
||||
|
||||
Post content in Markdown...
|
||||
```
|
||||
|
||||
### Output Structure
|
||||
|
||||
```
|
||||
www/
|
||||
├── index.html # Recent posts (10 most recent)
|
||||
├── posts/
|
||||
│ ├── index.html # Archive page
|
||||
│ ├── YYYY/
|
||||
│ │ ├── index.html # Year index
|
||||
│ │ └── MM/
|
||||
│ │ ├── index.html # Month rollup
|
||||
│ │ └── slug/
|
||||
│ │ └── index.html # Individual post
|
||||
├── projects/
|
||||
│ ├── index.html
|
||||
│ └── project-name/
|
||||
│ └── index.html
|
||||
├── feed.json # JSON Feed 1.1
|
||||
├── feed.xml # RSS 2.0
|
||||
└── [static files from public/]
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Ruby**: 3.4.1
|
||||
- **Phlex**: 2.3 - HTML generation
|
||||
- **Kramdown**: 2.5 - Markdown parsing
|
||||
- **kramdown-parser-gfm**: 1.1 - GitHub Flavored Markdown
|
||||
- **Rouge**: 4.6 - Syntax highlighting
|
||||
- **dry-struct**: 1.8 - Immutable data models
|
||||
- **Builder**: 3.3 - XML/RSS generation
|
||||
- **Bake**: 0.20+ - Task runner
|
||||
- **RSpec**: 3.13 - Testing
|
||||
- **StandardRB**: 1.43 - Code linting
|
||||
|
||||
## Differences from Swift Version
|
||||
|
||||
1. **Language**: Ruby 3.4 vs Swift 6.1
|
||||
2. **HTML generation**: Phlex vs Plot
|
||||
3. **Markdown**: Kramdown+Rouge vs Ink
|
||||
4. **Models**: dry-struct vs Swift structs
|
||||
5. **Build system**: Bake vs Make
|
||||
6. **Front-matter**: YAML vs custom format
|
||||
|
||||
## License
|
||||
|
||||
Personal project - not currently licensed for general use.
|
||||
141
pressa/SYNTAX_HIGHLIGHTING.md
Normal file
141
pressa/SYNTAX_HIGHLIGHTING.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# Syntax Highlighting with Rouge
|
||||
|
||||
Pressa uses Rouge for syntax highlighting through Kramdown. Your posts should already work correctly with Rouge if they use standard Markdown code fences.
|
||||
|
||||
## Supported Formats
|
||||
|
||||
### GitHub Flavored Markdown (Recommended)
|
||||
|
||||
````markdown
|
||||
```ruby
|
||||
def hello
|
||||
puts "Hello, World!"
|
||||
end
|
||||
```
|
||||
````
|
||||
|
||||
### Kramdown Syntax
|
||||
|
||||
````markdown
|
||||
~~~ ruby
|
||||
def hello
|
||||
puts "Hello, World!"
|
||||
end
|
||||
~~~
|
||||
````
|
||||
|
||||
### With Line Numbers (if needed)
|
||||
|
||||
You can enable line numbers in the Kramdown configuration, but by default Pressa has them disabled for cleaner output.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
Rouge supports 200+ languages. Common ones include:
|
||||
|
||||
- `ruby`
|
||||
- `javascript` / `js`
|
||||
- `python` / `py`
|
||||
- `swift`
|
||||
- `bash` / `shell`
|
||||
- `html`
|
||||
- `css`
|
||||
- `sql`
|
||||
- `yaml` / `yml`
|
||||
- `json`
|
||||
- `markdown` / `md`
|
||||
|
||||
Full list: https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers
|
||||
|
||||
## CSS Styling
|
||||
|
||||
Rouge generates syntax highlighting by wrapping code elements with `<span>` tags that have semantic class names.
|
||||
|
||||
Example output:
|
||||
```html
|
||||
<div class="language-ruby highlighter-rouge">
|
||||
<span class="k">class</span> <span class="nc">Post</span>
|
||||
<span class="k">end</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### CSS Classes
|
||||
|
||||
Common classes used by Rouge:
|
||||
|
||||
- `.k` - Keyword
|
||||
- `.nc` - Class name
|
||||
- `.nf` - Function name
|
||||
- `.s`, `.s1`, `.s2` - Strings
|
||||
- `.c`, `.c1` - Comments
|
||||
- `.n` - Name/identifier
|
||||
- `.o` - Operator
|
||||
- `.p` - Punctuation
|
||||
|
||||
### Generating CSS
|
||||
|
||||
You can generate Rouge CSS themes with:
|
||||
|
||||
```bash
|
||||
# List available themes
|
||||
bundle exec rougify help style
|
||||
|
||||
# Generate CSS for a theme
|
||||
bundle exec rougify style github > public/css/syntax.css
|
||||
bundle exec rougify style monokai > public/css/syntax-dark.css
|
||||
```
|
||||
|
||||
Popular themes:
|
||||
- `github` - GitHub's light theme
|
||||
- `monokai` - Dark theme
|
||||
- `base16` - Base16 color scheme
|
||||
- `thankful_eyes` - Easy on the eyes
|
||||
- `tulip` - Colorful
|
||||
|
||||
## Checking Your Posts
|
||||
|
||||
Your existing posts should work fine if they use:
|
||||
1. Standard Markdown code fences with language specifiers
|
||||
2. HTML `<pre><code>` blocks (will work but won't be highlighted)
|
||||
|
||||
To check a specific post:
|
||||
|
||||
```bash
|
||||
grep -A 5 '```' posts/2025/11/your-post.md
|
||||
```
|
||||
|
||||
## Configuration in Pressa
|
||||
|
||||
Syntax highlighting is configured in `lib/posts/repo.rb` and `lib/utils/markdown_renderer.rb`:
|
||||
|
||||
```ruby
|
||||
Kramdown::Document.new(
|
||||
markdown,
|
||||
input: 'GFM',
|
||||
syntax_highlighter: 'rouge',
|
||||
syntax_highlighter_opts: {
|
||||
line_numbers: false, # Change to true if you want line numbers
|
||||
wrap: false # Change to true for <div> wrapping
|
||||
}
|
||||
).to_html
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
You can test syntax highlighting with the provided test post:
|
||||
|
||||
```bash
|
||||
bundle exec bake debug
|
||||
bundle exec bake serve
|
||||
# Open http://localhost:8000 in browser
|
||||
```
|
||||
|
||||
The test post at `test-site/posts/2025/11/test-post.md` includes a Ruby code example with syntax highlighting.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
If you're migrating from Swift/Ink, both use similar Markdown parsers, so your code blocks should "just work." The main difference is:
|
||||
|
||||
- **Ink**: Built-in syntax highlighting (uses its own system)
|
||||
- **Rouge**: External gem, more themes, more languages, generates semantic HTML
|
||||
|
||||
Rouge output is more flexible because it generates plain HTML with classes, allowing you to change themes by just swapping CSS files.
|
||||
92
pressa/bake.rb
Normal file
92
pressa/bake.rb
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Build tasks for Pressa static site generator
|
||||
|
||||
# Generate the site in debug mode (localhost:8000)
|
||||
def debug
|
||||
build('http://localhost:8000')
|
||||
end
|
||||
|
||||
# Generate the site for the mudge development server
|
||||
def mudge
|
||||
build('http://mudge:8000')
|
||||
end
|
||||
|
||||
# Generate the site for beta/staging
|
||||
def beta
|
||||
build('https://beta.samhuri.net')
|
||||
end
|
||||
|
||||
# Generate the site for production
|
||||
def release
|
||||
build('https://samhuri.net')
|
||||
end
|
||||
|
||||
# Start local development server
|
||||
def serve
|
||||
require 'webrick'
|
||||
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: 'www')
|
||||
trap('INT') { server.shutdown }
|
||||
puts "Server running at http://localhost:8000"
|
||||
server.start
|
||||
end
|
||||
|
||||
# Publish to beta/staging server
|
||||
def publish_beta
|
||||
beta
|
||||
puts "Deploying to beta server..."
|
||||
system('rsync -avz --delete www/ beta.samhuri.net:/var/www/beta/')
|
||||
end
|
||||
|
||||
# Publish to production server
|
||||
def publish
|
||||
release
|
||||
puts "Deploying to production server..."
|
||||
system('rsync -avz --delete www/ samhuri.net:/var/www/html/')
|
||||
end
|
||||
|
||||
# Clean generated files
|
||||
def clean
|
||||
require 'fileutils'
|
||||
FileUtils.rm_rf('www')
|
||||
puts "Cleaned www/ directory"
|
||||
end
|
||||
|
||||
# Run RSpec tests
|
||||
def test
|
||||
exec 'bundle exec rspec'
|
||||
end
|
||||
|
||||
# Run Guard for continuous testing
|
||||
def guard
|
||||
exec 'bundle exec guard'
|
||||
end
|
||||
|
||||
# List all available drafts
|
||||
def drafts
|
||||
Dir.glob('drafts/*.md').sort.each do |draft|
|
||||
puts File.basename(draft)
|
||||
end
|
||||
end
|
||||
|
||||
# Run StandardRB linter
|
||||
def lint
|
||||
exec 'bundle exec standardrb'
|
||||
end
|
||||
|
||||
# Auto-fix StandardRB issues
|
||||
def lint_fix
|
||||
exec 'bundle exec standardrb --fix'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Build the site with specified URL
|
||||
# @parameter url [String] The site URL to use
|
||||
def build(url)
|
||||
require_relative 'lib/pressa'
|
||||
|
||||
puts "Building site for #{url}..."
|
||||
site = Pressa.create_site(url_override: url)
|
||||
generator = Pressa::SiteGenerator.new(site:)
|
||||
generator.generate(source_path: '.', target_path: 'www')
|
||||
puts "Site built successfully in www/"
|
||||
end
|
||||
38
pressa/bin/convert-frontmatter
Executable file
38
pressa/bin/convert-frontmatter
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative '../lib/utils/frontmatter_converter'
|
||||
|
||||
if ARGV.empty?
|
||||
puts "Usage: convert-frontmatter FILE [FILE...]"
|
||||
puts ""
|
||||
puts "Converts front-matter from custom format to YAML in-place."
|
||||
puts ""
|
||||
puts "Examples:"
|
||||
puts " convert-frontmatter posts/2025/11/*.md"
|
||||
puts " convert-frontmatter posts/**/*.md"
|
||||
exit 1
|
||||
end
|
||||
|
||||
converted_count = 0
|
||||
error_count = 0
|
||||
|
||||
ARGV.each do |file_path|
|
||||
unless File.exist?(file_path)
|
||||
puts "ERROR: File not found: #{file_path}"
|
||||
error_count += 1
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
Pressa::Utils::FrontmatterConverter.convert_file(file_path)
|
||||
puts "✓ #{file_path}"
|
||||
converted_count += 1
|
||||
rescue => e
|
||||
puts "ERROR: #{file_path}: #{e.message}"
|
||||
error_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
puts ""
|
||||
puts "Converted: #{converted_count}"
|
||||
puts "Errors: #{error_count}" if error_count > 0
|
||||
28
pressa/bin/pressa
Executable file
28
pressa/bin/pressa
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative '../lib/pressa'
|
||||
|
||||
if ARGV.length < 2
|
||||
puts "Usage: pressa SOURCE TARGET [URL]"
|
||||
puts ""
|
||||
puts "Arguments:"
|
||||
puts " SOURCE Directory containing posts/ and public/"
|
||||
puts " TARGET Directory to write generated site"
|
||||
puts " URL Optional site URL override"
|
||||
exit 1
|
||||
end
|
||||
|
||||
source_path = ARGV[0]
|
||||
target_path = ARGV[1]
|
||||
site_url = ARGV[2]
|
||||
|
||||
begin
|
||||
site = Pressa.create_site(url_override: site_url)
|
||||
generator = Pressa::SiteGenerator.new(site:)
|
||||
generator.generate(source_path:, target_path:)
|
||||
puts "Site generated successfully!"
|
||||
rescue => e
|
||||
puts "Error: #{e.message}"
|
||||
puts e.backtrace
|
||||
exit 1
|
||||
end
|
||||
151
pressa/bin/validate-output
Executable file
151
pressa/bin/validate-output
Executable file
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require 'fileutils'
|
||||
require 'digest'
|
||||
|
||||
class OutputValidator
|
||||
def initialize(swift_dir:, ruby_dir:)
|
||||
@swift_dir = swift_dir
|
||||
@ruby_dir = ruby_dir
|
||||
@differences = []
|
||||
@missing_in_ruby = []
|
||||
@missing_in_swift = []
|
||||
@identical_count = 0
|
||||
end
|
||||
|
||||
def validate
|
||||
puts "Comparing outputs:"
|
||||
puts " Swift: #{@swift_dir}"
|
||||
puts " Ruby: #{@ruby_dir}"
|
||||
puts ""
|
||||
|
||||
swift_files = find_html_files(@swift_dir)
|
||||
ruby_files = find_html_files(@ruby_dir)
|
||||
|
||||
puts "Found #{swift_files.length} Swift HTML files"
|
||||
puts "Found #{ruby_files.length} Ruby HTML files"
|
||||
puts ""
|
||||
|
||||
compare_files(swift_files, ruby_files)
|
||||
print_summary
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_html_files(dir)
|
||||
Dir.glob(File.join(dir, '**', '*.{html,xml,json}'))
|
||||
.map { |f| f.sub("#{dir}/", '') }
|
||||
.sort
|
||||
end
|
||||
|
||||
def compare_files(swift_files, ruby_files)
|
||||
all_files = (swift_files + ruby_files).uniq.sort
|
||||
|
||||
all_files.each do |relative_path|
|
||||
swift_path = File.join(@swift_dir, relative_path)
|
||||
ruby_path = File.join(@ruby_dir, relative_path)
|
||||
|
||||
if !File.exist?(swift_path)
|
||||
@missing_in_swift << relative_path
|
||||
elsif !File.exist?(ruby_path)
|
||||
@missing_in_ruby << relative_path
|
||||
else
|
||||
compare_file_contents(relative_path, swift_path, ruby_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def compare_file_contents(relative_path, swift_path, ruby_path)
|
||||
swift_content = normalize_html(File.read(swift_path))
|
||||
ruby_content = normalize_html(File.read(ruby_path))
|
||||
|
||||
if swift_content == ruby_content
|
||||
@identical_count += 1
|
||||
puts "✓ #{relative_path}"
|
||||
else
|
||||
@differences << {
|
||||
path: relative_path,
|
||||
swift_hash: Digest::SHA256.hexdigest(swift_content),
|
||||
ruby_hash: Digest::SHA256.hexdigest(ruby_content),
|
||||
swift_size: swift_content.length,
|
||||
ruby_size: ruby_content.length
|
||||
}
|
||||
puts "✗ #{relative_path} (differs)"
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_html(content)
|
||||
content
|
||||
.gsub(/\s+/, ' ')
|
||||
.gsub(/>\s+</, '><')
|
||||
.gsub(/<lastBuildDate>.*?<\/lastBuildDate>/, '')
|
||||
.strip
|
||||
end
|
||||
|
||||
def print_summary
|
||||
puts ""
|
||||
puts "=" * 60
|
||||
puts "VALIDATION SUMMARY"
|
||||
puts "=" * 60
|
||||
puts ""
|
||||
puts "Identical files: #{@identical_count}"
|
||||
puts "Different files: #{@differences.length}"
|
||||
puts "Missing in Ruby: #{@missing_in_ruby.length}"
|
||||
puts "Missing in Swift: #{@missing_in_swift.length}"
|
||||
puts ""
|
||||
|
||||
if @differences.any?
|
||||
puts "Files with differences:"
|
||||
@differences.each do |diff|
|
||||
puts " #{diff[:path]}"
|
||||
puts " Swift: #{diff[:swift_size]} bytes (#{diff[:swift_hash][0..7]}...)"
|
||||
puts " Ruby: #{diff[:ruby_size]} bytes (#{diff[:ruby_hash][0..7]}...)"
|
||||
end
|
||||
puts ""
|
||||
end
|
||||
|
||||
if @missing_in_ruby.any?
|
||||
puts "Missing in Ruby output:"
|
||||
@missing_in_ruby.each { |path| puts " #{path}" }
|
||||
puts ""
|
||||
end
|
||||
|
||||
if @missing_in_swift.any?
|
||||
puts "Missing in Swift output:"
|
||||
@missing_in_swift.each { |path| puts " #{path}" }
|
||||
puts ""
|
||||
end
|
||||
|
||||
success = @differences.empty? && @missing_in_ruby.empty? && @missing_in_swift.empty?
|
||||
puts success ? "✅ VALIDATION PASSED" : "❌ VALIDATION FAILED"
|
||||
puts ""
|
||||
|
||||
exit(success ? 0 : 1)
|
||||
end
|
||||
end
|
||||
|
||||
if ARGV.length != 2
|
||||
puts "Usage: validate-output SWIFT_DIR RUBY_DIR"
|
||||
puts ""
|
||||
puts "Compares HTML/XML/JSON output from Swift and Ruby generators."
|
||||
puts ""
|
||||
puts "Example:"
|
||||
puts " validate-output www-swift www-ruby"
|
||||
exit 1
|
||||
end
|
||||
|
||||
swift_dir = ARGV[0]
|
||||
ruby_dir = ARGV[1]
|
||||
|
||||
unless Dir.exist?(swift_dir)
|
||||
puts "ERROR: Swift directory not found: #{swift_dir}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
unless Dir.exist?(ruby_dir)
|
||||
puts "ERROR: Ruby directory not found: #{ruby_dir}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
validator = OutputValidator.new(swift_dir: swift_dir, ruby_dir: ruby_dir)
|
||||
validator.validate
|
||||
11
pressa/lib/plugin.rb
Normal file
11
pressa/lib/plugin.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module Pressa
|
||||
class Plugin
|
||||
def setup(site:, source_path:)
|
||||
raise NotImplementedError, "#{self.class}#setup must be implemented"
|
||||
end
|
||||
|
||||
def render(site:, target_path:)
|
||||
raise NotImplementedError, "#{self.class}#render must be implemented"
|
||||
end
|
||||
end
|
||||
end
|
||||
60
pressa/lib/posts/json_feed.rb
Normal file
60
pressa/lib/posts/json_feed.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
require 'json'
|
||||
require_relative '../utils/file_writer'
|
||||
|
||||
module Pressa
|
||||
module Posts
|
||||
class JSONFeedWriter
|
||||
FEED_VERSION = "https://jsonfeed.org/version/1.1"
|
||||
|
||||
def initialize(site:, posts_by_year:)
|
||||
@site = site
|
||||
@posts_by_year = posts_by_year
|
||||
end
|
||||
|
||||
def write_feed(target_path:, limit: 30)
|
||||
recent = @posts_by_year.recent_posts(limit)
|
||||
|
||||
feed = {
|
||||
version: FEED_VERSION,
|
||||
title: @site.title,
|
||||
home_page_url: @site.url,
|
||||
feed_url: @site.url_for('/feed.json'),
|
||||
description: @site.description,
|
||||
authors: [
|
||||
{
|
||||
name: @site.author,
|
||||
url: @site.url
|
||||
}
|
||||
],
|
||||
items: recent.map { |post| feed_item(post) }
|
||||
}
|
||||
|
||||
json = JSON.pretty_generate(feed)
|
||||
file_path = File.join(target_path, 'feed.json')
|
||||
Utils::FileWriter.write(path: file_path, content: json)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def feed_item(post)
|
||||
item = {
|
||||
id: @site.url_for(post.path),
|
||||
url: post.link_post? ? post.link : @site.url_for(post.path),
|
||||
title: post.link_post? ? "→ #{post.title}" : post.title,
|
||||
content_html: post.body,
|
||||
summary: post.excerpt,
|
||||
date_published: post.date.iso8601,
|
||||
authors: [
|
||||
{
|
||||
name: post.author
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
item[:tags] = post.tags unless post.tags.empty?
|
||||
|
||||
item
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
62
pressa/lib/posts/metadata.rb
Normal file
62
pressa/lib/posts/metadata.rb
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
require 'yaml'
|
||||
require 'date'
|
||||
|
||||
module Pressa
|
||||
module Posts
|
||||
class PostMetadata
|
||||
REQUIRED_FIELDS = %w[Title Author Date Timestamp].freeze
|
||||
|
||||
attr_reader :title, :author, :date, :formatted_date, :link, :tags, :scripts, :styles
|
||||
|
||||
def initialize(yaml_hash)
|
||||
@raw = yaml_hash
|
||||
validate_required_fields!
|
||||
parse_fields
|
||||
end
|
||||
|
||||
def self.parse(content)
|
||||
if content =~ /\A---\s*\n(.*?)\n---\s*\n/m
|
||||
yaml_content = $1
|
||||
yaml_hash = YAML.safe_load(yaml_content, permitted_classes: [Date, Time])
|
||||
new(yaml_hash)
|
||||
else
|
||||
raise "No YAML front-matter found in post"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_required_fields!
|
||||
missing = REQUIRED_FIELDS.reject { |field| @raw.key?(field) }
|
||||
raise "Missing required fields: #{missing.join(', ')}" unless missing.empty?
|
||||
end
|
||||
|
||||
def parse_fields
|
||||
@title = @raw['Title']
|
||||
@author = @raw['Author']
|
||||
timestamp = @raw['Timestamp']
|
||||
@date = timestamp.is_a?(String) ? DateTime.parse(timestamp) : timestamp.to_datetime
|
||||
@formatted_date = @raw['Date']
|
||||
@link = @raw['Link']
|
||||
@tags = parse_comma_separated(@raw['Tags'])
|
||||
@scripts = parse_scripts(@raw['Scripts'])
|
||||
@styles = parse_styles(@raw['Styles'])
|
||||
end
|
||||
|
||||
def parse_comma_separated(value)
|
||||
return [] if value.nil? || value.empty?
|
||||
value.split(',').map(&:strip)
|
||||
end
|
||||
|
||||
def parse_scripts(value)
|
||||
return [] if value.nil?
|
||||
parse_comma_separated(value).map { |src| Script.new(src:, defer: true) }
|
||||
end
|
||||
|
||||
def parse_styles(value)
|
||||
return [] if value.nil?
|
||||
parse_comma_separated(value).map { |href| Stylesheet.new(href:) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
93
pressa/lib/posts/models.rb
Normal file
93
pressa/lib/posts/models.rb
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
require 'dry-struct'
|
||||
require_relative '../site'
|
||||
|
||||
module Pressa
|
||||
module Posts
|
||||
class Post < Dry::Struct
|
||||
attribute :slug, Types::String
|
||||
attribute :title, Types::String
|
||||
attribute :author, Types::String
|
||||
attribute :date, Types::Params::DateTime
|
||||
attribute :formatted_date, Types::String
|
||||
attribute :link, Types::String.optional.default(nil)
|
||||
attribute :tags, Types::Array.of(Types::String).default([].freeze)
|
||||
attribute :scripts, Types::Array.of(Script).default([].freeze)
|
||||
attribute :styles, Types::Array.of(Stylesheet).default([].freeze)
|
||||
attribute :body, Types::String
|
||||
attribute :excerpt, Types::String
|
||||
attribute :path, Types::String
|
||||
|
||||
def link_post?
|
||||
!link.nil?
|
||||
end
|
||||
|
||||
def year
|
||||
date.year
|
||||
end
|
||||
|
||||
def month
|
||||
date.month
|
||||
end
|
||||
|
||||
def formatted_month
|
||||
date.strftime('%B')
|
||||
end
|
||||
|
||||
def padded_month
|
||||
format('%02d', month)
|
||||
end
|
||||
end
|
||||
|
||||
class Month < Dry::Struct
|
||||
attribute :name, Types::String
|
||||
attribute :number, Types::Integer
|
||||
attribute :padded, Types::String
|
||||
|
||||
def self.from_date(date)
|
||||
new(
|
||||
name: date.strftime('%B'),
|
||||
number: date.month,
|
||||
padded: format('%02d', date.month)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
class MonthPosts < Dry::Struct
|
||||
attribute :month, Month
|
||||
attribute :posts, Types::Array.of(Post)
|
||||
|
||||
def sorted_posts
|
||||
posts.sort_by(&:date).reverse
|
||||
end
|
||||
end
|
||||
|
||||
class YearPosts < Dry::Struct
|
||||
attribute :year, Types::Integer
|
||||
attribute :by_month, Types::Hash.map(Types::Integer, MonthPosts)
|
||||
|
||||
def sorted_months
|
||||
by_month.keys.sort.reverse.map { |month_num| by_month[month_num] }
|
||||
end
|
||||
|
||||
def all_posts
|
||||
by_month.values.flat_map(&:posts).sort_by(&:date).reverse
|
||||
end
|
||||
end
|
||||
|
||||
class PostsByYear < Dry::Struct
|
||||
attribute :by_year, Types::Hash.map(Types::Integer, YearPosts)
|
||||
|
||||
def sorted_years
|
||||
by_year.keys.sort.reverse
|
||||
end
|
||||
|
||||
def all_posts
|
||||
by_year.values.flat_map(&:all_posts).sort_by(&:date).reverse
|
||||
end
|
||||
|
||||
def recent_posts(limit = 10)
|
||||
all_posts.take(limit)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
pressa/lib/posts/plugin.rb
Normal file
38
pressa/lib/posts/plugin.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
require_relative '../plugin'
|
||||
require_relative 'repo'
|
||||
require_relative 'writer'
|
||||
require_relative 'json_feed'
|
||||
require_relative 'rss_feed'
|
||||
|
||||
module Pressa
|
||||
module Posts
|
||||
class Plugin < Pressa::Plugin
|
||||
attr_reader :posts_by_year
|
||||
|
||||
def setup(site:, source_path:)
|
||||
posts_dir = File.join(source_path, 'posts')
|
||||
return unless Dir.exist?(posts_dir)
|
||||
|
||||
repo = PostRepo.new
|
||||
@posts_by_year = repo.read_posts(posts_dir)
|
||||
end
|
||||
|
||||
def render(site:, target_path:)
|
||||
return unless @posts_by_year
|
||||
|
||||
writer = PostWriter.new(site:, posts_by_year: @posts_by_year)
|
||||
writer.write_posts(target_path:)
|
||||
writer.write_recent_posts(target_path:, limit: 10)
|
||||
writer.write_archive(target_path:)
|
||||
writer.write_year_indexes(target_path:)
|
||||
writer.write_month_rollups(target_path:)
|
||||
|
||||
json_feed = JSONFeedWriter.new(site:, posts_by_year: @posts_by_year)
|
||||
json_feed.write_feed(target_path:, limit: 30)
|
||||
|
||||
rss_feed = RSSFeedWriter.new(site:, posts_by_year: @posts_by_year)
|
||||
rss_feed.write_feed(target_path:, limit: 30)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
123
pressa/lib/posts/repo.rb
Normal file
123
pressa/lib/posts/repo.rb
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
require 'kramdown'
|
||||
require_relative 'models'
|
||||
require_relative 'metadata'
|
||||
|
||||
module Pressa
|
||||
module Posts
|
||||
class PostRepo
|
||||
EXCERPT_LENGTH = 300
|
||||
|
||||
def initialize(output_path: 'posts')
|
||||
@output_path = output_path
|
||||
@posts_by_year = {}
|
||||
end
|
||||
|
||||
def read_posts(posts_dir)
|
||||
enumerate_markdown_files(posts_dir) do |file_path|
|
||||
post = read_post(file_path)
|
||||
add_post_to_hierarchy(post)
|
||||
end
|
||||
|
||||
PostsByYear.new(by_year: @posts_by_year)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def enumerate_markdown_files(dir, &block)
|
||||
Dir.glob(File.join(dir, '**', '*.md')).each(&block)
|
||||
end
|
||||
|
||||
def read_post(file_path)
|
||||
content = File.read(file_path)
|
||||
metadata = PostMetadata.parse(content)
|
||||
|
||||
body_markdown = content.sub(/\A---\s*\n.*?\n---\s*\n/m, '')
|
||||
|
||||
html_body = render_markdown(body_markdown)
|
||||
|
||||
slug = File.basename(file_path, '.md')
|
||||
path = generate_path(slug, metadata.date)
|
||||
excerpt = generate_excerpt(body_markdown)
|
||||
|
||||
Post.new(
|
||||
slug:,
|
||||
title: metadata.title,
|
||||
author: metadata.author,
|
||||
date: metadata.date,
|
||||
formatted_date: metadata.formatted_date,
|
||||
link: metadata.link,
|
||||
tags: metadata.tags,
|
||||
scripts: metadata.scripts,
|
||||
styles: metadata.styles,
|
||||
body: html_body,
|
||||
excerpt:,
|
||||
path:
|
||||
)
|
||||
end
|
||||
|
||||
def render_markdown(markdown)
|
||||
Kramdown::Document.new(
|
||||
markdown,
|
||||
input: 'GFM',
|
||||
syntax_highlighter: 'rouge',
|
||||
syntax_highlighter_opts: {
|
||||
line_numbers: false,
|
||||
wrap: false
|
||||
}
|
||||
).to_html
|
||||
end
|
||||
|
||||
def generate_path(slug, date)
|
||||
year = date.year
|
||||
month = format('%02d', date.month)
|
||||
"/#{@output_path}/#{year}/#{month}/#{slug}"
|
||||
end
|
||||
|
||||
def generate_excerpt(markdown)
|
||||
text = markdown.dup
|
||||
|
||||
text.gsub!(/!\[.*?\]\([^)]+\)/, '')
|
||||
|
||||
text.gsub!(/\[(.*?)\]\([^)]+\)/, '\1')
|
||||
|
||||
text.gsub!(/<[^>]+>/, '')
|
||||
|
||||
text.gsub!(/\s+/, ' ')
|
||||
text.strip!
|
||||
|
||||
if text.length > EXCERPT_LENGTH
|
||||
"#{text[0...EXCERPT_LENGTH]}..."
|
||||
else
|
||||
text
|
||||
end
|
||||
end
|
||||
|
||||
def add_post_to_hierarchy(post)
|
||||
year = post.year
|
||||
month_num = post.month
|
||||
|
||||
@posts_by_year[year] ||= create_year_posts(year)
|
||||
year_posts = @posts_by_year[year]
|
||||
|
||||
month_posts = year_posts.by_month[month_num]
|
||||
if month_posts
|
||||
updated_posts = month_posts.posts + [post]
|
||||
year_posts.by_month[month_num] = MonthPosts.new(
|
||||
month: month_posts.month,
|
||||
posts: updated_posts
|
||||
)
|
||||
else
|
||||
month = Month.from_date(post.date)
|
||||
year_posts.by_month[month_num] = MonthPosts.new(
|
||||
month:,
|
||||
posts: [post]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def create_year_posts(year)
|
||||
YearPosts.new(year:, by_month: {})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
51
pressa/lib/posts/rss_feed.rb
Normal file
51
pressa/lib/posts/rss_feed.rb
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
require 'builder'
|
||||
require_relative '../utils/file_writer'
|
||||
|
||||
module Pressa
|
||||
module Posts
|
||||
class RSSFeedWriter
|
||||
def initialize(site:, posts_by_year:)
|
||||
@site = site
|
||||
@posts_by_year = posts_by_year
|
||||
end
|
||||
|
||||
def write_feed(target_path:, limit: 30)
|
||||
recent = @posts_by_year.recent_posts(limit)
|
||||
|
||||
xml = Builder::XmlMarkup.new(indent: 2)
|
||||
xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
||||
|
||||
xml.rss version: "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
|
||||
xml.channel do
|
||||
xml.title @site.title
|
||||
xml.link @site.url
|
||||
xml.description @site.description
|
||||
xml.language "en-us"
|
||||
xml.pubDate recent.first.date.rfc822 if recent.any?
|
||||
xml.lastBuildDate Time.now.rfc822
|
||||
xml.tag! "atom:link", href: @site.url_for('/feed.xml'), rel: "self", type: "application/rss+xml"
|
||||
|
||||
recent.each do |post|
|
||||
xml.item do
|
||||
title = post.link_post? ? "→ #{post.title}" : post.title
|
||||
xml.title title
|
||||
xml.link post.link_post? ? post.link : @site.url_for(post.path)
|
||||
xml.guid @site.url_for(post.path), isPermaLink: "true"
|
||||
xml.description { xml.cdata! post.body }
|
||||
xml.pubDate post.date.rfc822
|
||||
xml.author "#{@site.email} (#{@site.author})"
|
||||
|
||||
post.tags.each do |tag|
|
||||
xml.category tag
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
file_path = File.join(target_path, 'feed.xml')
|
||||
Utils::FileWriter.write(path: file_path, content: xml.target!)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
128
pressa/lib/posts/writer.rb
Normal file
128
pressa/lib/posts/writer.rb
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
require_relative '../utils/file_writer'
|
||||
require_relative '../views/layout'
|
||||
require_relative '../views/post_view'
|
||||
require_relative '../views/recent_posts_view'
|
||||
require_relative '../views/archive_view'
|
||||
require_relative '../views/year_posts_view'
|
||||
require_relative '../views/month_posts_view'
|
||||
|
||||
module Pressa
|
||||
module Posts
|
||||
class PostWriter
|
||||
def initialize(site:, posts_by_year:)
|
||||
@site = site
|
||||
@posts_by_year = posts_by_year
|
||||
end
|
||||
|
||||
def write_posts(target_path:)
|
||||
@posts_by_year.all_posts.each do |post|
|
||||
write_post(post:, target_path:)
|
||||
end
|
||||
end
|
||||
|
||||
def write_recent_posts(target_path:, limit: 10)
|
||||
recent = @posts_by_year.recent_posts(limit)
|
||||
content_view = Views::RecentPostsView.new(posts: recent, site: @site)
|
||||
|
||||
html = render_layout(
|
||||
page_title: @site.title,
|
||||
canonical_url: @site.url,
|
||||
content: content_view
|
||||
)
|
||||
|
||||
file_path = File.join(target_path, 'index.html')
|
||||
Utils::FileWriter.write(path: file_path, content: html)
|
||||
end
|
||||
|
||||
def write_archive(target_path:)
|
||||
content_view = Views::ArchiveView.new(posts_by_year: @posts_by_year, site: @site)
|
||||
|
||||
html = render_layout(
|
||||
page_title: "Archive – #{@site.title}",
|
||||
canonical_url: @site.url_for('/posts/'),
|
||||
content: content_view
|
||||
)
|
||||
|
||||
file_path = File.join(target_path, 'posts', 'index.html')
|
||||
Utils::FileWriter.write(path: file_path, content: html)
|
||||
end
|
||||
|
||||
def write_year_indexes(target_path:)
|
||||
@posts_by_year.sorted_years.each do |year|
|
||||
year_posts = @posts_by_year.by_year[year]
|
||||
write_year_index(year:, year_posts:, target_path:)
|
||||
end
|
||||
end
|
||||
|
||||
def write_month_rollups(target_path:)
|
||||
@posts_by_year.by_year.each do |year, year_posts|
|
||||
year_posts.by_month.each do |_month_num, month_posts|
|
||||
write_month_rollup(year:, month_posts:, target_path:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write_post(post:, target_path:)
|
||||
content_view = Views::PostView.new(post:, site: @site)
|
||||
|
||||
all_scripts = @site.scripts + post.scripts
|
||||
all_styles = @site.styles + post.styles
|
||||
|
||||
html = render_layout(
|
||||
page_title: "#{post.title} – #{@site.title}",
|
||||
canonical_url: @site.url_for(post.path),
|
||||
content: content_view,
|
||||
page_scripts: post.scripts,
|
||||
page_styles: post.styles
|
||||
)
|
||||
|
||||
file_path = File.join(target_path, post.path.sub(/^\//, ''), 'index.html')
|
||||
Utils::FileWriter.write(path: file_path, content: html)
|
||||
end
|
||||
|
||||
def write_year_index(year:, year_posts:, target_path:)
|
||||
content_view = Views::YearPostsView.new(year:, year_posts:, site: @site)
|
||||
|
||||
html = render_layout(
|
||||
page_title: "#{year} – #{@site.title}",
|
||||
canonical_url: @site.url_for("/posts/#{year}/"),
|
||||
content: content_view
|
||||
)
|
||||
|
||||
file_path = File.join(target_path, 'posts', year.to_s, 'index.html')
|
||||
Utils::FileWriter.write(path: file_path, content: html)
|
||||
end
|
||||
|
||||
def write_month_rollup(year:, month_posts:, target_path:)
|
||||
month = month_posts.month
|
||||
content_view = Views::MonthPostsView.new(year:, month_posts:, site: @site)
|
||||
|
||||
title = "#{month.name} #{year}"
|
||||
html = render_layout(
|
||||
page_title: "#{title} – #{@site.title}",
|
||||
canonical_url: @site.url_for("/posts/#{year}/#{month.padded}/"),
|
||||
content: content_view
|
||||
)
|
||||
|
||||
file_path = File.join(target_path, 'posts', year.to_s, month.padded, 'index.html')
|
||||
Utils::FileWriter.write(path: file_path, content: html)
|
||||
end
|
||||
|
||||
def render_layout(page_title:, canonical_url:, content:, page_scripts: [], page_styles: [])
|
||||
layout = Views::Layout.new(
|
||||
site: @site,
|
||||
page_title:,
|
||||
canonical_url:,
|
||||
page_scripts:,
|
||||
page_styles:
|
||||
)
|
||||
|
||||
layout.call do
|
||||
content.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
59
pressa/lib/pressa.rb
Normal file
59
pressa/lib/pressa.rb
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
require_relative 'site'
|
||||
require_relative 'site_generator'
|
||||
require_relative 'plugin'
|
||||
require_relative 'posts/plugin'
|
||||
require_relative 'projects/plugin'
|
||||
require_relative 'utils/markdown_renderer'
|
||||
|
||||
module Pressa
|
||||
def self.create_site(url_override: nil)
|
||||
url = url_override || 'https://samhuri.net'
|
||||
|
||||
projects = [
|
||||
Projects::Project.new(
|
||||
name: 'bin',
|
||||
title: 'bin',
|
||||
description: 'my collection of scripts in ~/bin',
|
||||
url: 'https://github.com/samsonjs/bin'
|
||||
),
|
||||
Projects::Project.new(
|
||||
name: 'config',
|
||||
title: 'config',
|
||||
description: 'important dot files',
|
||||
url: 'https://github.com/samsonjs/config'
|
||||
),
|
||||
Projects::Project.new(
|
||||
name: 'unix-node',
|
||||
title: 'unix-node',
|
||||
description: 'Node.js CommonJS module that exports useful Unix commands',
|
||||
url: 'https://github.com/samsonjs/unix-node'
|
||||
),
|
||||
Projects::Project.new(
|
||||
name: 'strftime',
|
||||
title: 'strftime',
|
||||
description: 'strftime for JavaScript',
|
||||
url: 'https://github.com/samsonjs/strftime'
|
||||
)
|
||||
]
|
||||
|
||||
Site.new(
|
||||
author: 'Sami Samhuri',
|
||||
email: 'sami@samhuri.net',
|
||||
title: 'samhuri.net',
|
||||
description: 'The personal blog of Sami Samhuri',
|
||||
url:,
|
||||
image_url: "#{url}/images",
|
||||
scripts: [],
|
||||
styles: [
|
||||
Stylesheet.new(href: 'css/style.css')
|
||||
],
|
||||
plugins: [
|
||||
Posts::Plugin.new,
|
||||
Projects::Plugin.new(projects:)
|
||||
],
|
||||
renderers: [
|
||||
Utils::MarkdownRenderer.new
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
22
pressa/lib/projects/models.rb
Normal file
22
pressa/lib/projects/models.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
require 'dry-struct'
|
||||
require_relative '../site'
|
||||
|
||||
module Pressa
|
||||
module Projects
|
||||
class Project < Dry::Struct
|
||||
attribute :name, Types::String
|
||||
attribute :title, Types::String
|
||||
attribute :description, Types::String
|
||||
attribute :url, Types::String
|
||||
|
||||
def github_path
|
||||
uri = URI.parse(url)
|
||||
uri.path.sub(/^\//, '')
|
||||
end
|
||||
|
||||
def path
|
||||
"/projects/#{name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
69
pressa/lib/projects/plugin.rb
Normal file
69
pressa/lib/projects/plugin.rb
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
require_relative '../plugin'
|
||||
require_relative '../utils/file_writer'
|
||||
require_relative '../views/layout'
|
||||
require_relative '../views/projects_view'
|
||||
require_relative '../views/project_view'
|
||||
require_relative 'models'
|
||||
|
||||
module Pressa
|
||||
module Projects
|
||||
class Plugin < Pressa::Plugin
|
||||
def initialize(projects: [])
|
||||
@projects = projects
|
||||
end
|
||||
|
||||
def setup(site:, source_path:)
|
||||
end
|
||||
|
||||
def render(site:, target_path:)
|
||||
write_projects_index(site:, target_path:)
|
||||
|
||||
@projects.each do |project|
|
||||
write_project_page(project:, site:, target_path:)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write_projects_index(site:, target_path:)
|
||||
content_view = Views::ProjectsView.new(projects: @projects, site:)
|
||||
|
||||
html = render_layout(
|
||||
site:,
|
||||
page_title: "Projects – #{site.title}",
|
||||
canonical_url: site.url_for('/projects/'),
|
||||
content: content_view
|
||||
)
|
||||
|
||||
file_path = File.join(target_path, 'projects', 'index.html')
|
||||
Utils::FileWriter.write(path: file_path, content: html)
|
||||
end
|
||||
|
||||
def write_project_page(project:, site:, target_path:)
|
||||
content_view = Views::ProjectView.new(project:, site:)
|
||||
|
||||
html = render_layout(
|
||||
site:,
|
||||
page_title: "#{project.title} – #{site.title}",
|
||||
canonical_url: site.url_for(project.path),
|
||||
content: content_view
|
||||
)
|
||||
|
||||
file_path = File.join(target_path, 'projects', project.name, 'index.html')
|
||||
Utils::FileWriter.write(path: file_path, content: html)
|
||||
end
|
||||
|
||||
def render_layout(site:, page_title:, canonical_url:, content:)
|
||||
layout = Views::Layout.new(
|
||||
site:,
|
||||
page_title:,
|
||||
canonical_url:
|
||||
)
|
||||
|
||||
layout.call do
|
||||
content.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
pressa/lib/site.rb
Normal file
38
pressa/lib/site.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
require 'dry-struct'
|
||||
|
||||
module Pressa
|
||||
module Types
|
||||
include Dry.Types()
|
||||
end
|
||||
|
||||
class Script < Dry::Struct
|
||||
attribute :src, Types::String
|
||||
attribute :defer, Types::Bool.default(true)
|
||||
end
|
||||
|
||||
class Stylesheet < Dry::Struct
|
||||
attribute :href, Types::String
|
||||
end
|
||||
|
||||
class Site < Dry::Struct
|
||||
attribute :author, Types::String
|
||||
attribute :email, Types::String
|
||||
attribute :title, Types::String
|
||||
attribute :description, Types::String
|
||||
attribute :url, Types::String
|
||||
attribute :image_url, Types::String.optional.default(nil)
|
||||
attribute :scripts, Types::Array.of(Script).default([].freeze)
|
||||
attribute :styles, Types::Array.of(Stylesheet).default([].freeze)
|
||||
attribute :plugins, Types::Array.default([].freeze)
|
||||
attribute :renderers, Types::Array.default([].freeze)
|
||||
|
||||
def url_for(path)
|
||||
"#{url}#{path}"
|
||||
end
|
||||
|
||||
def image_url_for(path)
|
||||
return nil unless image_url
|
||||
"#{image_url}#{path}"
|
||||
end
|
||||
end
|
||||
end
|
||||
63
pressa/lib/site_generator.rb
Normal file
63
pressa/lib/site_generator.rb
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
require 'fileutils'
|
||||
require_relative 'utils/file_writer'
|
||||
|
||||
module Pressa
|
||||
class SiteGenerator
|
||||
attr_reader :site
|
||||
|
||||
def initialize(site:)
|
||||
@site = site
|
||||
end
|
||||
|
||||
def generate(source_path:, target_path:)
|
||||
FileUtils.rm_rf(target_path)
|
||||
FileUtils.mkdir_p(target_path)
|
||||
|
||||
site.plugins.each { |plugin| plugin.setup(site:, source_path:) }
|
||||
|
||||
site.plugins.each { |plugin| plugin.render(site:, target_path:) }
|
||||
|
||||
copy_static_files(source_path, target_path)
|
||||
process_public_directory(source_path, target_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def copy_static_files(source_path, target_path)
|
||||
public_dir = File.join(source_path, 'public')
|
||||
return unless Dir.exist?(public_dir)
|
||||
|
||||
Dir.glob(File.join(public_dir, '**', '*'), File::FNM_DOTMATCH).each do |source_file|
|
||||
next if File.directory?(source_file)
|
||||
next if File.basename(source_file) == '.' || File.basename(source_file) == '..'
|
||||
|
||||
relative_path = source_file.sub("#{public_dir}/", '')
|
||||
target_file = File.join(target_path, relative_path)
|
||||
|
||||
FileUtils.mkdir_p(File.dirname(target_file))
|
||||
FileUtils.cp(source_file, target_file)
|
||||
end
|
||||
end
|
||||
|
||||
def process_public_directory(source_path, target_path)
|
||||
public_dir = File.join(source_path, 'public')
|
||||
return unless Dir.exist?(public_dir)
|
||||
|
||||
site.renderers.each do |renderer|
|
||||
Dir.glob(File.join(public_dir, '**', '*'), File::FNM_DOTMATCH).each do |source_file|
|
||||
next if File.directory?(source_file)
|
||||
|
||||
filename = File.basename(source_file)
|
||||
ext = File.extname(source_file)[1..]
|
||||
|
||||
if renderer.can_render_file?(filename:, extension: ext)
|
||||
relative_path = File.dirname(source_file).sub("#{public_dir}/", '')
|
||||
target_dir = File.join(target_path, relative_path)
|
||||
|
||||
renderer.render(site:, file_path: source_file, target_dir:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
pressa/lib/utils/file_writer.rb
Normal file
21
pressa/lib/utils/file_writer.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
require 'fileutils'
|
||||
|
||||
module Pressa
|
||||
module Utils
|
||||
class FileWriter
|
||||
def self.write(path:, content:, permissions: 0o644)
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
|
||||
File.write(path, content, mode: 'w')
|
||||
File.chmod(permissions, path)
|
||||
end
|
||||
|
||||
def self.write_data(path:, data:, permissions: 0o644)
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
|
||||
File.binwrite(path, data)
|
||||
File.chmod(permissions, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
71
pressa/lib/utils/frontmatter_converter.rb
Normal file
71
pressa/lib/utils/frontmatter_converter.rb
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
module Pressa
|
||||
module Utils
|
||||
class FrontmatterConverter
|
||||
FIELD_PATTERN = /^([A-Z][a-z]+):\s*(.+)$/
|
||||
|
||||
def self.convert_file(input_path, output_path = nil)
|
||||
content = File.read(input_path)
|
||||
converted = convert_content(content)
|
||||
|
||||
if output_path
|
||||
File.write(output_path, converted)
|
||||
else
|
||||
File.write(input_path, converted)
|
||||
end
|
||||
end
|
||||
|
||||
def self.convert_content(content)
|
||||
unless content.start_with?("---\n")
|
||||
raise "File does not start with front-matter delimiter"
|
||||
end
|
||||
|
||||
parts = content.split(/^---\n/, 3)
|
||||
if parts.length < 3
|
||||
raise "Could not find end of front-matter"
|
||||
end
|
||||
|
||||
frontmatter = parts[1]
|
||||
body = parts[2]
|
||||
|
||||
yaml_frontmatter = convert_frontmatter_to_yaml(frontmatter)
|
||||
|
||||
"---\n#{yaml_frontmatter}---\n#{body}"
|
||||
end
|
||||
|
||||
def self.convert_frontmatter_to_yaml(frontmatter)
|
||||
fields = {}
|
||||
|
||||
frontmatter.each_line do |line|
|
||||
line = line.strip
|
||||
next if line.empty?
|
||||
|
||||
if line =~ FIELD_PATTERN
|
||||
field_name = $1
|
||||
field_value = $2.strip
|
||||
|
||||
fields[field_name] = field_value
|
||||
end
|
||||
end
|
||||
|
||||
yaml_lines = []
|
||||
fields.each do |name, value|
|
||||
yaml_lines << format_yaml_field(name, value)
|
||||
end
|
||||
|
||||
yaml_lines.join("\n") + "\n"
|
||||
end
|
||||
|
||||
private_class_method def self.format_yaml_field(name, value)
|
||||
needs_quoting = (value.include?(':') && !value.start_with?('http')) ||
|
||||
value.include?(',') ||
|
||||
name == 'Date'
|
||||
|
||||
if needs_quoting
|
||||
"#{name}: \"#{value}\""
|
||||
else
|
||||
"#{name}: #{value}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
85
pressa/lib/utils/markdown_renderer.rb
Normal file
85
pressa/lib/utils/markdown_renderer.rb
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
require 'kramdown'
|
||||
require 'yaml'
|
||||
require_relative 'file_writer'
|
||||
require_relative '../views/layout'
|
||||
|
||||
class String
|
||||
include Phlex::SGML::SafeObject
|
||||
end
|
||||
|
||||
module Pressa
|
||||
module Utils
|
||||
class MarkdownRenderer
|
||||
def can_render_file?(filename:, extension:)
|
||||
extension == 'md'
|
||||
end
|
||||
|
||||
def render(site:, file_path:, target_dir:)
|
||||
content = File.read(file_path)
|
||||
metadata, body_markdown = parse_content(content)
|
||||
|
||||
html_body = render_markdown(body_markdown)
|
||||
|
||||
page_title = metadata['Title'] || File.basename(file_path, '.md').capitalize
|
||||
show_extension = metadata['Show extension'] == 'true'
|
||||
|
||||
html = render_layout(
|
||||
site:,
|
||||
page_title:,
|
||||
canonical_url: site.url,
|
||||
body: html_body
|
||||
)
|
||||
|
||||
output_filename = if show_extension
|
||||
File.basename(file_path, '.md') + '.html'
|
||||
else
|
||||
File.join(File.basename(file_path, '.md'), 'index.html')
|
||||
end
|
||||
|
||||
output_path = File.join(target_dir, output_filename)
|
||||
FileWriter.write(path: output_path, content: html)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_content(content)
|
||||
if content =~ /\A---\s*\n(.*?)\n---\s*\n(.*)/m
|
||||
yaml_content = $1
|
||||
markdown = $2
|
||||
metadata = YAML.safe_load(yaml_content) || {}
|
||||
[metadata, markdown]
|
||||
else
|
||||
[{}, content]
|
||||
end
|
||||
end
|
||||
|
||||
def render_markdown(markdown)
|
||||
Kramdown::Document.new(
|
||||
markdown,
|
||||
input: 'GFM',
|
||||
syntax_highlighter: 'rouge',
|
||||
syntax_highlighter_opts: {
|
||||
line_numbers: false,
|
||||
wrap: false
|
||||
}
|
||||
).to_html
|
||||
end
|
||||
|
||||
def render_layout(site:, page_title:, canonical_url:, body:)
|
||||
layout = Views::Layout.new(
|
||||
site:,
|
||||
page_title: "#{page_title} – #{site.title}",
|
||||
canonical_url:
|
||||
)
|
||||
|
||||
layout.call do
|
||||
article(class: "page") do
|
||||
h1 { page_title }
|
||||
raw(body)
|
||||
div(class: "fin") { "◼" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
62
pressa/lib/views/archive_view.rb
Normal file
62
pressa/lib/views/archive_view.rb
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
require 'phlex'
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class ArchiveView < Phlex::HTML
|
||||
def initialize(posts_by_year:, site:)
|
||||
@posts_by_year = posts_by_year
|
||||
@site = site
|
||||
end
|
||||
|
||||
def view_template
|
||||
article(class: "archive") do
|
||||
h1 { "Archive" }
|
||||
|
||||
@posts_by_year.sorted_years.each do |year|
|
||||
year_posts = @posts_by_year.by_year[year]
|
||||
render_year(year, year_posts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_year(year, year_posts)
|
||||
section(class: "year") do
|
||||
h2 do
|
||||
a(href: @site.url_for("/posts/#{year}/")) { year.to_s }
|
||||
end
|
||||
|
||||
year_posts.sorted_months.each do |month_posts|
|
||||
render_month(year, month_posts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_month(year, month_posts)
|
||||
month = month_posts.month
|
||||
|
||||
section(class: "month") do
|
||||
h3 do
|
||||
a(href: @site.url_for("/posts/#{year}/#{month.padded}/")) do
|
||||
"#{month.name} #{year}"
|
||||
end
|
||||
end
|
||||
|
||||
ul do
|
||||
month_posts.sorted_posts.each do |post|
|
||||
li do
|
||||
if post.link_post?
|
||||
a(href: post.link) { "→ #{post.title}" }
|
||||
else
|
||||
a(href: @site.url_for(post.path)) { post.title }
|
||||
end
|
||||
plain " – #{post.formatted_date}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
103
pressa/lib/views/layout.rb
Normal file
103
pressa/lib/views/layout.rb
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
require 'phlex'
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class Layout < Phlex::HTML
|
||||
attr_reader :site, :page_title, :canonical_url, :page_scripts, :page_styles
|
||||
|
||||
def initialize(site:, page_title:, canonical_url:, page_scripts: [], page_styles: [])
|
||||
@site = site
|
||||
@page_title = page_title
|
||||
@canonical_url = canonical_url
|
||||
@page_scripts = page_scripts
|
||||
@page_styles = page_styles
|
||||
end
|
||||
|
||||
def view_template(&block)
|
||||
doctype
|
||||
|
||||
html(lang: "en") do
|
||||
head do
|
||||
meta(charset: "utf-8")
|
||||
meta(name: "viewport", content: "width=device-width, initial-scale=1")
|
||||
title { page_title }
|
||||
meta(name: "description", content: site.description)
|
||||
meta(name: "author", content: site.author)
|
||||
|
||||
link(rel: "canonical", href: canonical_url)
|
||||
|
||||
meta(property: "og:title", content: page_title)
|
||||
meta(property: "og:description", content: site.description)
|
||||
meta(property: "og:url", content: canonical_url)
|
||||
meta(property: "og:type", content: "website")
|
||||
if site.image_url
|
||||
meta(property: "og:image", content: site.image_url)
|
||||
end
|
||||
|
||||
meta(name: "twitter:card", content: "summary")
|
||||
meta(name: "twitter:title", content: page_title)
|
||||
meta(name: "twitter:description", content: site.description)
|
||||
|
||||
link(rel: "alternate", type: "application/rss+xml", title: "RSS Feed", href: site.url_for('/feed.xml'))
|
||||
link(rel: "alternate", type: "application/json", title: "JSON Feed", href: site.url_for('/feed.json'))
|
||||
|
||||
all_styles.each do |style|
|
||||
link(rel: "stylesheet", href: site.url_for("/#{style.href}"))
|
||||
end
|
||||
end
|
||||
|
||||
body do
|
||||
render_header
|
||||
main(&block)
|
||||
render_footer
|
||||
render_scripts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_styles
|
||||
site.styles + page_styles
|
||||
end
|
||||
|
||||
def all_scripts
|
||||
site.scripts + page_scripts
|
||||
end
|
||||
|
||||
def render_header
|
||||
header do
|
||||
h1 { a(href: "/") { site.title } }
|
||||
nav do
|
||||
ul do
|
||||
li { a(href: "/") { "Home" } }
|
||||
li { a(href: "/posts/") { "Archive" } }
|
||||
li { a(href: "/projects/") { "Projects" } }
|
||||
li { a(href: "/about/") { "About" } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_footer
|
||||
footer do
|
||||
p { "© #{Time.now.year} #{site.author}" }
|
||||
p do
|
||||
plain "Email: "
|
||||
a(href: "mailto:#{site.email}") { site.email }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_scripts
|
||||
all_scripts.each do |script|
|
||||
if script.defer
|
||||
script_(src: site.url_for("/#{script.src}"), defer: true)
|
||||
else
|
||||
script_(src: site.url_for("/#{script.src}"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
pressa/lib/views/month_posts_view.rb
Normal file
24
pressa/lib/views/month_posts_view.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
require 'phlex'
|
||||
require_relative 'post_view'
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class MonthPostsView < Phlex::HTML
|
||||
def initialize(year:, month_posts:, site:)
|
||||
@year = year
|
||||
@month_posts = month_posts
|
||||
@site = site
|
||||
end
|
||||
|
||||
def view_template
|
||||
article(class: "month-posts") do
|
||||
h1 { "#{@month_posts.month.name} #{@year}" }
|
||||
|
||||
@month_posts.sorted_posts.each do |post|
|
||||
render PostView.new(post:, site: @site)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
44
pressa/lib/views/post_view.rb
Normal file
44
pressa/lib/views/post_view.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
require 'phlex'
|
||||
|
||||
class String
|
||||
include Phlex::SGML::SafeObject
|
||||
end
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class PostView < Phlex::HTML
|
||||
def initialize(post:, site:)
|
||||
@post = post
|
||||
@site = site
|
||||
end
|
||||
|
||||
def view_template
|
||||
article(class: "post") do
|
||||
header do
|
||||
if @post.link_post?
|
||||
h1 do
|
||||
a(href: @post.link) { "→ #{@post.title}" }
|
||||
end
|
||||
else
|
||||
h1 { @post.title }
|
||||
end
|
||||
|
||||
div(class: "post-meta") do
|
||||
time(datetime: @post.date.iso8601) { @post.formatted_date }
|
||||
plain " · "
|
||||
a(href: @site.url_for(@post.path), class: "permalink") { "Permalink" }
|
||||
end
|
||||
end
|
||||
|
||||
div(class: "post-content") do
|
||||
raw(@post.body)
|
||||
end
|
||||
|
||||
footer(class: "post-footer") do
|
||||
div(class: "fin") { "◼" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
45
pressa/lib/views/project_view.rb
Normal file
45
pressa/lib/views/project_view.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
require 'phlex'
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class ProjectView < Phlex::HTML
|
||||
def initialize(project:, site:)
|
||||
@project = project
|
||||
@site = site
|
||||
end
|
||||
|
||||
def view_template
|
||||
article(class: "project", data_title: @project.github_path) do
|
||||
header do
|
||||
h1 { @project.title }
|
||||
p(class: "description") { @project.description }
|
||||
p do
|
||||
a(href: @project.url) { "View on GitHub →" }
|
||||
end
|
||||
end
|
||||
|
||||
section(class: "project-stats") do
|
||||
h2 { "Statistics" }
|
||||
div(id: "stats") do
|
||||
p { "Loading..." }
|
||||
end
|
||||
end
|
||||
|
||||
section(class: "project-contributors") do
|
||||
h2 { "Contributors" }
|
||||
div(id: "contributors") do
|
||||
p { "Loading..." }
|
||||
end
|
||||
end
|
||||
|
||||
section(class: "project-languages") do
|
||||
h2 { "Languages" }
|
||||
div(id: "languages") do
|
||||
p { "Loading..." }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
pressa/lib/views/projects_view.rb
Normal file
29
pressa/lib/views/projects_view.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
require 'phlex'
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class ProjectsView < Phlex::HTML
|
||||
def initialize(projects:, site:)
|
||||
@projects = projects
|
||||
@site = site
|
||||
end
|
||||
|
||||
def view_template
|
||||
article(class: "projects") do
|
||||
h1 { "Projects" }
|
||||
|
||||
p { "Open source projects I've created or contributed to." }
|
||||
|
||||
ul(class: "projects-list") do
|
||||
@projects.each do |project|
|
||||
li do
|
||||
a(href: @site.url_for(project.path)) { project.title }
|
||||
plain " – #{project.description}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
pressa/lib/views/recent_posts_view.rb
Normal file
21
pressa/lib/views/recent_posts_view.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
require 'phlex'
|
||||
require_relative 'post_view'
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class RecentPostsView < Phlex::HTML
|
||||
def initialize(posts:, site:)
|
||||
@posts = posts
|
||||
@site = site
|
||||
end
|
||||
|
||||
def view_template
|
||||
div(class: "recent-posts") do
|
||||
@posts.each do |post|
|
||||
render PostView.new(post:, site: @site)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
50
pressa/lib/views/year_posts_view.rb
Normal file
50
pressa/lib/views/year_posts_view.rb
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
require 'phlex'
|
||||
|
||||
module Pressa
|
||||
module Views
|
||||
class YearPostsView < Phlex::HTML
|
||||
def initialize(year:, year_posts:, site:)
|
||||
@year = year
|
||||
@year_posts = year_posts
|
||||
@site = site
|
||||
end
|
||||
|
||||
def view_template
|
||||
article(class: "year-posts") do
|
||||
h1 { @year.to_s }
|
||||
|
||||
@year_posts.sorted_months.each do |month_posts|
|
||||
render_month(month_posts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_month(month_posts)
|
||||
month = month_posts.month
|
||||
|
||||
section(class: "month") do
|
||||
h2 do
|
||||
a(href: @site.url_for("/posts/#{@year}/#{month.padded}/")) do
|
||||
month.name
|
||||
end
|
||||
end
|
||||
|
||||
ul do
|
||||
month_posts.sorted_posts.each do |post|
|
||||
li do
|
||||
if post.link_post?
|
||||
a(href: post.link) { "→ #{post.title}" }
|
||||
else
|
||||
a(href: @site.url_for(post.path)) { post.title }
|
||||
end
|
||||
plain " – #{post.formatted_date}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
pressa/spec/examples.txt
Normal file
17
pressa/spec/examples.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
example_id | status | run_time |
|
||||
------------------------------------------------- | ------ | --------------- |
|
||||
./spec/posts/metadata_spec.rb[1:1:1] | passed | 0.00009 seconds |
|
||||
./spec/posts/metadata_spec.rb[1:1:2] | passed | 0.00032 seconds |
|
||||
./spec/posts/metadata_spec.rb[1:1:3] | passed | 0.00228 seconds |
|
||||
./spec/posts/repo_spec.rb[1:1:1] | passed | 0.00078 seconds |
|
||||
./spec/posts/repo_spec.rb[1:1:2] | passed | 0.01536 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:1:1] | passed | 0.00024 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:1:2] | passed | 0.00003 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:1:3] | passed | 0.00006 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:1:4] | passed | 0.00004 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:1:5] | passed | 0.00004 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:1:6] | passed | 0.00003 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:1:7] | passed | 0.00042 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:1:8] | passed | 0.00013 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:2:1] | passed | 0.00059 seconds |
|
||||
./spec/utils/frontmatter_converter_spec.rb[1:2:2] | passed | 0.00004 seconds |
|
||||
69
pressa/spec/posts/metadata_spec.rb
Normal file
69
pressa/spec/posts/metadata_spec.rb
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Pressa::Posts::PostMetadata do
|
||||
describe '.parse' do
|
||||
it 'parses valid YAML front-matter' do
|
||||
content = <<~MARKDOWN
|
||||
---
|
||||
Title: Test Post
|
||||
Author: Trent Reznor
|
||||
Date: 5th November, 2025
|
||||
Timestamp: 2025-11-05T10:00:00-08:00
|
||||
Tags: Ruby, Testing
|
||||
Scripts: highlight.js
|
||||
Styles: code.css
|
||||
Link: https://example.net/external
|
||||
---
|
||||
|
||||
This is the post body.
|
||||
MARKDOWN
|
||||
|
||||
metadata = described_class.parse(content)
|
||||
|
||||
expect(metadata.title).to eq('Test Post')
|
||||
expect(metadata.author).to eq('Trent Reznor')
|
||||
expect(metadata.formatted_date).to eq('5th November, 2025')
|
||||
expect(metadata.date.year).to eq(2025)
|
||||
expect(metadata.date.month).to eq(11)
|
||||
expect(metadata.date.day).to eq(5)
|
||||
expect(metadata.link).to eq('https://example.net/external')
|
||||
expect(metadata.tags).to eq(['Ruby', 'Testing'])
|
||||
expect(metadata.scripts.map(&:src)).to eq(['highlight.js'])
|
||||
expect(metadata.styles.map(&:href)).to eq(['code.css'])
|
||||
end
|
||||
|
||||
it 'raises error when required fields are missing' do
|
||||
content = <<~MARKDOWN
|
||||
---
|
||||
Title: Incomplete Post
|
||||
---
|
||||
|
||||
Body content
|
||||
MARKDOWN
|
||||
|
||||
expect {
|
||||
described_class.parse(content)
|
||||
}.to raise_error(/Missing required fields/)
|
||||
end
|
||||
|
||||
it 'handles posts without optional fields' do
|
||||
content = <<~MARKDOWN
|
||||
---
|
||||
Title: Simple Post
|
||||
Author: Fat Mike
|
||||
Date: 1st January, 2025
|
||||
Timestamp: 2025-01-01T12:00:00-08:00
|
||||
---
|
||||
|
||||
Simple content
|
||||
MARKDOWN
|
||||
|
||||
metadata = described_class.parse(content)
|
||||
|
||||
expect(metadata.tags).to eq([])
|
||||
expect(metadata.scripts).to eq([])
|
||||
expect(metadata.styles).to eq([])
|
||||
expect(metadata.link).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
73
pressa/spec/posts/repo_spec.rb
Normal file
73
pressa/spec/posts/repo_spec.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
require 'spec_helper'
|
||||
require 'fileutils'
|
||||
require 'tmpdir'
|
||||
|
||||
RSpec.describe Pressa::Posts::PostRepo do
|
||||
let(:repo) { described_class.new }
|
||||
|
||||
describe '#read_posts' do
|
||||
it 'reads and organizes posts by year and month' do
|
||||
Dir.mktmpdir do |tmpdir|
|
||||
posts_dir = File.join(tmpdir, 'posts', '2025', '11')
|
||||
FileUtils.mkdir_p(posts_dir)
|
||||
|
||||
post_content = <<~MARKDOWN
|
||||
---
|
||||
Title: Shredding in November
|
||||
Author: Shaun White
|
||||
Date: 5th November, 2025
|
||||
Timestamp: 2025-11-05T10:00:00-08:00
|
||||
---
|
||||
|
||||
Had an epic day at Whistler. The powder was deep and the lines were short.
|
||||
MARKDOWN
|
||||
|
||||
File.write(File.join(posts_dir, 'shredding.md'), post_content)
|
||||
|
||||
posts_by_year = repo.read_posts(File.join(tmpdir, 'posts'))
|
||||
|
||||
expect(posts_by_year.all_posts.length).to eq(1)
|
||||
|
||||
post = posts_by_year.all_posts.first
|
||||
expect(post.title).to eq('Shredding in November')
|
||||
expect(post.author).to eq('Shaun White')
|
||||
expect(post.slug).to eq('shredding')
|
||||
expect(post.year).to eq(2025)
|
||||
expect(post.month).to eq(11)
|
||||
expect(post.path).to eq('/posts/2025/11/shredding')
|
||||
end
|
||||
end
|
||||
|
||||
it 'generates excerpts from post content' do
|
||||
Dir.mktmpdir do |tmpdir|
|
||||
posts_dir = File.join(tmpdir, 'posts', '2025', '11')
|
||||
FileUtils.mkdir_p(posts_dir)
|
||||
|
||||
post_content = <<~MARKDOWN
|
||||
---
|
||||
Title: Test Post
|
||||
Author: Greg Graffin
|
||||
Date: 5th November, 2025
|
||||
Timestamp: 2025-11-05T10:00:00-08:00
|
||||
---
|
||||
|
||||
This is a test post with some content. It should generate an excerpt.
|
||||
|
||||

|
||||
|
||||
More content with a [link](https://example.net).
|
||||
MARKDOWN
|
||||
|
||||
File.write(File.join(posts_dir, 'test.md'), post_content)
|
||||
|
||||
posts_by_year = repo.read_posts(File.join(tmpdir, 'posts'))
|
||||
post = posts_by_year.all_posts.first
|
||||
|
||||
expect(post.excerpt).to include('test post')
|
||||
expect(post.excerpt).not_to include('![')
|
||||
expect(post.excerpt).to include('link')
|
||||
expect(post.excerpt).not_to include('[link]')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
pressa/spec/spec_helper.rb
Normal file
25
pressa/spec/spec_helper.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
require_relative '../lib/pressa'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.expect_with :rspec do |expectations|
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
end
|
||||
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
config.shared_context_metadata_behavior = :apply_to_host_groups
|
||||
config.filter_run_when_matching :focus
|
||||
config.example_status_persistence_file_path = "spec/examples.txt"
|
||||
config.disable_monkey_patching!
|
||||
config.warnings = true
|
||||
|
||||
if config.files_to_run.one?
|
||||
config.default_formatter = "doc"
|
||||
end
|
||||
|
||||
config.profile_examples = 10
|
||||
config.order = :random
|
||||
Kernel.srand config.seed
|
||||
end
|
||||
194
pressa/spec/utils/frontmatter_converter_spec.rb
Normal file
194
pressa/spec/utils/frontmatter_converter_spec.rb
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
require 'spec_helper'
|
||||
require_relative '../../lib/utils/frontmatter_converter'
|
||||
|
||||
RSpec.describe Pressa::Utils::FrontmatterConverter do
|
||||
describe '.convert_content' do
|
||||
it 'converts simple front-matter to YAML' do
|
||||
input = <<~MARKDOWN
|
||||
---
|
||||
Title: Test Post
|
||||
Author: Sami Samhuri
|
||||
Date: 11th November, 2025
|
||||
Timestamp: 2025-11-11T14:00:00-08:00
|
||||
---
|
||||
|
||||
This is the post body.
|
||||
MARKDOWN
|
||||
|
||||
output = described_class.convert_content(input)
|
||||
|
||||
expect(output).to start_with("---\n")
|
||||
expect(output).to include("Title: Test Post")
|
||||
expect(output).to include("Author: Sami Samhuri")
|
||||
expect(output).to include("Date: \"11th November, 2025\"")
|
||||
expect(output).to include("Timestamp: \"2025-11-11T14:00:00-08:00\"")
|
||||
expect(output).to end_with("---\n\nThis is the post body.\n")
|
||||
end
|
||||
|
||||
it 'converts front-matter with tags' do
|
||||
input = <<~MARKDOWN
|
||||
---
|
||||
Title: Zelda Tones for iOS
|
||||
Author: Sami Samhuri
|
||||
Date: 6th March, 2013
|
||||
Timestamp: 2013-03-06T18:51:13-08:00
|
||||
Tags: zelda, nintendo, pacman, ringtones, tones, ios
|
||||
---
|
||||
|
||||
<h2>Zelda</h2>
|
||||
|
||||
<p>
|
||||
<a href="http://mattgemmell.com">Matt Gemmell</a> recently shared some
|
||||
<a href="http://mattgemmell.com/2013/03/05/iphone-5-super-nintendo-wallpapers/">sweet Super Nintendo wallpapers for iPhone 5</a>.
|
||||
</p>
|
||||
MARKDOWN
|
||||
|
||||
output = described_class.convert_content(input)
|
||||
|
||||
expect(output).to include("Title: Zelda Tones for iOS")
|
||||
expect(output).to include("Tags: \"zelda, nintendo, pacman, ringtones, tones, ios\"")
|
||||
expect(output).to include("<h2>Zelda</h2>")
|
||||
end
|
||||
|
||||
it 'converts front-matter with Link field' do
|
||||
input = <<~MARKDOWN
|
||||
---
|
||||
Title: Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo
|
||||
Author: Sami Samhuri
|
||||
Date: 16th September, 2006
|
||||
Timestamp: 2006-09-16T22:11:00-07:00
|
||||
Tags: amusement, buffalo
|
||||
Link: http://en.wikipedia.org/wiki/Buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo
|
||||
---
|
||||
|
||||
Wouldn't the sentence 'I want to put a hyphen between the words Fish and And...
|
||||
MARKDOWN
|
||||
|
||||
output = described_class.convert_content(input)
|
||||
|
||||
expect(output).to include("Link: http://en.wikipedia.org/wiki/Buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo")
|
||||
expect(output).to include("Tags: \"amusement, buffalo\"")
|
||||
end
|
||||
|
||||
it 'converts front-matter with Scripts and Styles' do
|
||||
input = <<~MARKDOWN
|
||||
---
|
||||
Title: Code Example Post
|
||||
Author: Sami Samhuri
|
||||
Date: 1st January, 2025
|
||||
Timestamp: 2025-01-01T12:00:00-08:00
|
||||
Scripts: highlight.js, custom.js
|
||||
Styles: code.css, theme.css
|
||||
---
|
||||
|
||||
Some code here.
|
||||
MARKDOWN
|
||||
|
||||
output = described_class.convert_content(input)
|
||||
|
||||
expect(output).to include("Scripts: \"highlight.js, custom.js\"")
|
||||
expect(output).to include("Styles: \"code.css, theme.css\"")
|
||||
end
|
||||
|
||||
it 'handles Date fields with colons correctly' do
|
||||
input = <<~MARKDOWN
|
||||
---
|
||||
Title: Test
|
||||
Author: Sami Samhuri
|
||||
Date: 1st January, 2025
|
||||
Timestamp: 2025-01-01T12:00:00-08:00
|
||||
---
|
||||
|
||||
Body
|
||||
MARKDOWN
|
||||
|
||||
output = described_class.convert_content(input)
|
||||
|
||||
expect(output).to include("Date: \"1st January, 2025\"")
|
||||
expect(output).to include("Timestamp: \"2025-01-01T12:00:00-08:00\"")
|
||||
end
|
||||
|
||||
it 'raises error if no front-matter delimiter' do
|
||||
input = "Just some content without front-matter"
|
||||
|
||||
expect {
|
||||
described_class.convert_content(input)
|
||||
}.to raise_error("File does not start with front-matter delimiter")
|
||||
end
|
||||
|
||||
it 'raises error if front-matter is not closed' do
|
||||
input = <<~MARKDOWN
|
||||
---
|
||||
Title: Unclosed
|
||||
Author: Test
|
||||
|
||||
Body without closing delimiter
|
||||
MARKDOWN
|
||||
|
||||
expect {
|
||||
described_class.convert_content(input)
|
||||
}.to raise_error("Could not find end of front-matter")
|
||||
end
|
||||
|
||||
it 'preserves empty lines in body' do
|
||||
input = <<~MARKDOWN
|
||||
---
|
||||
Title: Test
|
||||
Author: Sami Samhuri
|
||||
Date: 1st January, 2025
|
||||
Timestamp: 2025-01-01T12:00:00-08:00
|
||||
---
|
||||
|
||||
First paragraph.
|
||||
|
||||
Second paragraph after empty line.
|
||||
MARKDOWN
|
||||
|
||||
output = described_class.convert_content(input)
|
||||
|
||||
expect(output).to include("\nFirst paragraph.\n\nSecond paragraph after empty line.\n")
|
||||
end
|
||||
end
|
||||
|
||||
describe '.convert_frontmatter_to_yaml' do
|
||||
it 'converts all standard fields' do
|
||||
input = <<~FRONTMATTER
|
||||
Title: Test Post
|
||||
Author: Sami Samhuri
|
||||
Date: 11th November, 2025
|
||||
Timestamp: 2025-11-11T14:00:00-08:00
|
||||
Tags: Ruby, Testing
|
||||
Link: https://example.net
|
||||
Scripts: app.js
|
||||
Styles: style.css
|
||||
FRONTMATTER
|
||||
|
||||
yaml = described_class.convert_frontmatter_to_yaml(input)
|
||||
|
||||
expect(yaml).to include("Title: Test Post")
|
||||
expect(yaml).to include("Author: Sami Samhuri")
|
||||
expect(yaml).to include("Date: \"11th November, 2025\"")
|
||||
expect(yaml).to include("Timestamp: \"2025-11-11T14:00:00-08:00\"")
|
||||
expect(yaml).to include("Tags: \"Ruby, Testing\"")
|
||||
expect(yaml).to include("Link: https://example.net")
|
||||
expect(yaml).to include("Scripts: app.js")
|
||||
expect(yaml).to include("Styles: style.css")
|
||||
end
|
||||
|
||||
it 'handles empty lines gracefully' do
|
||||
input = <<~FRONTMATTER
|
||||
Title: Test
|
||||
|
||||
Author: Sami Samhuri
|
||||
Date: 1st January, 2025
|
||||
|
||||
Timestamp: 2025-01-01T12:00:00-08:00
|
||||
FRONTMATTER
|
||||
|
||||
yaml = described_class.convert_frontmatter_to_yaml(input)
|
||||
|
||||
expect(yaml).to include("Title: Test")
|
||||
expect(yaml).to include("Author: Sami Samhuri")
|
||||
end
|
||||
end
|
||||
end
|
||||
40
pressa/test-site/posts/2025/11/test-post.md
Normal file
40
pressa/test-site/posts/2025/11/test-post.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
Title: Testing Pressa with Ruby and Phlex
|
||||
Author: Trent Reznor
|
||||
Date: 11th November, 2025
|
||||
Timestamp: 2025-11-11T14:00:00-08:00
|
||||
Tags: Ruby, Phlex, Static Sites
|
||||
---
|
||||
|
||||
This is a test post to verify that Pressa is working correctly. We're building a static site generator using:
|
||||
|
||||
- Ruby 3.4
|
||||
- Phlex for HTML generation
|
||||
- Kramdown with Rouge for Markdown and syntax highlighting
|
||||
- dry-struct for immutable data models
|
||||
|
||||
## Code Example
|
||||
|
||||
Here's some Ruby code:
|
||||
|
||||
```ruby
|
||||
class Post < Dry::Struct
|
||||
attribute :title, Types::String
|
||||
attribute :body, Types::String
|
||||
end
|
||||
|
||||
post = Post.new(title: "Hello World", body: "This is a test")
|
||||
puts post.title
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
The generator supports:
|
||||
|
||||
1. Hierarchical post organization (year/month)
|
||||
2. Link posts (external URLs)
|
||||
3. JSON and RSS feeds
|
||||
4. Archive pages
|
||||
5. Projects section
|
||||
|
||||
Pretty cool, right?
|
||||
51
pressa/test-site/public/css/style.css
Normal file
51
pressa/test-site/public/css/style.css
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 2px solid #eee;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.post {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.fin {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
}
|
||||
51
pressa/test-www/css/style.css
Normal file
51
pressa/test-www/css/style.css
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 2px solid #eee;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.post {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.fin {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
}
|
||||
33
pressa/test-www/feed.json
Normal file
33
pressa/test-www/feed.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"version": "https://jsonfeed.org/version/1.1",
|
||||
"title": "samhuri.net",
|
||||
"home_page_url": "http://localhost:8000",
|
||||
"feed_url": "http://localhost:8000/feed.json",
|
||||
"description": "The personal blog of Sami Samhuri",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sami Samhuri",
|
||||
"url": "http://localhost:8000"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "http://localhost:8000/posts/2025/11/test-post",
|
||||
"url": "http://localhost:8000/posts/2025/11/test-post",
|
||||
"title": "Testing Pressa with Ruby and Phlex",
|
||||
"content_html": "<p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>\n\n<ul>\n <li>Ruby 3.4</li>\n <li>Phlex for HTML generation</li>\n <li>Kramdown with Rouge for Markdown and syntax highlighting</li>\n <li>dry-struct for immutable data models</li>\n</ul>\n\n<h2 id=\"code-example\">Code Example</h2>\n\n<p>Here’s some Ruby code:</p>\n\n<div class=\"language-ruby highlighter-rouge\"><span class=\"k\">class</span> <span class=\"nc\">Post</span> <span class=\"o\"><</span> <span class=\"no\">Dry</span><span class=\"o\">::</span><span class=\"no\">Struct</span>\n <span class=\"n\">attribute</span> <span class=\"ss\">:title</span><span class=\"p\">,</span> <span class=\"no\">Types</span><span class=\"o\">::</span><span class=\"no\">String</span>\n <span class=\"n\">attribute</span> <span class=\"ss\">:body</span><span class=\"p\">,</span> <span class=\"no\">Types</span><span class=\"o\">::</span><span class=\"no\">String</span>\n<span class=\"k\">end</span>\n\n<span class=\"n\">post</span> <span class=\"o\">=</span> <span class=\"no\">Post</span><span class=\"p\">.</span><span class=\"nf\">new</span><span class=\"p\">(</span><span class=\"ss\">title: </span><span class=\"s2\">\"Hello World\"</span><span class=\"p\">,</span> <span class=\"ss\">body: </span><span class=\"s2\">\"This is a test\"</span><span class=\"p\">)</span>\n<span class=\"nb\">puts</span> <span class=\"n\">post</span><span class=\"p\">.</span><span class=\"nf\">title</span>\n</div>\n\n<h2 id=\"features\">Features</h2>\n\n<p>The generator supports:</p>\n\n<ol>\n <li>Hierarchical post organization (year/month)</li>\n <li>Link posts (external URLs)</li>\n <li>JSON and RSS feeds</li>\n <li>Archive pages</li>\n <li>Projects section</li>\n</ol>\n\n<p>Pretty cool, right?</p>\n",
|
||||
"summary": "This is a test post to verify that Pressa is working correctly. We're building a static site generator using: - Ruby 3.4 - Phlex for HTML generation - Kramdown with Rouge for Markdown and syntax highlighting - dry-struct for immutable data models ## Code Example Here's some Ruby code: ```ruby class ...",
|
||||
"date_published": "2025-11-11T14:00:00-08:00",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Trent Reznor"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Ruby",
|
||||
"Phlex",
|
||||
"Static Sites"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
60
pressa/test-www/feed.xml
Normal file
60
pressa/test-www/feed.xml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>samhuri.net</title>
|
||||
<link>http://localhost:8000</link>
|
||||
<description>The personal blog of Sami Samhuri</description>
|
||||
<language>en-us</language>
|
||||
<pubDate>Tue, 11 Nov 2025 14:00:00 -0800</pubDate>
|
||||
<lastBuildDate>Tue, 11 Nov 2025 14:52:39 -0800</lastBuildDate>
|
||||
<atom:link href="http://localhost:8000/feed.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<title>Testing Pressa with Ruby and Phlex</title>
|
||||
<link>http://localhost:8000/posts/2025/11/test-post</link>
|
||||
<guid isPermaLink="true">http://localhost:8000/posts/2025/11/test-post</guid>
|
||||
<description>
|
||||
<![CDATA[<p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>
|
||||
|
||||
<ul>
|
||||
<li>Ruby 3.4</li>
|
||||
<li>Phlex for HTML generation</li>
|
||||
<li>Kramdown with Rouge for Markdown and syntax highlighting</li>
|
||||
<li>dry-struct for immutable data models</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="code-example">Code Example</h2>
|
||||
|
||||
<p>Here’s some Ruby code:</p>
|
||||
|
||||
<div class="language-ruby highlighter-rouge"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Struct</span>
|
||||
<span class="n">attribute</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||
<span class="n">attribute</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||
<span class="k">end</span>
|
||||
|
||||
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Hello World"</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"This is a test"</span><span class="p">)</span>
|
||||
<span class="nb">puts</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span>
|
||||
</div>
|
||||
|
||||
<h2 id="features">Features</h2>
|
||||
|
||||
<p>The generator supports:</p>
|
||||
|
||||
<ol>
|
||||
<li>Hierarchical post organization (year/month)</li>
|
||||
<li>Link posts (external URLs)</li>
|
||||
<li>JSON and RSS feeds</li>
|
||||
<li>Archive pages</li>
|
||||
<li>Projects section</li>
|
||||
</ol>
|
||||
|
||||
<p>Pretty cool, right?</p>
|
||||
]]>
|
||||
</description>
|
||||
<pubDate>Tue, 11 Nov 2025 14:00:00 -0800</pubDate>
|
||||
<author>sami@samhuri.net (Sami Samhuri)</author>
|
||||
<category>Ruby</category>
|
||||
<category>Phlex</category>
|
||||
<category>Static Sites</category>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
36
pressa/test-www/index.html
Normal file
36
pressa/test-www/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000"><meta property="og:title" content="samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><div class="recent-posts"><article class="post"><header><h1>Testing Pressa with Ruby and Phlex</h1><div class="post-meta"><time datetime="2025-11-11T14:00:00-08:00">11th November, 2025</time> · <a href="http://localhost:8000/posts/2025/11/test-post" class="permalink">Permalink</a></div></header><div class="post-content"><p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>
|
||||
|
||||
<ul>
|
||||
<li>Ruby 3.4</li>
|
||||
<li>Phlex for HTML generation</li>
|
||||
<li>Kramdown with Rouge for Markdown and syntax highlighting</li>
|
||||
<li>dry-struct for immutable data models</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="code-example">Code Example</h2>
|
||||
|
||||
<p>Here’s some Ruby code:</p>
|
||||
|
||||
<div class="language-ruby highlighter-rouge"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Struct</span>
|
||||
<span class="n">attribute</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||
<span class="n">attribute</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||
<span class="k">end</span>
|
||||
|
||||
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Hello World"</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"This is a test"</span><span class="p">)</span>
|
||||
<span class="nb">puts</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span>
|
||||
</div>
|
||||
|
||||
<h2 id="features">Features</h2>
|
||||
|
||||
<p>The generator supports:</p>
|
||||
|
||||
<ol>
|
||||
<li>Hierarchical post organization (year/month)</li>
|
||||
<li>Link posts (external URLs)</li>
|
||||
<li>JSON and RSS feeds</li>
|
||||
<li>Archive pages</li>
|
||||
<li>Projects section</li>
|
||||
</ol>
|
||||
|
||||
<p>Pretty cool, right?</p>
|
||||
</div><footer class="post-footer"><div class="fin">◼</div></footer></article></div></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
36
pressa/test-www/posts/2025/11/index.html
Normal file
36
pressa/test-www/posts/2025/11/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>November 2025 – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/posts/2025/11/"><meta property="og:title" content="November 2025 – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/posts/2025/11/"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="November 2025 – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="month-posts"><h1>November 2025</h1><article class="post"><header><h1>Testing Pressa with Ruby and Phlex</h1><div class="post-meta"><time datetime="2025-11-11T14:00:00-08:00">11th November, 2025</time> · <a href="http://localhost:8000/posts/2025/11/test-post" class="permalink">Permalink</a></div></header><div class="post-content"><p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>
|
||||
|
||||
<ul>
|
||||
<li>Ruby 3.4</li>
|
||||
<li>Phlex for HTML generation</li>
|
||||
<li>Kramdown with Rouge for Markdown and syntax highlighting</li>
|
||||
<li>dry-struct for immutable data models</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="code-example">Code Example</h2>
|
||||
|
||||
<p>Here’s some Ruby code:</p>
|
||||
|
||||
<div class="language-ruby highlighter-rouge"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Struct</span>
|
||||
<span class="n">attribute</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||
<span class="n">attribute</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||
<span class="k">end</span>
|
||||
|
||||
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Hello World"</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"This is a test"</span><span class="p">)</span>
|
||||
<span class="nb">puts</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span>
|
||||
</div>
|
||||
|
||||
<h2 id="features">Features</h2>
|
||||
|
||||
<p>The generator supports:</p>
|
||||
|
||||
<ol>
|
||||
<li>Hierarchical post organization (year/month)</li>
|
||||
<li>Link posts (external URLs)</li>
|
||||
<li>JSON and RSS feeds</li>
|
||||
<li>Archive pages</li>
|
||||
<li>Projects section</li>
|
||||
</ol>
|
||||
|
||||
<p>Pretty cool, right?</p>
|
||||
</div><footer class="post-footer"><div class="fin">◼</div></footer></article></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
36
pressa/test-www/posts/2025/11/test-post/index.html
Normal file
36
pressa/test-www/posts/2025/11/test-post/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Testing Pressa with Ruby and Phlex – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/posts/2025/11/test-post"><meta property="og:title" content="Testing Pressa with Ruby and Phlex – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/posts/2025/11/test-post"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Testing Pressa with Ruby and Phlex – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="post"><header><h1>Testing Pressa with Ruby and Phlex</h1><div class="post-meta"><time datetime="2025-11-11T14:00:00-08:00">11th November, 2025</time> · <a href="http://localhost:8000/posts/2025/11/test-post" class="permalink">Permalink</a></div></header><div class="post-content"><p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>
|
||||
|
||||
<ul>
|
||||
<li>Ruby 3.4</li>
|
||||
<li>Phlex for HTML generation</li>
|
||||
<li>Kramdown with Rouge for Markdown and syntax highlighting</li>
|
||||
<li>dry-struct for immutable data models</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="code-example">Code Example</h2>
|
||||
|
||||
<p>Here’s some Ruby code:</p>
|
||||
|
||||
<div class="language-ruby highlighter-rouge"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Struct</span>
|
||||
<span class="n">attribute</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||
<span class="n">attribute</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||
<span class="k">end</span>
|
||||
|
||||
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Hello World"</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"This is a test"</span><span class="p">)</span>
|
||||
<span class="nb">puts</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span>
|
||||
</div>
|
||||
|
||||
<h2 id="features">Features</h2>
|
||||
|
||||
<p>The generator supports:</p>
|
||||
|
||||
<ol>
|
||||
<li>Hierarchical post organization (year/month)</li>
|
||||
<li>Link posts (external URLs)</li>
|
||||
<li>JSON and RSS feeds</li>
|
||||
<li>Archive pages</li>
|
||||
<li>Projects section</li>
|
||||
</ol>
|
||||
|
||||
<p>Pretty cool, right?</p>
|
||||
</div><footer class="post-footer"><div class="fin">◼</div></footer></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
1
pressa/test-www/posts/2025/index.html
Normal file
1
pressa/test-www/posts/2025/index.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>2025 – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/posts/2025/"><meta property="og:title" content="2025 – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/posts/2025/"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="2025 – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="year-posts"><h1>2025</h1><section class="month"><h2><a href="http://localhost:8000/posts/2025/11/">November</a></h2><ul><li><a href="http://localhost:8000/posts/2025/11/test-post">Testing Pressa with Ruby and Phlex</a> – 11th November, 2025</li></ul></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
1
pressa/test-www/posts/index.html
Normal file
1
pressa/test-www/posts/index.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Archive – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/posts/"><meta property="og:title" content="Archive – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/posts/"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Archive – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="archive"><h1>Archive</h1><section class="year"><h2><a href="http://localhost:8000/posts/2025/">2025</a></h2><section class="month"><h3><a href="http://localhost:8000/posts/2025/11/">November 2025</a></h3><ul><li><a href="http://localhost:8000/posts/2025/11/test-post">Testing Pressa with Ruby and Phlex</a> – 11th November, 2025</li></ul></section></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
1
pressa/test-www/projects/bin/index.html
Normal file
1
pressa/test-www/projects/bin/index.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>bin – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/bin"><meta property="og:title" content="bin – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/bin"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="bin – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="project" data-title="samsonjs/bin"><header><h1>bin</h1><p class="description">my collection of scripts in ~/bin</p><p><a href="https://github.com/samsonjs/bin">View on GitHub →</a></p></header><section class="project-stats"><h2>Statistics</h2><div id="stats"><p>Loading...</p></div></section><section class="project-contributors"><h2>Contributors</h2><div id="contributors"><p>Loading...</p></div></section><section class="project-languages"><h2>Languages</h2><div id="languages"><p>Loading...</p></div></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
1
pressa/test-www/projects/config/index.html
Normal file
1
pressa/test-www/projects/config/index.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>config – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/config"><meta property="og:title" content="config – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/config"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="config – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="project" data-title="samsonjs/config"><header><h1>config</h1><p class="description">important dot files</p><p><a href="https://github.com/samsonjs/config">View on GitHub →</a></p></header><section class="project-stats"><h2>Statistics</h2><div id="stats"><p>Loading...</p></div></section><section class="project-contributors"><h2>Contributors</h2><div id="contributors"><p>Loading...</p></div></section><section class="project-languages"><h2>Languages</h2><div id="languages"><p>Loading...</p></div></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
1
pressa/test-www/projects/index.html
Normal file
1
pressa/test-www/projects/index.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Projects – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/"><meta property="og:title" content="Projects – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Projects – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="projects"><h1>Projects</h1><p>Open source projects I've created or contributed to.</p><ul class="projects-list"><li><a href="http://localhost:8000/projects/bin">bin</a> – my collection of scripts in ~/bin</li><li><a href="http://localhost:8000/projects/config">config</a> – important dot files</li><li><a href="http://localhost:8000/projects/unix-node">unix-node</a> – Node.js CommonJS module that exports useful Unix commands</li><li><a href="http://localhost:8000/projects/strftime">strftime</a> – strftime for JavaScript</li></ul></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
1
pressa/test-www/projects/strftime/index.html
Normal file
1
pressa/test-www/projects/strftime/index.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>strftime – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/strftime"><meta property="og:title" content="strftime – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/strftime"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="strftime – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="project" data-title="samsonjs/strftime"><header><h1>strftime</h1><p class="description">strftime for JavaScript</p><p><a href="https://github.com/samsonjs/strftime">View on GitHub →</a></p></header><section class="project-stats"><h2>Statistics</h2><div id="stats"><p>Loading...</p></div></section><section class="project-contributors"><h2>Contributors</h2><div id="contributors"><p>Loading...</p></div></section><section class="project-languages"><h2>Languages</h2><div id="languages"><p>Loading...</p></div></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
1
pressa/test-www/projects/unix-node/index.html
Normal file
1
pressa/test-www/projects/unix-node/index.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>unix-node – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/unix-node"><meta property="og:title" content="unix-node – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/unix-node"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="unix-node – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="project" data-title="samsonjs/unix-node"><header><h1>unix-node</h1><p class="description">Node.js CommonJS module that exports useful Unix commands</p><p><a href="https://github.com/samsonjs/unix-node">View on GitHub →</a></p></header><section class="project-stats"><h2>Statistics</h2><div id="stats"><p>Loading...</p></div></section><section class="project-contributors"><h2>Contributors</h2><div id="contributors"><p>Loading...</p></div></section><section class="project-languages"><h2>Languages</h2><div id="languages"><p>Loading...</p></div></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||
Loading…
Reference in a new issue