prt 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.githooks/pre-commit +17 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.rubocop.yml +28 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +8 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +340 -0
- data/Rakefile +26 -0
- data/bin/setup +11 -0
- data/exe/parrot +12 -0
- data/lib/helpers.rb +19 -0
- data/lib/parrot/commands/build.rb +1274 -0
- data/lib/parrot/commands/new.rb +25 -0
- data/lib/parrot/commands/post.rb +83 -0
- data/lib/parrot/commands/serve.rb +129 -0
- data/lib/parrot/constants.rb +45 -0
- data/lib/parrot/file_cache.rb +45 -0
- data/lib/parrot/logger.rb +23 -0
- data/lib/parrot/metadata.rb +4 -0
- data/lib/parrot/runner.rb +29 -0
- data/lib/parrot/template_handler.rb +19 -0
- data/lib/parrot.rb +120 -0
- data/parrot.gemspec +36 -0
- data/skel/.gitignore +3 -0
- data/skel/config.yaml +68 -0
- data/skel/css/app.scss +189 -0
- data/skel/images/.keep +0 -0
- data/skel/images/apple-touch-icon.png +0 -0
- data/skel/images/favicon.ico +0 -0
- data/skel/images/favicon.svg +40 -0
- data/skel/images/parrot.jpeg +0 -0
- data/skel/javascripts/app.js +37 -0
- data/skel/public/.keep +0 -0
- data/skel/views/404.md +7 -0
- data/skel/views/about.md +17 -0
- data/skel/views/layout.html.erb +72 -0
- data/skel/views/posts/about_parrot.md +63 -0
- data/skel/views/posts/sample.md +37 -0
- data/spec/parrot/commands/build_spec.rb +747 -0
- data/spec/parrot/commands/new_spec.rb +43 -0
- data/spec/parrot/commands/post_spec.rb +80 -0
- data/spec/parrot/commands/serve_spec.rb +66 -0
- data/spec/parrot/logger_spec.rb +39 -0
- data/spec/parrot/metadata_spec.rb +11 -0
- data/spec/parrot/runner_spec.rb +28 -0
- data/spec/parrot_spec.rb +37 -0
- data/spec/spec_helper.rb +7 -0
- metadata +237 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f74ac67f763dd65550644121d13f55c64fe593802618f0cc2fccc0d2f29643b9
|
|
4
|
+
data.tar.gz: 335d950f5cc47c5f7a012c65683dddc939748e69dc3b448283836f357a20f3ab
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4d14df5f29bc760679c7c08469828dfe9624a865833e7f242c417ca3f0e21c9067f0b7e40080f45aa21d6916972c9f1a4b31a53636739088650801e6dc494953
|
|
7
|
+
data.tar.gz: a9a22494f84754c69d33002436c5a167b4520ba23565c0c427b2ec0908ba58041b743ded1491e878aedc4b64957d562046e218fbab1b044c1697bd3daa04080d
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
echo "Running RuboCop..."
|
|
4
|
+
bundle exec rubocop
|
|
5
|
+
|
|
6
|
+
if [ $? -ne 0 ]; then
|
|
7
|
+
echo "RuboCop failed. Fix the offenses (bundle exec rubocop -a) and commit again."
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
echo "Running RSpec..."
|
|
12
|
+
bundle exec rspec
|
|
13
|
+
|
|
14
|
+
if [ $? -ne 0 ]; then
|
|
15
|
+
echo "RSpec failed. Commit aborted."
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [master]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: RSpec
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: ruby/setup-ruby@v1
|
|
15
|
+
with:
|
|
16
|
+
ruby-version: .ruby-version
|
|
17
|
+
bundler-cache: true
|
|
18
|
+
- run: bundle exec rspec
|
|
19
|
+
|
|
20
|
+
rubocop:
|
|
21
|
+
name: RuboCop
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: ruby/setup-ruby@v1
|
|
26
|
+
with:
|
|
27
|
+
ruby-version: .ruby-version
|
|
28
|
+
bundler-cache: true
|
|
29
|
+
- run: bundle exec rubocop --format github
|
data/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
.bundle
|
|
4
|
+
.config
|
|
5
|
+
.yardoc
|
|
6
|
+
.idea
|
|
7
|
+
Gemfile.lock
|
|
8
|
+
InstalledFiles
|
|
9
|
+
_yardoc
|
|
10
|
+
coverage
|
|
11
|
+
doc/
|
|
12
|
+
lib/bundler/man
|
|
13
|
+
pkg
|
|
14
|
+
rdoc
|
|
15
|
+
spec/reports
|
|
16
|
+
test/tmp
|
|
17
|
+
test/version_tmp
|
|
18
|
+
tmp
|
|
19
|
+
blog/
|
|
20
|
+
*.log
|
|
21
|
+
|
|
22
|
+
# Incremental-build checksum written into a blog's build directory
|
|
23
|
+
public/.checksum
|
|
24
|
+
*.test.log.*
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.2
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
Exclude:
|
|
6
|
+
- 'blog/**/*'
|
|
7
|
+
- 'vendor/**/*'
|
|
8
|
+
|
|
9
|
+
# BuildCommand is one large class by design; size/complexity limits and
|
|
10
|
+
# class doc comments aren't enforced.
|
|
11
|
+
Metrics:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Style/Documentation:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
Style/FrozenStringLiteralComment:
|
|
18
|
+
Enabled: false
|
|
19
|
+
|
|
20
|
+
Layout/LineLength:
|
|
21
|
+
Max: 150
|
|
22
|
+
|
|
23
|
+
# Commands take `(args = [], config)`; changing that would change their API.
|
|
24
|
+
Style/OptionalArguments:
|
|
25
|
+
Enabled: false
|
|
26
|
+
|
|
27
|
+
Style/OptionalBooleanParameter:
|
|
28
|
+
Enabled: false
|
data/.ruby-gemset
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parrot
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.6
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Branch naming
|
|
4
|
+
- Features: `feat/{github-issue-number}-{readable-branch-name}`
|
|
5
|
+
(e.g. `feat/2-post-tags`).
|
|
6
|
+
- Bugs: `fix/{github-issue-number}-{readable-branch-name}`
|
|
7
|
+
(e.g. `fix/7-broken-internal-links`).
|
|
8
|
+
- The readable part is lowercase and kebab-case.
|
data/Gemfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
|
+
|
|
3
|
+
# define only the development dependencies here
|
|
4
|
+
group :development do
|
|
5
|
+
gem 'debug'
|
|
6
|
+
gem 'pry'
|
|
7
|
+
gem 'rake'
|
|
8
|
+
gem 'rspec'
|
|
9
|
+
gem 'rubocop', require: false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# All runtime dependencies are defined in gemspec
|
|
13
|
+
gemspec
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2013 Deepak
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Parrot
|
|
2
|
+
|
|
3
|
+
A static site generator for Markdown blogs, written in Ruby. Point it at a
|
|
4
|
+
folder of Markdown and it produces a folder of HTML/CSS/JS. Syntax highlighting
|
|
5
|
+
and LaTeX math work out of the box.
|
|
6
|
+
|
|
7
|
+
Demo: [deepsnapster.com](https://deepsnapster.com) is built with Parrot.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Parrot needs Ruby (developed and tested on 4.0; 3.x should work). It is
|
|
12
|
+
published on RubyGems as `prt`:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
$ gem install prt
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That puts the `parrot` command on your `PATH`. Or add it to a `Gemfile`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem 'prt'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
then run `bundle install` and use `bundle exec parrot`.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
$ parrot new blog # scaffold a blog from the skeleton
|
|
30
|
+
$ cd blog
|
|
31
|
+
$ parrot post --title "Hello world" # add a post — it appears in the generated index automatically
|
|
32
|
+
$ parrot serve # build, then serve on http://localhost:8000 and rebuild on change
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For a one-off build without the server:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
$ parrot build # writes the site into ./public
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
| Command | What it does |
|
|
44
|
+
| ------------------ | -------------------------------------------------------------- |
|
|
45
|
+
| `parrot new <dir>` | Copy the skeleton blog into `<dir>` (must not already exist) |
|
|
46
|
+
| `parrot post --title "<title>"` | Scaffold `views/posts/<slug>.md` with today's date |
|
|
47
|
+
| `parrot build` | Build the current blog into `public/` |
|
|
48
|
+
| `parrot serve` | Build, serve `public/` on port 8000, and watch for changes |
|
|
49
|
+
|
|
50
|
+
Global flags: `-q` / `--quiet`, `-v` / `--version`, `-h` / `--help`.
|
|
51
|
+
|
|
52
|
+
`post`, `build` and `serve` operate on the current working directory, so run
|
|
53
|
+
them from the blog's root.
|
|
54
|
+
|
|
55
|
+
## Project layout
|
|
56
|
+
|
|
57
|
+
A generated blog looks like this:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
blog/
|
|
61
|
+
├── config.yaml # post listing settings — see "The index page" below
|
|
62
|
+
├── views/
|
|
63
|
+
│ ├── layout.html.erb # page wrapper; <%= yield %> is the rendered Markdown
|
|
64
|
+
│ ├── 404.md # built to public/404.html
|
|
65
|
+
│ ├── about.md # built to public/about.html, linked from the nav
|
|
66
|
+
│ └── posts/
|
|
67
|
+
│ └── *.md # one Markdown file per post
|
|
68
|
+
├── css/
|
|
69
|
+
│ └── **/*.{scss,css} # concatenated and compiled to public/app.css
|
|
70
|
+
├── javascripts/
|
|
71
|
+
│ └── app.js # copied to public/app.js
|
|
72
|
+
├── images/ # images referenced from pages are copied to public/images/
|
|
73
|
+
└── public/ # build output — serve/deploy this, don't edit it
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
There's no `views/index.md` — the home page is generated at build time from
|
|
77
|
+
everything in `views/posts/*.md`.
|
|
78
|
+
|
|
79
|
+
## Writing posts
|
|
80
|
+
|
|
81
|
+
Parrot uses simple markdown format https://www.markdownguide.org/basic-syntax/ for
|
|
82
|
+
text formatting.
|
|
83
|
+
|
|
84
|
+
Posts are [kramdown](https://kramdown.gettalong.org/) Markdown with GitHub-style
|
|
85
|
+
fenced code blocks. A fresh blog ships `views/posts/post1.md` (headings, code,
|
|
86
|
+
math) and `views/posts/post2.md` (images, lists, tables, quotes) as worked
|
|
87
|
+
examples of everything below.
|
|
88
|
+
|
|
89
|
+
- **Headings** — `#` for the post title, `##` / `###` for sections.
|
|
90
|
+
- **Code** — inline with `` `backticks` ``; fenced blocks tagged with a language
|
|
91
|
+
are syntax highlighted with [Rouge](https://github.com/rouge-ruby/rouge):
|
|
92
|
+
|
|
93
|
+
````
|
|
94
|
+
```ruby
|
|
95
|
+
puts "hello"
|
|
96
|
+
```
|
|
97
|
+
````
|
|
98
|
+
|
|
99
|
+
The theme is Monokai. Change `HIGHLIGHT_THEME` in `lib/parrot/constants.rb` to
|
|
100
|
+
any Rouge theme name (`github`, `gruvbox`, `molokai`, …).
|
|
101
|
+
- **Math** — LaTeX between `$$ … $$` is rendered by MathJax: inline when it sits
|
|
102
|
+
inside a line, a display block when it's on its own line.
|
|
103
|
+
- **Tables** — GitHub-style pipe tables render to HTML:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
| Feature | Supported |
|
|
107
|
+
| ------- | --------- |
|
|
108
|
+
| Tables | yes |
|
|
109
|
+
```
|
|
110
|
+
- **Blockquotes** — a line starting with `>`:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
> Blockquotes are good for asides and pull quotes.
|
|
114
|
+
```
|
|
115
|
+
- **Internal links** — `[text](#post2.md)` is rewritten to `post2.html` during
|
|
116
|
+
the build, so link posts to each other by their Markdown filename.
|
|
117
|
+
- **`{post_date}`** — a literal `{post_date}` placeholder anywhere in a post's
|
|
118
|
+
body is replaced at build time with its header `date`, formatted per
|
|
119
|
+
`post_date_format.on_post` in `config.yaml` (see "The index page" below).
|
|
120
|
+
|
|
121
|
+
Right under each post's `<h1>`, the build adds a
|
|
122
|
+
`<p class="post-meta">` line with the post's date (per
|
|
123
|
+
`post_date_format.on_post`) and its category as a
|
|
124
|
+
`<a class="category-tag">` link to that category's page. Either part is
|
|
125
|
+
left out when the header doesn't have it. Posts written by older versions of
|
|
126
|
+
`parrot post` have a `_{post_date}_` line under the title; it's replaced by
|
|
127
|
+
this line, so the date isn't shown twice.
|
|
128
|
+
|
|
129
|
+
Every built post also gets a link back to the index (`<p class="back-link">`) at
|
|
130
|
+
the top of its `<main>`, pointing at `index.html`; style it with the
|
|
131
|
+
`.back-link` class in your CSS. The index page doesn't get one. Its text is
|
|
132
|
+
`config.yaml`'s `post_listing.back_link_text` (default "← Back to all posts";
|
|
133
|
+
"" omits the link — see "The index page" below).
|
|
134
|
+
|
|
135
|
+
## Post header
|
|
136
|
+
|
|
137
|
+
Each post starts with an HTML comment holding its metadata. `parrot post` writes
|
|
138
|
+
`title`, `date`, `lang` and an empty `category`; `description` and `tags` are
|
|
139
|
+
ones you can add by hand:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
<!--
|
|
143
|
+
title: My new post title
|
|
144
|
+
date: 08/09/2026
|
|
145
|
+
lang: en
|
|
146
|
+
category: Programming
|
|
147
|
+
description: One or two sentences for search results and social cards.
|
|
148
|
+
tags: algorithms, coding
|
|
149
|
+
-->
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`title` becomes the page's `<title>` and `og:title` at build time. `lang` sets
|
|
153
|
+
`<html lang="…">` for that page — leave it `en`, or set it per post (`ml`, `hi`,
|
|
154
|
+
…) when a post is in another language, which also feeds `og:locale`.
|
|
155
|
+
`description` is optional: it fills `<meta name="description">`, `og:description`
|
|
156
|
+
and `twitter:description`, and Parrot falls back to the post's first paragraph
|
|
157
|
+
when it's absent. `tags` is an optional comma-separated list: when present,
|
|
158
|
+
the tags are listed at the bottom of the post as
|
|
159
|
+
`<p class="post-tags">Tags: <span class="tag">algorithms</span> …</p>` (style it
|
|
160
|
+
with `.post-tags` / `.tag`), and emitted as `article:tag` meta tags and the
|
|
161
|
+
JSON-LD `keywords`. `{post_tags}` in `list_format` shows them on the index as
|
|
162
|
+
written. `category` is optional and takes one name per post; see "Categories"
|
|
163
|
+
below. Set your site's URL once in `views/layout.html.erb` — the
|
|
164
|
+
`<meta property="og:url">` and `<link rel="canonical">` tags — and Parrot
|
|
165
|
+
rewrites both per page, appending the built file's path
|
|
166
|
+
(`https://example.com/post1.html`, `https://example.com/` for the index).
|
|
167
|
+
|
|
168
|
+
The layout also ships link-preview tags — `og:type`, `og:site_name`, `og:image`
|
|
169
|
+
and `twitter:card`. A relative `og:image` path (`images/parrot.jpeg`) is copied
|
|
170
|
+
into the build and rewritten to an absolute URL; swap it for your own image or a
|
|
171
|
+
full URL. When it's a local PNG/JPEG/GIF, Parrot reads its size and adds
|
|
172
|
+
`og:image:width`/`height`. The index stays `og:type=website`; each post is built
|
|
173
|
+
as `og:type=article` with an `article:published_time` derived from its header
|
|
174
|
+
`date`. Every page also gets a schema.org JSON-LD block — `BlogPosting` for
|
|
175
|
+
posts, `WebSite` for the index.
|
|
176
|
+
|
|
177
|
+
Other layout defaults worth knowing: `app.js` loads with `defer`, the CDNs are
|
|
178
|
+
`preconnect`ed, `theme-color` is set for light and dark, and a parrot icon ships
|
|
179
|
+
in three forms — `images/favicon.ico`, `images/favicon.svg` and
|
|
180
|
+
`images/apple-touch-icon.png` (swap in your own). Any `images/…` file referenced
|
|
181
|
+
from an `<img>` or `<link>` is copied into the build.
|
|
182
|
+
|
|
183
|
+
## Draft mode
|
|
184
|
+
```
|
|
185
|
+
<!--
|
|
186
|
+
title: My new post title
|
|
187
|
+
date: 08/09/2026
|
|
188
|
+
lang: en
|
|
189
|
+
description: One or two sentences for search results and social cards.
|
|
190
|
+
draft: true
|
|
191
|
+
-->
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
If `draft: true` is set in the header then the post becomes a draft, the draft post won't be published. By default draft mode is set to false.
|
|
195
|
+
`parrot build` leaves drafts out of the index listing, `sitemap.xml` and
|
|
196
|
+
`feed.xml` too; `parrot serve` builds and lists them so you can preview them.
|
|
197
|
+
|
|
198
|
+
## The index page
|
|
199
|
+
|
|
200
|
+
`public/index.html` is generated at build time from every file in
|
|
201
|
+
`views/posts/*.md`, newest first — there's nothing to hand-edit. How it's
|
|
202
|
+
rendered is controlled by `config.yaml` at the blog's root:
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
post_listing:
|
|
206
|
+
list_title: "Post listing" # the index page's <h1>; "" omits it
|
|
207
|
+
back_link_text: "← Back to all posts" # the link atop every post; "" omits it
|
|
208
|
+
group_by: none # none | year | month
|
|
209
|
+
list_format: "{post_date} ~ [{post_title}]({post_link}) {post_category_tag}"
|
|
210
|
+
|
|
211
|
+
post_date_format:
|
|
212
|
+
on_list: "%m/%Y" # {post_date} inside list_format above
|
|
213
|
+
on_post: "%d/%m/%Y" # a literal {post_date} placeholder inside a post's own body
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**`group_by`** wraps the listing in `## <year>` or `## <Month Year>` sections
|
|
217
|
+
(newest first); `none` is a flat list.
|
|
218
|
+
|
|
219
|
+
**`list_format`** is a Markdown template applied to each post:
|
|
220
|
+
|
|
221
|
+
- `{post_<key>}` — that key from the post's header, as plain text:
|
|
222
|
+
`{post_title}`, `{post_lang}`, or any custom field you add to a post's
|
|
223
|
+
`<!-- key: value -->` header.
|
|
224
|
+
- `{post_date}` — the header's `date`, formatted per `post_date_format.on_list`
|
|
225
|
+
rather than shown as-authored.
|
|
226
|
+
- `{post_category_tag}` — the post's `category` as a `<a class="category-tag">`
|
|
227
|
+
link to its category page; empty for a post without one. Leave it out of
|
|
228
|
+
`list_format` to hide categories in listings.
|
|
229
|
+
- `{post_link}` — the post's href, the one field Parrot computes itself rather
|
|
230
|
+
than reading from the header. Wrap whichever part should be clickable in
|
|
231
|
+
Markdown link syntax yourself: `[{post_title}]({post_link})` links just the
|
|
232
|
+
title; `[{post_date} ~ {post_title}]({post_link})` links the whole line.
|
|
233
|
+
- Anything else in `{…}` is a Ruby [`strftime`](https://ruby-doc.org/3.2.2/Date.html#method-i-strftime)
|
|
234
|
+
format string applied to the post's header `date` directly, e.g.
|
|
235
|
+
`{%A, %B %d %Y}` — shown as formatted, not run through `post_date_format`.
|
|
236
|
+
|
|
237
|
+
`post_date_format.on_list` and `.on_post` are themselves `strftime` format
|
|
238
|
+
strings, formatting `{post_date}` wherever it's used — in `list_format` above,
|
|
239
|
+
and as a literal `{post_date}` placeholder inside a post's own Markdown body
|
|
240
|
+
(see "Writing posts").
|
|
241
|
+
|
|
242
|
+
`config.yaml` is optional; a missing file, section, or key falls back to the
|
|
243
|
+
defaults shown above.
|
|
244
|
+
|
|
245
|
+
## Categories
|
|
246
|
+
|
|
247
|
+
Give a post one category in its header (`category: Ruby`) and the build adds:
|
|
248
|
+
|
|
249
|
+
- **`category-ruby.html`**, listing that category's posts newest first. It uses
|
|
250
|
+
the same `list_format`, `group_by` and pager as the index. Once a category
|
|
251
|
+
has more posts than `per_page`, the listing continues on
|
|
252
|
+
`category-ruby_2.html`, `category-ruby_3.html`, and so on.
|
|
253
|
+
- **`categories.html`**, linked from the layout's nav, listing every category
|
|
254
|
+
with its post count (`<ul class="category-list">`).
|
|
255
|
+
- A link to the category under the post's title, and in listings through
|
|
256
|
+
`{post_category_tag}`.
|
|
257
|
+
|
|
258
|
+
Page names come from the category's name, lowercased, with spaces as hyphens and
|
|
259
|
+
anything other than letters, digits and hyphens dropped. Names that end up the
|
|
260
|
+
same ("C++" and "C") share one page, and the build warns about it. A post can't
|
|
261
|
+
be named `category`, `categories`, or after a category page that the build
|
|
262
|
+
writes (`category-ruby.md`). Category pages are listed in `sitemap.xml`.
|
|
263
|
+
|
|
264
|
+
## How `serve` rebuilds
|
|
265
|
+
|
|
266
|
+
- On startup Parrot hashes every source file. If the combined checksum differs
|
|
267
|
+
from `public/.checksum` — or that file is missing — it runs a full build and
|
|
268
|
+
then writes the new checksum. Otherwise it skips straight to serving the
|
|
269
|
+
existing `public/`.
|
|
270
|
+
- While running, each saved file rebuilds only what it affects: a single post,
|
|
271
|
+
a new/removed post, `views/404.md`, `views/about.md`, the compiled CSS, `app.js`, or a copied
|
|
272
|
+
image. Because the index is generated from `views/posts/*.md`, adding,
|
|
273
|
+
removing or editing a post also rebuilds the index and category pages (along
|
|
274
|
+
with `sitemap.xml` and `feed.xml`); editing `config.yaml` rebuilds the index,
|
|
275
|
+
category pages, `sitemap.xml` and every post, since it can affect both the listing and each post's
|
|
276
|
+
`{post_date}` placeholder. Editing `views/layout.html.erb` rebuilds
|
|
277
|
+
everything.
|
|
278
|
+
- `public/.checksum` is regenerated build state. It is gitignored and must not
|
|
279
|
+
be deployed.
|
|
280
|
+
|
|
281
|
+
## Deployment
|
|
282
|
+
|
|
283
|
+
Run `parrot build` and upload the contents of `public/` to any static host —
|
|
284
|
+
GitHub Pages, Netlify, S3, nginx, and so on. Exclude `public/.checksum`.
|
|
285
|
+
|
|
286
|
+
The build also writes `public/sitemap.xml` (every post with a `<lastmod>` from
|
|
287
|
+
its `date`, and the index dated to the newest post), `public/robots.txt`
|
|
288
|
+
pointing crawlers at it, and `public/feed.xml` (an RSS 2.0 feed, newest post
|
|
289
|
+
first, linked from every page for autodiscovery). These use the base URL from
|
|
290
|
+
`views/layout.html.erb`, so set that before deploying; if the layout has no
|
|
291
|
+
`og:url`/canonical, the sitemap and feed are skipped and `robots.txt` omits the
|
|
292
|
+
`Sitemap:` line.
|
|
293
|
+
|
|
294
|
+
`views/404.md` is built to `public/404.html` (marked `noindex`, kept out of the
|
|
295
|
+
sitemap and feed) for hosts that serve it on a missing path. `parrot serve` does
|
|
296
|
+
the same locally, answering a missing path with that page and a 404 status.
|
|
297
|
+
|
|
298
|
+
`views/about.md` is built to `public/about.html`, which the layout's nav links
|
|
299
|
+
to from every page — put your bio and social links there. It takes the same
|
|
300
|
+
optional `title`/`description`/`lang` header as a post, is listed in the
|
|
301
|
+
sitemap, and stays out of the index listing and the feed. Delete the file (and
|
|
302
|
+
its nav link) if you don't want an About page.
|
|
303
|
+
|
|
304
|
+
`about`, `404`, `index`, `index2`, `index3`, …, `categories`, `category`, `now`,
|
|
305
|
+
`post`, `posts`, `note` and `notes` are reserved post names — the first few would overwrite Parrot's
|
|
306
|
+
own pages, the rest are kept free for pages of their own. `parrot post` refuses
|
|
307
|
+
such a title, and `parrot build` fails if one is in `views/posts/`.
|
|
308
|
+
|
|
309
|
+
## Development
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
$ ./bin/setup # install gems and the git hooks
|
|
313
|
+
$ bundle exec rspec # run the test suite
|
|
314
|
+
$ bundle exec rspec -f d # documentation format
|
|
315
|
+
$ bundle exec rubocop # lint (config in .rubocop.yml)
|
|
316
|
+
$ bundle exec rubocop -a # autocorrect safe offenses
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`bin/setup` runs `bundle install` and then points git at the versioned hooks
|
|
320
|
+
in `.githooks/` (`git config core.hooksPath .githooks`). Run it once after
|
|
321
|
+
cloning. From then on the `pre-commit` hook runs RuboCop and RSpec, and aborts
|
|
322
|
+
the commit if either fails.
|
|
323
|
+
|
|
324
|
+
Run the suite from a directory that has no `blog/` folder — some specs create
|
|
325
|
+
and delete `./blog`.
|
|
326
|
+
|
|
327
|
+
GitHub Actions (`.github/workflows/ci.yml`) runs RSpec and RuboCop on every pull
|
|
328
|
+
request and on pushes to `master`.
|
|
329
|
+
|
|
330
|
+
## Contributing
|
|
331
|
+
|
|
332
|
+
1. Fork it
|
|
333
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
334
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
335
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
336
|
+
5. Open a Pull Request
|
|
337
|
+
|
|
338
|
+
## License
|
|
339
|
+
|
|
340
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
3
|
+
|
|
4
|
+
RSpec::Core::RakeTask.new(:run_version_spec) do |t|
|
|
5
|
+
# Specify the path to the specific test file you want to run
|
|
6
|
+
t.pattern = 'spec/parrot/version_spec.rb'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
desc 'Bump version'
|
|
10
|
+
task :bump_patch_version do
|
|
11
|
+
require_relative 'lib/parrot/version'
|
|
12
|
+
puts("Current version = #{Parrot::VERSION}")
|
|
13
|
+
major, minor, patch = Parrot::VERSION.split('.').map(&:to_i)
|
|
14
|
+
patch += 1
|
|
15
|
+
new_version = [major, minor, patch].join('.')
|
|
16
|
+
puts("New version = #{new_version}")
|
|
17
|
+
# Update version file
|
|
18
|
+
contents = File.read('lib/parrot/version.rb')
|
|
19
|
+
contents.sub!(Parrot::VERSION, new_version)
|
|
20
|
+
File.write('lib/parrot/version.rb', contents)
|
|
21
|
+
# Update version spec
|
|
22
|
+
contents = File.read('spec/parrot/version_spec.rb')
|
|
23
|
+
contents.sub!(Parrot::VERSION, new_version)
|
|
24
|
+
File.write('spec/parrot/version_spec.rb', contents)
|
|
25
|
+
Rake::Task['run_version_spec'].invoke
|
|
26
|
+
end
|
data/bin/setup
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
def run(command)
|
|
4
|
+
puts "== #{command}"
|
|
5
|
+
system(command) || abort("Setup failed: #{command}")
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
run('bundle install')
|
|
9
|
+
# Point git at the versioned hooks in .githooks (pre-commit runs RuboCop and RSpec).
|
|
10
|
+
run('git config core.hooksPath .githooks')
|
|
11
|
+
puts 'Setup complete.'
|
data/exe/parrot
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require File.expand_path('../../lib/parrot.rb', File.realpath(__FILE__))
|
|
7
|
+
Parrot::Parrot.new(ARGV).run
|
|
8
|
+
rescue StandardError => e
|
|
9
|
+
warn "Fatal error: #{e.message}"
|
|
10
|
+
warn e.backtrace.join("\n")
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
data/lib/helpers.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Parrot
|
|
2
|
+
module Helpers
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def testing?
|
|
6
|
+
ENV['PARROT_TESTING'] == 'true'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# "My First Post!" -> "my-first-post". Used for post filenames and for
|
|
10
|
+
# category page names, so it only ever yields [a-z0-9-].
|
|
11
|
+
def slugify(text)
|
|
12
|
+
text.downcase
|
|
13
|
+
.gsub(/[^a-z0-9\s-]/, '')
|
|
14
|
+
.strip
|
|
15
|
+
.gsub(/\s+/, '-')
|
|
16
|
+
.gsub(/-+/, '-')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|