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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.githooks/pre-commit +17 -0
  3. data/.github/workflows/ci.yml +29 -0
  4. data/.gitignore +24 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +28 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/CLAUDE.md +8 -0
  10. data/Gemfile +13 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +340 -0
  13. data/Rakefile +26 -0
  14. data/bin/setup +11 -0
  15. data/exe/parrot +12 -0
  16. data/lib/helpers.rb +19 -0
  17. data/lib/parrot/commands/build.rb +1274 -0
  18. data/lib/parrot/commands/new.rb +25 -0
  19. data/lib/parrot/commands/post.rb +83 -0
  20. data/lib/parrot/commands/serve.rb +129 -0
  21. data/lib/parrot/constants.rb +45 -0
  22. data/lib/parrot/file_cache.rb +45 -0
  23. data/lib/parrot/logger.rb +23 -0
  24. data/lib/parrot/metadata.rb +4 -0
  25. data/lib/parrot/runner.rb +29 -0
  26. data/lib/parrot/template_handler.rb +19 -0
  27. data/lib/parrot.rb +120 -0
  28. data/parrot.gemspec +36 -0
  29. data/skel/.gitignore +3 -0
  30. data/skel/config.yaml +68 -0
  31. data/skel/css/app.scss +189 -0
  32. data/skel/images/.keep +0 -0
  33. data/skel/images/apple-touch-icon.png +0 -0
  34. data/skel/images/favicon.ico +0 -0
  35. data/skel/images/favicon.svg +40 -0
  36. data/skel/images/parrot.jpeg +0 -0
  37. data/skel/javascripts/app.js +37 -0
  38. data/skel/public/.keep +0 -0
  39. data/skel/views/404.md +7 -0
  40. data/skel/views/about.md +17 -0
  41. data/skel/views/layout.html.erb +72 -0
  42. data/skel/views/posts/about_parrot.md +63 -0
  43. data/skel/views/posts/sample.md +37 -0
  44. data/spec/parrot/commands/build_spec.rb +747 -0
  45. data/spec/parrot/commands/new_spec.rb +43 -0
  46. data/spec/parrot/commands/post_spec.rb +80 -0
  47. data/spec/parrot/commands/serve_spec.rb +66 -0
  48. data/spec/parrot/logger_spec.rb +39 -0
  49. data/spec/parrot/metadata_spec.rb +11 -0
  50. data/spec/parrot/runner_spec.rb +28 -0
  51. data/spec/parrot_spec.rb +37 -0
  52. data/spec/spec_helper.rb +7 -0
  53. 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
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
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