postwave 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []