postwave 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f460f1e6202c2d56b2c355266fd8819691cdb2a8f12b6aa33febd8a0c6b5d580
4
+ data.tar.gz: 5159c6aa7c213bfff6ec6f1a2f3fdb8c4f1ef8f03dbe9ff5014b93709ee8c625
5
+ SHA512:
6
+ metadata.gz: 5102d693e92f76288dbdf152f00cfdf6a386e0b4a27bb4415ba8baccdd13d64b04e624f136bb993046bf7b71facbf5d78848716aa8c146abe6a1f9d006b91282
7
+ data.tar.gz: cfbb408e719c8a56e54ef81614ae5e7a7a362e1b3f7f863e4e462d449dfab3188beca5e4536fe13381097c1eb14d8b43842c9108d5429f44909aaf87a765961a
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in trackstar.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # Postwave 🌊
2
+
3
+ Write your posts statically. Interact with them dynamically.
4
+
5
+ ## What is Postwave?
6
+
7
+ Postwave is an opinionated flat-file based based blog engine.
8
+
9
+ It lets you write posts in Markdown and then display them on a dynamic site using a client library.
10
+
11
+ ## Getting Started
12
+
13
+ ### Installation
14
+
15
+ ```
16
+ gem install postwave
17
+ ```
18
+
19
+ ### Setup
20
+
21
+ ```
22
+ > postwave new
23
+ ```
24
+
25
+ Run this from the root directory of your project. It will create a `postwave.yaml` config file in the current directory and a `/_posts/` directory. This is where you will write your posts.
26
+
27
+ Here is what will be created:
28
+
29
+ ```
30
+ |- _posts/
31
+ | |- meta/
32
+ | |- tags/
33
+ | |- index.csv
34
+ | |- summary.yaml
35
+ postwave.yaml
36
+ ```
37
+
38
+ `_posts/`: This is where you write all your posts in Markdown
39
+
40
+ `_posts/meta/tags/`: This will contain files for every tag your define in your posts
41
+
42
+ `_posts/meta/index.csv`: This will contain an ordered list of all the posts
43
+
44
+ `_posts/meta/summary.yaml`: This file will contain some summary information about the posts. Total count, etc.
45
+
46
+ `postwave.yaml`: The config file for Postwave.
47
+
48
+ ### Create A New Blog Post
49
+
50
+ ```
51
+ > postwave post
52
+ ```
53
+
54
+ This will generate at new Markdown file in the `_posts/` directory. The filename will be the current timestamp. This will eventually be overwritten by the `build` command, so don't worry too much about it. The file will have a general structure like this:
55
+
56
+ ```
57
+ ---
58
+ title:
59
+ date: 2022-01-01
60
+ tags:
61
+ ---
62
+
63
+ Start writing!
64
+ ```
65
+ Tags should be comma separated.
66
+
67
+ You can keep a post in "draft" status (meaning it won't get processed or added to the index) by adding `draft: true` to the top section of the post.
68
+
69
+ ```
70
+ ---
71
+ title: This Post Isn't Quite Ready
72
+ date: 2022-01-01
73
+ tags:
74
+ draft: true
75
+ ---
76
+ ```
77
+
78
+ ### Build the Blog
79
+
80
+ ```
81
+ > postwave build
82
+ ```
83
+
84
+ This will "build" the blog. This involves:
85
+ - regenerating the `index.csv` file
86
+ - generating slugs for each posts based on the post title and ensuring that there are no duplicate slugs
87
+ - changing the post file names to match `yyyy-dd-mm-slug.md`
88
+ - updating the `summary.yaml`
89
+ - creating and updating tag files (which will be `/tags/[tag-name].yaml` files for each tag)
90
+
91
+ ## Available Client Libraries
92
+
93
+ - [Ruby](https://github.com/dorkrawk/postwave-ruby-client)
94
+
95
+ ## What is Postwave Not?
96
+
97
+ Postwave is not for everything.
98
+
99
+ It is not:
100
+ - for people who want to generate a purely static site
101
+ - for people who want unlimited customization
102
+ - for giant blogs with many many thousands of posts (maybe?)
103
+
104
+ ## Why did you build another blogging tool?
105
+
106
+ I don't know. I probably like writing blog engines more than I like writing blog posts.
107
+
108
+ I wanted something that would let me writing simple Markdown posts but still let me just embed the index and post content into a custom dynamic site. This scratched an itch and seemed like it would be fun to build.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'spec'
6
+ t.test_files = FileList['spec/*_spec.rb']
7
+ end
8
+
9
+ desc "Run tests"
10
+ task :default => :test
data/bin/postwave ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/postwave'
4
+ require 'optparse'
5
+
6
+ options = {}
7
+
8
+ subtext = <<HELP
9
+ Commands:
10
+ new : creates new Postwave project in the current directory
11
+ post : creates a new post in the /_posts/ directory
12
+ build : builds the posts and meta information for the blog
13
+ See 'postwave COMMAND --help' for more information on a specific command.
14
+ HELP
15
+
16
+ global = OptionParser.new do |opts|
17
+ opts.banner = "Usage: postwave [options] [command]"
18
+ opts.on '-v', '--version', 'Show version' do |v|
19
+ options[:version] = v
20
+ end
21
+ opts.separator ""
22
+ opts.separator subtext
23
+ end
24
+
25
+ subcommands = {
26
+ 'new' => OptionParser.new do |opts|
27
+ opts.banner = "Usage: new"
28
+ end,
29
+ 'post' => OptionParser.new do |opts|
30
+ opts.banner = "Usage: post"
31
+ end,
32
+ 'build' => OptionParser.new do |opts|
33
+ opts.banner = "Usage: build [options]"
34
+ opts.on("-q", "--quiet", "quietly run ") do |v|
35
+ options[:quiet] = v
36
+ end
37
+ end
38
+ }
39
+
40
+ global.order!
41
+ command = ARGV.shift
42
+ subcommands[command].order! if command
43
+
44
+ Postwave.call(command, options)
@@ -0,0 +1,105 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+ require "singleton"
4
+ require 'csv'
5
+ require 'time'
6
+ require_relative "blog_utilities"
7
+ require_relative "display_helper"
8
+ require_relative "post"
9
+
10
+ module Postwave
11
+ class BlogBuilder
12
+ include Singleton
13
+ include BlogUtilities
14
+ include DisplayHelper
15
+
16
+ INDEX_HEADERS = ["slug", "date", "title"]
17
+
18
+ def build
19
+ start = Time.now
20
+
21
+ output_building
22
+
23
+ if !is_set_up?
24
+ output_missing_setup
25
+ return
26
+ end
27
+
28
+ # load, rename, and sort post file names
29
+ posts = load_posts
30
+ posts = ensure_unique_slugs(posts).sort_by { |p| p.date }.reverse
31
+ draft_posts, published_posts = posts.partition { |p| p.respond_to?(:draft) ? p.draft : false }
32
+ tags = {}
33
+
34
+ CSV.open(File.join(Dir.pwd, POSTS_DIR, META_DIR, INDEX_FILE_NAME), "w") do |csv|
35
+ csv << INDEX_HEADERS
36
+ published_posts.each do |post|
37
+ post.update_file_name!
38
+
39
+ csv << [post.slug, post.date, post.title]
40
+
41
+ post.tags.each do |tag|
42
+ if tags.has_key? tag
43
+ tags[tag] << post.slug
44
+ else
45
+ tags[tag] = [post.slug]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ output_post_processed(published_posts)
51
+ output_drafts_skipped(draft_posts)
52
+
53
+ build_tags_files(tags)
54
+ build_summary(published_posts, tags)
55
+
56
+ build_time = Time.now - start
57
+ output_build_completed(build_time)
58
+ end
59
+
60
+ def load_posts
61
+ posts = []
62
+ Dir.glob(File.join(Dir.pwd, POSTS_DIR, "*.md")) do |post_file_path|
63
+ posts << Postwave::Post.new_from_file_path(post_file_path)
64
+ end
65
+ posts
66
+ end
67
+
68
+ def ensure_unique_slugs(posts)
69
+ slug_count = {}
70
+
71
+ posts.sort_by { |p| p.date }.each do |post|
72
+ title_slug = post.title_slug
73
+ if slug_count.key?(title_slug)
74
+ slug_count[title_slug] += 1
75
+ post.slug = "#{title_slug}-#{slug_count[title_slug]}"
76
+ else
77
+ slug_count[title_slug] = 0
78
+ end
79
+ end
80
+
81
+ posts
82
+ end
83
+
84
+ def build_tags_files(tags)
85
+ tags.each do |tag, post_slugs|
86
+ tag_info = {
87
+ count: post_slugs.count,
88
+ post_slugs: post_slugs
89
+ }
90
+ File.write(File.join(Dir.pwd, POSTS_DIR, META_DIR, TAGS_DIR, "#{tag}.yaml"), tag_info.to_yaml)
91
+ end
92
+ output_tags_created(tags)
93
+ end
94
+
95
+ def build_summary(posts, tags)
96
+ summary = {
97
+ post_count: posts.count,
98
+ most_recent_file_name: posts.first.file_name,
99
+ most_recent_date: posts.first.date,
100
+ tags: tags.keys
101
+ }
102
+ File.write(File.join(Dir.pwd, POSTS_DIR, META_DIR, SUMMARY_FILE_NAME), summary.to_yaml)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,49 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+ require "singleton"
4
+ require_relative "blog_utilities"
5
+ require_relative "display_helper"
6
+
7
+ module Postwave
8
+ class BlogCreator
9
+ include Singleton
10
+ include BlogUtilities
11
+ include DisplayHelper
12
+
13
+ def create
14
+ output_creating_blog
15
+
16
+ if is_set_up?
17
+ output_exising_setup
18
+ return
19
+ end
20
+
21
+ build_directories
22
+ build_files
23
+ write_initial_summary_contents
24
+
25
+ output_blog_created
26
+ end
27
+
28
+ def build_directories
29
+ directory_paths.each do |path|
30
+ FileUtils.mkdir_p(path)
31
+ end
32
+ end
33
+
34
+ def build_files
35
+ file_paths.each do |path|
36
+ FileUtils.touch(path)
37
+ end
38
+ end
39
+
40
+ def write_initial_summary_contents
41
+ summary = {
42
+ post_count: 0,
43
+ tags: []
44
+ }
45
+
46
+ File.write(File.join(Dir.pwd, POSTS_DIR, META_DIR, SUMMARY_FILE_NAME), summary.to_yaml)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,40 @@
1
+ module Postwave
2
+ module BlogUtilities
3
+ CONFIG_FILE_NAME = "postwave.yaml"
4
+ INDEX_FILE_NAME = "index.csv"
5
+ SUMMARY_FILE_NAME = "summary.yaml"
6
+ POSTS_DIR = "_posts"
7
+ META_DIR = "meta"
8
+ TAGS_DIR = "tags"
9
+
10
+ def is_set_up?
11
+ missing_paths = find_missing_paths
12
+ missing_paths.empty?
13
+ end
14
+
15
+ def file_paths
16
+ [
17
+ File.join(Dir.pwd, CONFIG_FILE_NAME),
18
+ File.join(Dir.pwd, POSTS_DIR, META_DIR, INDEX_FILE_NAME),
19
+ File.join(Dir.pwd, POSTS_DIR, META_DIR, SUMMARY_FILE_NAME),
20
+ ]
21
+ end
22
+
23
+ def directory_paths
24
+ [
25
+ File.join(Dir.pwd, POSTS_DIR),
26
+ File.join(Dir.pwd, POSTS_DIR, META_DIR),
27
+ File.join(Dir.pwd, POSTS_DIR, META_DIR, TAGS_DIR),
28
+ ]
29
+ end
30
+
31
+ def find_missing_paths
32
+ paths_to_check = directory_paths + file_paths
33
+ missing_paths = []
34
+ paths_to_check.each do |path|
35
+ missing_paths << path if !FileTest.exists?(path)
36
+ end
37
+ missing_paths
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,80 @@
1
+ module Postwave
2
+ module DisplayHelper
3
+
4
+ # new
5
+
6
+ def output_creating_blog
7
+ puts "🌊 Creating new blog..."
8
+ end
9
+
10
+ def output_blog_created
11
+ puts "New blog set up.".green
12
+ end
13
+
14
+ # post
15
+
16
+ def output_creating_post
17
+ puts "🌊 Creating new post..."
18
+ end
19
+
20
+ def output_post_created(post_path)
21
+ puts "New post created at: #{post_path}".green
22
+ end
23
+
24
+ # build
25
+
26
+ def output_building
27
+ puts "🌊 Building..."
28
+ end
29
+
30
+ def output_post_processed(posts)
31
+ count = posts.count
32
+ puts "Processed #{count} #{simple_pluralizer("post", count)}."
33
+ end
34
+
35
+ def output_drafts_skipped(drafts)
36
+ if drafts.any?
37
+ count = drafts.count
38
+ puts "Skipped #{count} #{simple_pluralizer("draft", count)}."
39
+ end
40
+ end
41
+
42
+ def output_tags_created(tags)
43
+ count = tags.count
44
+ puts "Built tag files for #{count} #{simple_pluralizer("tag", count)}."
45
+ end
46
+
47
+ def output_build_completed(build_time)
48
+ puts "Built succesfully in #{build_time} seconds.".green
49
+ end
50
+
51
+ # errors
52
+
53
+ def output_exising_setup
54
+ puts "A blog already exists in this location.".red
55
+ end
56
+
57
+ def output_missing_setup
58
+ puts "You need to set up a blog first.".red
59
+ end
60
+
61
+ def output_general_error
62
+ puts "Something went wrong.".red
63
+ end
64
+
65
+ private
66
+ def simple_pluralizer(word, count)
67
+ if count == 1
68
+ word
69
+ else
70
+ word + "s"
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # 🐒 patch String to add terminal colors
77
+ class String
78
+ def red; "\e[31m#{self}\e[0m" end
79
+ def green; "\e[32m#{self}\e[0m" end
80
+ end
@@ -0,0 +1,89 @@
1
+ require_relative "blog_utilities"
2
+
3
+ module Postwave
4
+ class Post
5
+ include BlogUtilities
6
+
7
+ KNOWN_FIELDS = %w(title date tags title_slug body draft)
8
+ REQUIRED_FIELDS = %w(title date)
9
+ MEATADATA_DELIMTER = "---"
10
+
11
+ attr_accessor :file_name
12
+
13
+ def self.new_from_file_path(path)
14
+ metadata_delimter_count = 0
15
+ body_buffer_count = 0
16
+ field_content = { "body" => "" }
17
+
18
+ File.readlines(path).each do |line|
19
+ clean_line = line.strip
20
+ if clean_line == MEATADATA_DELIMTER
21
+ metadata_delimter_count += 1
22
+ next
23
+ end
24
+
25
+ if metadata_delimter_count == 0
26
+ next
27
+ elsif metadata_delimter_count == 1
28
+ field, value = clean_line.split(":", 2).map(&:strip)
29
+ field_content[field] = value
30
+ else
31
+ if body_buffer_count == 0
32
+ body_buffer_count += 1
33
+ next if clean_line.empty?
34
+ end
35
+
36
+ field_content["body"] += "#{line}\n"
37
+ end
38
+ end
39
+
40
+ # turn "tags" into an array
41
+ if field_content["tags"]
42
+ field_content["tags"] = field_content["tags"].split(",").map do |tag|
43
+ tag.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
44
+ end
45
+ end
46
+
47
+ # turn "draft" into boolean
48
+ if field_content["draft"]
49
+ field_content["draft"] = field_content["draft"].downcase == "true"
50
+ end
51
+
52
+ self.new(path, field_content)
53
+ end
54
+
55
+ def initialize(file_name, field_content = {})
56
+ @file_name = file_name
57
+
58
+ field_content.each do |field, value|
59
+ instance_variable_set("@#{field}", value)
60
+ self.class.send(:attr_accessor, field)
61
+ end
62
+ end
63
+
64
+ def title_slug
65
+ @title_slug ||= @title.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
66
+ end
67
+
68
+ def slug
69
+ @slug ||= @title_slug
70
+ end
71
+
72
+ def slug=(new_slug)
73
+ @slug = new_slug
74
+ end
75
+
76
+ def generated_file_name
77
+ # YYYY-MM-DD-slug-from-title.md
78
+ "#{@date[..9]}-#{slug}.md"
79
+ end
80
+
81
+ def update_file_name!
82
+ desired_file_name = generated_file_name
83
+ return false if @file_name == desired_file_name
84
+
85
+ File.rename(@file_name, File.join(Dir.pwd, POSTS_DIR, desired_file_name))
86
+ @file_name = desired_file_name
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,40 @@
1
+ require "fileutils"
2
+ require "singleton"
3
+ require_relative "blog_utilities"
4
+ require_relative "display_helper"
5
+
6
+
7
+ module Postwave
8
+ class PostCreator
9
+ include Singleton
10
+ include BlogUtilities
11
+ include DisplayHelper
12
+
13
+ def create
14
+ output_creating_post
15
+
16
+ if !is_set_up?
17
+ output_missing_setup
18
+ return
19
+ end
20
+
21
+ now = Time.now
22
+ post_file_name = "#{now.to_i}.md"
23
+
24
+ initial_content = <<~CONTENT
25
+ ---
26
+ title: #{(0...8).map { (65 + rand(26)).chr }.join}
27
+ date: #{now.strftime("%F %R")}
28
+ tags:
29
+ ---
30
+
31
+ Start writing!
32
+ CONTENT
33
+
34
+
35
+ File.write(File.join(Dir.pwd, POSTS_DIR, post_file_name), initial_content)
36
+
37
+ output_post_created(File.join(POSTS_DIR, post_file_name))
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module Postwave
2
+ VERSION = "0.0.1"
3
+ end
data/lib/postwave.rb ADDED
@@ -0,0 +1,21 @@
1
+ require_relative "postwave/blog_creator"
2
+ require_relative "postwave/blog_builder"
3
+ require_relative "postwave/post_creator"
4
+ require_relative "postwave/version"
5
+
6
+ module Postwave
7
+ def self.call(command, options)
8
+ case command
9
+ when "new"
10
+ Postwave::BlogCreator.instance.create
11
+ when "post"
12
+ Postwave::PostCreator.instance.create
13
+ when "build"
14
+ Postwave::BlogBuilder.instance.build
15
+ else
16
+ if options[:version]
17
+ puts "postwave #{VERSION} [ruby]"
18
+ end
19
+ end
20
+ end
21
+ end
data/postwave.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'postwave/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "postwave"
8
+ spec.version = Postwave::VERSION
9
+ spec.authors = ["Dave Schwantes"]
10
+ spec.email = ["dave.schwantes@gmail.com"]
11
+
12
+ spec.summary = "An opinionated flatfile based blog engine."
13
+ spec.description = "Write your posts statically. Interact with them dynamically."
14
+ spec.homepage = "https://github.com/dorkrawk/postwave"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.executables = ['postwave']
19
+ spec.require_paths = ["lib"]
20
+
21
+ # spec.add_dependency ""
22
+
23
+ spec.add_development_dependency "bundler"
24
+ spec.add_development_dependency "rake", "~> 12.3"
25
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: postwave
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Dave Schwantes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-09-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.3'
41
+ description: Write your posts statically. Interact with them dynamically.
42
+ email:
43
+ - dave.schwantes@gmail.com
44
+ executables:
45
+ - postwave
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - Gemfile
51
+ - README.md
52
+ - Rakefile
53
+ - bin/postwave
54
+ - lib/postwave.rb
55
+ - lib/postwave/blog_builder.rb
56
+ - lib/postwave/blog_creator.rb
57
+ - lib/postwave/blog_utilities.rb
58
+ - lib/postwave/display_helper.rb
59
+ - lib/postwave/post.rb
60
+ - lib/postwave/post_creator.rb
61
+ - lib/postwave/version.rb
62
+ - postwave.gemspec
63
+ homepage: https://github.com/dorkrawk/postwave
64
+ licenses:
65
+ - MIT
66
+ metadata: {}
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.2.22
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: An opinionated flatfile based blog engine.
86
+ test_files: []