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
@@ -0,0 +1,25 @@
1
+ module Parrot
2
+ module Commands
3
+ # New command is to create a new html app
4
+ # @usage parrot new myblog
5
+ class NewCommand
6
+ attr_reader :config, :app_root
7
+
8
+ def initialize(args = [], config)
9
+ @config = config
10
+ @app_root = args.first
11
+ raise ArgumentError if @app_root.nil? || @config.nil?
12
+ end
13
+
14
+ def run
15
+ raise "Directory #{@app_root} already exists" if Dir.exist? @app_root
16
+
17
+ config.logger.info "Creating new blog at #{app_root}"
18
+ skeleton_path = File.expand_path('../../../skel', __dir__)
19
+
20
+ config.logger.info "Copying skeleten from #{skeleton_path}"
21
+ FileUtils.cp_r(skeleton_path, @app_root)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,83 @@
1
+ require 'date'
2
+ require 'optparse'
3
+
4
+ module Parrot
5
+ module Commands
6
+ # Post command scaffolds a new Markdown post. The index page's listing is
7
+ # generated at build time from every file in views/posts, so nothing here
8
+ # needs to link it in.
9
+ # @usage parrot post --title "My first post"
10
+ # Runs from the blog's root, like `build` and `serve`.
11
+ class PostCommand
12
+ USAGE = 'Usage: parrot post --title "My new post"'.freeze
13
+
14
+ attr_reader :config, :app_root, :title, :slug
15
+
16
+ def initialize(args = [], config)
17
+ @config = config
18
+ @title = extract_title(Array(args)).strip
19
+ @app_root = @config&.root_dir
20
+ raise ArgumentError, USAGE if @title.empty?
21
+ raise ArgumentError if @app_root.nil?
22
+
23
+ @slug = Helpers.slugify(@title)
24
+ end
25
+
26
+ def run
27
+ raise ArgumentError, "Title has no letters or digits to build a filename from.\n#{USAGE}" if slug.empty?
28
+ if slug.match?(RESERVED_POST_NAMES)
29
+ raise ArgumentError,
30
+ "\"#{slug}\" is a reserved post name (index*, 404, about, categories, now, post(s), note(s)). " \
31
+ "Pick a different title.\n#{USAGE}"
32
+ end
33
+
34
+ posts_dir = File.join(app_root, 'views', 'posts')
35
+
36
+ raise "Run this from the blog's root (no views/posts found)" unless Dir.exist?(posts_dir)
37
+
38
+ post_path = File.join(posts_dir, "#{slug}.md")
39
+ raise "Post #{post_path} already exists" if File.exist?(post_path)
40
+
41
+ File.write(post_path, post_template(Date.today.strftime('%d/%m/%Y')))
42
+ config.logger.info "Created #{post_path}"
43
+ end
44
+
45
+ private
46
+
47
+ # The title comes from `--title`. Any words left over after the option are
48
+ # treated as part of the title too, so all of these give "My new post":
49
+ # parrot post --title "My new post"
50
+ # parrot post --title 'My new post'
51
+ # parrot post --title My new post
52
+ def extract_title(args)
53
+ from_option = []
54
+ rest = args.dup
55
+
56
+ OptionParser.new do |opts|
57
+ opts.on('--title TITLE', 'Title for the new post') { |t| from_option << t }
58
+ end.parse!(rest)
59
+
60
+ (from_option + rest).join(' ')
61
+ rescue OptionParser::ParseError
62
+ raise ArgumentError, USAGE
63
+ end
64
+
65
+ # {post_date} is filled in at build time, formatted per
66
+ # config.yaml's post_date_format.on_post.
67
+ def post_template(header_date)
68
+ <<~MARKDOWN
69
+ <!--
70
+ title: #{title}
71
+ date: #{header_date}
72
+ lang: en
73
+ category:
74
+ -->
75
+
76
+ # {post_title}
77
+
78
+ Write your post here.
79
+ MARKDOWN
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,129 @@
1
+ require 'webrick'
2
+ require_relative '../template_handler'
3
+ require 'pp'
4
+ require 'watchr'
5
+ require_relative '../file_cache'
6
+ require_relative 'build'
7
+
8
+ module Parrot
9
+ module Commands
10
+ class ServeCommand
11
+ # Combined checksum of the last successful build, kept inside the build
12
+ # directory. It is regenerated output, so it is gitignored and must be
13
+ # excluded from anything that ships the build directory.
14
+ CHECKSUM_FILE = '.checksum'.freeze
15
+
16
+ # Serves the build directory like WEBrick's FileHandler, but answers a
17
+ # missing path with the built 404.html (still with a 404 status), the
18
+ # way static hosts do, instead of WEBrick's generic error page.
19
+ class FileHandler < WEBrick::HTTPServlet::FileHandler
20
+ def initialize(server, root, options = {}, default = WEBrick::Config::FileHandler)
21
+ super
22
+ @not_found_page = File.join(root, '404.html')
23
+ end
24
+
25
+ def service(req, res)
26
+ super
27
+ rescue WEBrick::HTTPStatus::NotFound
28
+ raise unless File.file?(@not_found_page)
29
+
30
+ res.status = 404
31
+ res['Content-Type'] = 'text/html; charset=utf-8'
32
+ res.body = File.read(@not_found_page)
33
+ end
34
+ end
35
+
36
+ attr_reader :config, :document_root, :app_root
37
+
38
+ def initialize(args = [], config)
39
+ @config = config
40
+ @args = args
41
+ @app_root = config.root_dir
42
+ @document_root = "#{app_root}/public"
43
+ @port = 8000
44
+ end
45
+
46
+ def run
47
+ Thread.new do
48
+ config.logger.info 'Running watch for changes'
49
+ watch_app
50
+ end
51
+
52
+ run_server
53
+ end
54
+
55
+ private
56
+
57
+ def run_server
58
+ server = WEBrick::HTTPServer.new Port: @port
59
+ server.mount('/', FileHandler, @document_root)
60
+
61
+ trap 'INT' do
62
+ server.shutdown
63
+ end
64
+
65
+ server.start
66
+ end
67
+
68
+ def watch_app
69
+ ENV['HANDLER'] = `uname`.strip
70
+ watcher = Watchr::Script.new
71
+ all_files = Dir.glob('**/*').select { |item| File.file?(item) && !item.start_with?('public/') }
72
+ @cache = FileCache.instance
73
+ builder = BuildCommand.new([], config)
74
+ builder.unset_build_mode!
75
+
76
+ all_files.each do |file|
77
+ absolute_path = File.join(app_root, file)
78
+ @cache.set(absolute_path)
79
+ end
80
+
81
+ build_if_stale(builder, @cache.checksum)
82
+
83
+ watcher.watch(all_files.join('|')) do |file|
84
+ config.logger.info "File changed #{file}"
85
+ path = "#{app_root}/#{file}"
86
+
87
+ if @cache.changed? path
88
+ @cache.set(path)
89
+ builder.build(file)
90
+ end
91
+ end
92
+
93
+ controller = Watchr::Controller.new(watcher, Watchr.handler.new)
94
+ controller.run
95
+ rescue Exception => e # rubocop:disable Lint/RescueException -- also log Ctrl-C/exit from the watcher loop
96
+ config.logger.error("#{e.message}\n#{e.backtrace.join("\n")}")
97
+ exit(-1)
98
+ end
99
+
100
+ # Rebuild the whole blog only when the source tree as a whole has moved
101
+ # since the last build. The given FileCache checksum is compared with the
102
+ # one stored in the build directory; a missing or mismatched file triggers
103
+ # a full build, after which the stored checksum is refreshed.
104
+ def build_if_stale(builder, checksum)
105
+ checksum_path = File.join(document_root, CHECKSUM_FILE)
106
+ stored = File.read(checksum_path).strip if File.exist?(checksum_path)
107
+
108
+ if stored == checksum
109
+ config.logger.info 'No source changes since last build, skipping full build'
110
+ return
111
+ end
112
+
113
+ builder.run
114
+ File.write(checksum_path, checksum)
115
+ config.logger.info "Wrote build checksum to #{checksum_path}"
116
+ end
117
+
118
+ def handle_path(path)
119
+ path.chop! while path[-1] == '/'
120
+
121
+ path = '/index.html' if path == ''
122
+
123
+ template_type = path.split('.').last.to_sym
124
+ handler = TemplateHandler.new(root: document_root, path: path, handle: template_type)
125
+ handler.compile
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,45 @@
1
+ MATHJAX_URL = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'.freeze
2
+
3
+ # Rouge theme used for syntax highlighting of code blocks in posts.
4
+ # Any theme name from Rouge::Themes works here (e.g. "monokai", "github",
5
+ # "gruvbox", "colorful", "molokai", "thankful_eyes").
6
+ HIGHLIGHT_THEME = 'monokai'.freeze
7
+
8
+ # `{post_<key>}` expands to that key from the post's header (so
9
+ # `{post_title}`, `{post_lang}`, or any custom header field), as plain
10
+ # text; `{post_date}` is the same header field but run through
11
+ # `post_date_format.on_list` below rather than shown as-authored.
12
+ # `{post_link}` is the one field Parrot computes itself rather than
13
+ # reading from the header: the post's href. Wrap whichever span should
14
+ # be clickable in ordinary Markdown link syntax, [...]({post_link}).
15
+ # `{post_category_tag}` is the post's header `category` as a link to that
16
+ # category's listing (`<a class="category-tag">`), or nothing when the post
17
+ # has no category. This default links the title and shows that tag:
18
+ # "{post_date} ~ [{post_title}]({post_link}) {post_category_tag}"
19
+ # To make the whole line a link instead:
20
+ # "[{post_date} ~ {post_title}]({post_link})"
21
+ # A bare strftime format string like {%d/%m/%Y} also still works here,
22
+ # shown exactly as formatted rather than through post_date_format.
23
+ DEFAULT_LIST_FORMAT = '{post_date} ~ [{post_title}]({post_link}) {post_category_tag}'.freeze
24
+ DEFAULT_GROUP_BY = 'none'.freeze
25
+ DEFAULT_LIST_TITLE = 'Post listing'.freeze
26
+ DEFAULT_BACK_LINK_TEXT = '← Back to all posts'.freeze
27
+ DEFAULT_NEWER_LINK_TEXT = '← Newer posts'.freeze
28
+ DEFAULT_OLDER_LINK_TEXT = 'Older posts →'.freeze
29
+
30
+ # `{post_date}` is the one header field with its own dedicated config,
31
+ # config.yaml's `post_date_format` — a Ruby strftime format string (see
32
+ # Date#strftime) per context: `on_list` when `{post_date}` appears in a
33
+ # post_listing `list_format`, `on_post` when it appears as a literal
34
+ # placeholder inside a post's own Markdown body.
35
+ DEFAULT_POST_DATE_FORMAT = { 'on_list' => '%m/%Y', 'on_post' => '%d/%m/%Y' }.freeze
36
+
37
+ # Post filenames (without .md) that `parrot post` refuses to create and
38
+ # `parrot build` fails on. index*, 404 and about would build to the same
39
+ # public/*.html as a page Parrot generates itself (the index pages,
40
+ # 404.html, about.html, categories.html); category, now, post(s) and note(s)
41
+ # are kept free for pages of their own. A post named like a category page
42
+ # (category-<slug>.html) is caught by the build instead, since those names
43
+ # depend on which categories exist. Case-insensitive, since macOS and
44
+ # Windows filesystems are.
45
+ RESERVED_POST_NAMES = /\A(?:index\d*|404|about|categor(?:y|ies)|now|posts?|notes?)\z/i
@@ -0,0 +1,45 @@
1
+ require 'digest'
2
+ require 'singleton'
3
+
4
+ class FileCache
5
+ include Singleton
6
+
7
+ attr_accessor :cache
8
+
9
+ # Digest over the combined state of every tracked file. Recomputed on
10
+ # every #set, so a single value tells you whether the cache as a whole
11
+ # has moved.
12
+ attr_reader :checksum
13
+
14
+ def initialize
15
+ @cache = {}
16
+ @checksum = compute_checksum
17
+ end
18
+
19
+ def fetch(path)
20
+ cache[path]
21
+ end
22
+
23
+ def set(path)
24
+ digest = Digest::SHA256.hexdigest(File.read(path))
25
+ cache[path] = digest
26
+ @checksum = compute_checksum
27
+ digest
28
+ end
29
+
30
+ def changed?(path)
31
+ return true unless cache.key?(path)
32
+
33
+ data = File.read(path)
34
+ cache[path] != Digest::SHA256.hexdigest(data)
35
+ end
36
+
37
+ private
38
+
39
+ # Hash the whole cache. Entries are sorted by path so the result depends
40
+ # only on the contents, not on insertion order.
41
+ def compute_checksum
42
+ payload = cache.sort.map { |path, sha| "#{path}:#{sha}" }.join("\n")
43
+ Digest::SHA256.hexdigest(payload)
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ require 'logger'
2
+
3
+ module Parrot
4
+ class ParrotLoggerBuilder
5
+ attr_reader :logger, :device
6
+
7
+ def initialize(quiet = false)
8
+ @device = if Helpers.testing?
9
+ File.new('parrot.test.log', 'a+')
10
+ else
11
+ quiet ? File.new('parrot.log', 'a+') : $stdout
12
+ end
13
+
14
+ # shift_age = 10, keep 10 log files
15
+ # shift_size = 1048576, maximum 1 MB of log file size
16
+ @logger = Logger.new(@device, 10, 1_048_576, progname: 'ParrotLogger')
17
+ end
18
+
19
+ def log(message)
20
+ @logger.info(message)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ module Parrot
2
+ VERSION = '0.3.0'.freeze
3
+ HOMEPAGE = 'https://github.com/deepakkumarnd/parrot'.freeze
4
+ end
@@ -0,0 +1,29 @@
1
+ require 'fileutils'
2
+ require_relative 'commands/new'
3
+ require_relative 'commands/build'
4
+ require_relative 'commands/serve'
5
+ require_relative 'commands/post'
6
+
7
+ module Parrot
8
+ class Runner
9
+ include Commands
10
+
11
+ attr_reader :command
12
+
13
+ def initialize(command, args = [], config)
14
+ @config = config
15
+ klass = to_command_class(command)
16
+ @command = klass.new(args, @config)
17
+ end
18
+
19
+ def run_command
20
+ @command.run
21
+ end
22
+
23
+ private
24
+
25
+ def to_command_class(command)
26
+ Commands.const_get("#{command.capitalize}Command")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ module Parrot
2
+ class TemplateHandler
3
+ HANDLERS = {
4
+ html: [:slim],
5
+ css: [:scss],
6
+ js: [:coffee]
7
+ }.freeze
8
+
9
+ def initialize(options = {})
10
+ @root = options[:root]
11
+ @path = options[:path]
12
+ @handler = HANDLERS[options[:handle]]
13
+ end
14
+
15
+ def handler_engines(type)
16
+ HANDLERS[type]
17
+ end
18
+ end
19
+ end
data/lib/parrot.rb ADDED
@@ -0,0 +1,120 @@
1
+ require 'optparse'
2
+
3
+ require_relative 'helpers'
4
+ require_relative 'parrot/metadata'
5
+ require_relative 'parrot/runner'
6
+ require_relative 'parrot/logger'
7
+ require_relative 'parrot/constants'
8
+
9
+ module Parrot
10
+ Config = Struct.new(:root_dir, :logger, :build_mode)
11
+
12
+ SubcommandEntry = Struct.new(:usage, :docstr)
13
+
14
+ SUB_COMMANDS_DOC = {
15
+ new: SubcommandEntry.new('new <blog_name>', 'Create new blog'),
16
+ build: SubcommandEntry.new('build', 'Build the blog'),
17
+ serve: SubcommandEntry.new('serve', 'Start development server locally'),
18
+ post: SubcommandEntry.new('post --title <post title>', 'Add new post with a title')
19
+ }.freeze
20
+
21
+ USAGE_LINE = 'parrot [options] [subcommand] [args]'.freeze
22
+
23
+ HELP_TEXT =
24
+ <<~HELP_TEXT.freeze
25
+ Examples:
26
+ - Create new blog
27
+ parrot new blog
28
+ #{' '}
29
+ - Start development server
30
+ cd blog
31
+ parrot serve
32
+
33
+ - Add a new post
34
+ parrot post --title "My first post"
35
+
36
+ - Build the blog
37
+ parrot build
38
+ HELP_TEXT
39
+
40
+ HELP_HEADER =
41
+ <<~HEADER_TEXT.freeze
42
+ Version: #{Parrot::VERSION}
43
+ Usage: #{USAGE_LINE}
44
+ Repository: #{Parrot::HOMEPAGE}
45
+ Repository: #{Parrot::HOMEPAGE}/blob/master/README.md
46
+ HEADER_TEXT
47
+
48
+ class Parrot
49
+ SUB_COMMANDS = SUB_COMMANDS_DOC.keys.map(&:to_s).freeze
50
+
51
+ attr_accessor :root_dir, :logger, :config
52
+
53
+ def initialize(args = [])
54
+ @options = { quiet: Helpers.testing? || false }
55
+ extract_options!(args)
56
+ @command = args.shift
57
+ @args = args
58
+ @logger = ParrotLoggerBuilder.new(@options[:quiet]).logger
59
+ @root_dir = Dir.pwd
60
+ @config = Config.new(@root_dir, @logger)
61
+ end
62
+
63
+ def run
64
+ return if @command.nil?
65
+
66
+ exit_if_invalid(@command)
67
+ Runner.new(@command, @args, config).run_command
68
+ rescue ArgumentError => e
69
+ # A command was called with missing or malformed arguments; show how to
70
+ # call it instead of dumping a backtrace.
71
+ warn(e.message)
72
+ exit(1)
73
+ end
74
+
75
+ def exit_if_invalid(command)
76
+ return if SUB_COMMANDS.include?(command)
77
+
78
+ puts('That is not a valid command. View detailed help with parrot -h')
79
+ puts USAGE_LINE
80
+ exit!
81
+ end
82
+
83
+ def quiet?
84
+ @options[:quiet]
85
+ end
86
+
87
+ def usage(parser = nil)
88
+ max_length = SUB_COMMANDS_DOC.map { |_k, v| v.usage.length }.max + 10
89
+ sub_command_doc = SUB_COMMANDS_DOC.map do |_command, entry|
90
+ entry_text = entry.usage.to_s.ljust(max_length)
91
+ "#{entry_text}#{entry.docstr}"
92
+ end.join("\n")
93
+
94
+ sub_command_doc = "Sub Commands:\n#{sub_command_doc}\n"
95
+ line_sep = "#{'-' * 80}\n"
96
+ [
97
+ HELP_HEADER,
98
+ parser,
99
+ sub_command_doc,
100
+ HELP_TEXT
101
+ ].join(line_sep)
102
+ end
103
+
104
+ private
105
+
106
+ def extract_options!(args)
107
+ OptionParser.new("Usage: #{USAGE_LINE}") do |parser|
108
+ parser.on('-q', '--quiet', 'Quiet mode') { @options[:quiet] = true }
109
+ parser.on_tail('-v', '--version', 'Prints version information') do
110
+ puts("Parrot: #{VERSION}")
111
+ end
112
+ parser.on_tail('-h', '--help', 'Prints usage instruction') do
113
+ puts usage(parser)
114
+ end
115
+ end.order!(args)
116
+ # Stop at the first non-option (the sub-command) so flags that belong to
117
+ # the sub-command, e.g. `parrot post --title "..."`, are left untouched.
118
+ end
119
+ end
120
+ end
data/parrot.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ require_relative 'lib/parrot/metadata'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'prt'
5
+ spec.version = Parrot::VERSION
6
+ spec.authors = ['Deepak Kumar']
7
+ spec.email = ['deepakkumarnd@gmail.com']
8
+ spec.summary = 'A static markdown blog builder written in ruby for minimalist bloggers'
9
+ spec.description = 'A static blogging tool written in ruby, posts are in markdown format'
10
+ spec.homepage = Parrot::HOMEPAGE
11
+ spec.license = 'MIT'
12
+
13
+ # Runtime Dependencies
14
+ spec.add_dependency 'kramdown'
15
+ spec.add_dependency 'kramdown-parser-gfm'
16
+ spec.add_dependency 'logger'
17
+ spec.add_dependency 'nokogiri'
18
+ spec.add_dependency 'observer'
19
+ spec.add_dependency 'rouge'
20
+ spec.add_dependency 'sassc'
21
+ spec.add_dependency 'tilt'
22
+ spec.add_dependency 'watchr'
23
+ spec.add_dependency 'webrick'
24
+
25
+ # File Management
26
+ spec.bindir = 'exe'
27
+ spec.executables = 'parrot'
28
+ spec.files = `git ls-files`.split("\n")
29
+ spec.require_paths = ['lib']
30
+ spec.required_ruby_version = '>= 3.2'
31
+
32
+ spec.post_install_message = <<~MESSAGE
33
+ Refer to readme at README.md to know how to get started with Parrot
34
+ MESSAGE
35
+ spec.metadata['rubygems_mfa_required'] = 'true'
36
+ end
data/skel/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ # Incremental-build checksum. Regenerated on every `parrot serve`; never
2
+ # commit it and never ship it with the built site.
3
+ public/.checksum
data/skel/config.yaml ADDED
@@ -0,0 +1,68 @@
1
+ # Settings for the post listing Parrot generates on the index page from
2
+ # everything in views/posts/*.md. There is no views/index.md to hand-edit —
3
+ # the listing is always built from the posts themselves, newest first.
4
+ #
5
+ # Posts with a `category: Name` header also get a listing page per category
6
+ # (category-name.html, then category-name_2.html, … once it outgrows
7
+ # per_page), all linked from categories.html. Those pages use the same
8
+ # group_by, per_page, pager text and list_format as the index.
9
+ post_listing:
10
+ # The index page's <h1>. Set to "" to omit the heading entirely.
11
+ list_title: "Post listing"
12
+
13
+ # How to group posts in the listing.
14
+ # none - a single flat list
15
+ # year - a "## <year>" heading per year
16
+ # month - a "## <Month Year>" heading per month
17
+ group_by: none
18
+
19
+ # Text for the link back to the index shown at the top of every post. It
20
+ # always points at whichever index page that post actually appears on.
21
+ # Set to "" to omit the link entirely.
22
+ back_link_text: "← Back to all posts"
23
+
24
+ # Maximum number of posts on one index page. Once there are more posts
25
+ # than this, Parrot splits the listing across index.html (newest posts),
26
+ # index2.html, index3.html, etc., each with a pager linking to the next
27
+ # and previous page. Leave unset (or 0) to keep every post on index.html.
28
+ # per_page: 10
29
+
30
+ # Text for the pager links at the bottom of a paginated index page. Set
31
+ # either to "" to omit that link (e.g. on the first/last page, where it
32
+ # wouldn't point anywhere anyway).
33
+ newer_link_text: "← Newer posts"
34
+ older_link_text: "Older posts →"
35
+
36
+ # Markdown format string for each post's list entry.
37
+ # {post_<key>} - that key from the post's header, as plain text:
38
+ # {post_title}, {post_lang}, or any custom field you add
39
+ # to a post's <!-- key: value --> header
40
+ # {post_date} - the header's `date`, formatted per post_date_format.on_list
41
+ # below rather than shown as-authored
42
+ # {post_category_tag} - the post's `category` as a link to its category
43
+ # page (class "category-tag"); empty for posts without one
44
+ # {post_link} - the post's href, the one field Parrot computes itself
45
+ # rather than reading from the header — wrap whichever
46
+ # part should be clickable in Markdown link syntax
47
+ # yourself: [...]({post_link})
48
+ # {...} - anything else is a Ruby strftime format string (see
49
+ # Ruby's Date#strftime) applied to the post's header
50
+ # `date`, shown as-is rather than through
51
+ # post_date_format.on_list, e.g. {%A, %B %d %Y}
52
+ #
53
+ # Link just the title and show the category (default):
54
+ # list_format: "{post_date} ~ [{post_title}]({post_link}) {post_category_tag}"
55
+ # Link the whole line instead:
56
+ # list_format: "[{post_date} ~ {post_title}]({post_link})"
57
+ list_format: "{post_date} ~ [{post_title}]({post_link}) {post_category_tag}"
58
+
59
+ # How to format a post's header `date` wherever it's shown via the
60
+ # {post_date} token — as opposed to a bare strftime pattern like {%d/%m/%Y},
61
+ # which is already a fixed format. A Ruby strftime format string (see
62
+ # Ruby's Date#strftime), same as list_format's bare {...} patterns.
63
+ post_date_format:
64
+ # Used for {post_date} inside post_listing's list_format above.
65
+ on_list: "%m/%Y"
66
+ # Used for a literal {post_date} placeholder inside a post's own Markdown
67
+ # body — `parrot post` writes new posts with one under the title.
68
+ on_post: "%d/%m/%Y"