aardi 0.9.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/lib/aardi/abstract_blog.rb +27 -0
  3. data/lib/aardi/abstract_feed.rb +32 -0
  4. data/lib/aardi/abstract_page_support.rb +20 -0
  5. data/lib/aardi/archive.rb +48 -0
  6. data/lib/aardi/atom_feed.rb +58 -0
  7. data/lib/aardi/blog.rb +50 -0
  8. data/lib/aardi/config.rb +21 -0
  9. data/lib/aardi/content.rb +17 -0
  10. data/lib/aardi/content_hashes.rb +39 -0
  11. data/lib/aardi/custom_renderer.rb +43 -0
  12. data/lib/aardi/day.rb +44 -0
  13. data/lib/aardi/file_target.rb +43 -0
  14. data/lib/aardi/folder.rb +41 -0
  15. data/lib/aardi/home.rb +51 -0
  16. data/lib/aardi/init_files/config.yml +26 -0
  17. data/lib/aardi/json_feed.rb +32 -0
  18. data/lib/aardi/ledger.rb +15 -0
  19. data/lib/aardi/month.rb +48 -0
  20. data/lib/aardi/orphanage.rb +15 -0
  21. data/lib/aardi/page.rb +25 -0
  22. data/lib/aardi/page_content.rb +21 -0
  23. data/lib/aardi/page_target.rb +16 -0
  24. data/lib/aardi/path_servlet.rb +18 -0
  25. data/lib/aardi/post.rb +56 -0
  26. data/lib/aardi/site.rb +68 -0
  27. data/lib/aardi/sitemap.rb +56 -0
  28. data/lib/aardi/tasks/fixtimes.rake +8 -0
  29. data/lib/aardi/tasks/homepage.rake +6 -0
  30. data/lib/aardi/tasks/init.rake +34 -0
  31. data/lib/aardi/tasks/load_config.rake +7 -0
  32. data/lib/aardi/tasks/new.rake +16 -0
  33. data/lib/aardi/tasks/now.rake +6 -0
  34. data/lib/aardi/tasks/recent.rake +6 -0
  35. data/lib/aardi/tasks/render.rake +6 -0
  36. data/lib/aardi/tasks/server.rake +18 -0
  37. data/lib/aardi/tasks.rb +5 -0
  38. data/lib/aardi/template.rb +41 -0
  39. data/lib/aardi/timekeeper.rb +53 -0
  40. data/lib/aardi/version.rb +5 -0
  41. data/lib/aardi/year.rb +46 -0
  42. data/lib/aardi.rb +54 -0
  43. metadata +274 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6ee9071fced8c74f53967ad02aad285eb7d0f2604c6491531013110838df516e
4
+ data.tar.gz: f398bf6a84c066b43a86f29ebad0055c8b2d4a8d706f0e43769e6c2ebbec7523
5
+ SHA512:
6
+ metadata.gz: d29b6326aacdecdebec15c45c2023bc3d10eec76e2a10fff2cc57604b9888c82a7e515cbd07e7e274c78dd787c39854d8dcc0b6a295164996e21526e1aad32b4
7
+ data.tar.gz: 6e2c8fa1284906984e908968aae84bf3a5757da235e94ffe1856a95db1aac935505259e35d3db1c6a2fab74036eff57095050cf1be2eb54ebc7a5fc9b5c6d171
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class AbstractBlog
5
+ attr_reader :key
6
+
7
+ def metadata = (@metadata ||= {})
8
+
9
+ def mtime = children.max_by(&:mtime)&.mtime
10
+
11
+ def render
12
+ children.each(&:render)
13
+ write_target
14
+ end
15
+
16
+ private
17
+
18
+ def children
19
+ []
20
+ end
21
+
22
+ def write_target
23
+ source = PageContent.new content, title, metadata
24
+ PageTarget.new(source, target_path).write
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class AbstractFeed < AbstractBlog
5
+ def initialize(posts)
6
+ @posts = posts
7
+ end
8
+
9
+ def render
10
+ write_target
11
+ end
12
+
13
+ def target_path = "./#{feed_file}"
14
+
15
+ private
16
+
17
+ def children
18
+ @posts
19
+ end
20
+
21
+ def creation = children.max_by(&:creation)&.creation
22
+
23
+ def feed_url = "#{Aardi.config[:site_url]}/#{feed_file}"
24
+
25
+ def updated = children.max_by(&:updated)&.updated
26
+
27
+ def write_target
28
+ source = Content.new(content)
29
+ FileTarget.new(source, target_path).write
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ module AbstractPageSupport
5
+ attr_reader :metadata, :mtime
6
+
7
+ def parse_source(path)
8
+ File.open(path, encoding: "utf-8") do |file|
9
+ parts = file.read.rpartition("\n----\n")
10
+ @metadata = YAML.safe_load(parts.first, permitted_classes: [Time]) || {}
11
+ @src_content = parts.last
12
+ @mtime = file.mtime.utc
13
+ end
14
+ end
15
+
16
+ def title
17
+ metadata["Title"] || @src_content.split("\n").first.sub(/^#+ /, "")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Archive < AbstractBlog
5
+ def initialize(posts, archive_path)
6
+ @posts = posts
7
+ @archive_path = archive_path
8
+ end
9
+
10
+ def content
11
+ year_fmt = "| %<year>s | %<months>s \n"
12
+ month_fmt = "[&nbsp;%<count>s&nbsp;](#{Aardi.config[:site_url]}/%<archive_path>s/%<year>s/%<month>s/)"
13
+
14
+ "#{header}#{years.map { |year| year.archive_row(year_fmt, month_fmt) }.join}"
15
+ end
16
+
17
+ def target_path = "./#{@archive_path}/index.html"
18
+
19
+ def title = Aardi.config[:blog_archive_title]
20
+
21
+ private
22
+
23
+ def calendar
24
+ index = Hash.new { |hash, year| hash[year] = Year.new(year, @archive_path) }
25
+ @posts.each do |post|
26
+ index[post.year] << post
27
+ end
28
+
29
+ index
30
+ end
31
+
32
+ def children
33
+ years
34
+ end
35
+
36
+ def header
37
+ "# #{Aardi.config[:blog_archive_title]}
38
+
39
+ ||Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|
40
+ |---|---|---|---|---|---|---|---|---|---|---|---|---|
41
+ "
42
+ end
43
+
44
+ def years
45
+ @years ||= calendar.values.sort_by { |date| -date.key }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class ATOMFeed < AbstractFeed
5
+ def content
6
+ atom_feed = Nokogiri::XML::Builder.new(encoding: "UTF-8") do
7
+ feed("xmlns" => "http://www.w3.org/2005/Atom") do |feed|
8
+ feed_details(feed)
9
+ end
10
+ end
11
+
12
+ atom_feed.to_xml
13
+ end
14
+
15
+ # :reek:TooManyStatements
16
+ def feed_details(feed)
17
+ aardi_config = Aardi.config
18
+
19
+ feed.author do
20
+ name(aardi_config[:site_author])
21
+ end
22
+
23
+ subnodes = {id: feed_url, link: {href: feed_url, rel: "self"},
24
+ title: aardi_config[:site_title], updated: updated.iso8601}
25
+
26
+ subnodes.each do |node, value|
27
+ feed.public_send node, value
28
+ end
29
+
30
+ @posts.each do |post|
31
+ post_details(post, feed)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def feed_file
38
+ "index.xml"
39
+ end
40
+
41
+ # :reek:FeatureEnvy
42
+ # :reek:TooManyStatements
43
+ def post_details(post, feed)
44
+ feed.entry do
45
+ post_url = post.url
46
+
47
+ # For safety, must use content_ and not content:
48
+ content_(post.feed_snippet).type = "html"
49
+
50
+ subnodes = {id: post_url, link: {href: post_url}, title: post.title,
51
+ published: post.creation.iso8601, updated: post.updated.iso8601}
52
+ subnodes.each do |node, value|
53
+ feed.public_send node, value
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/aardi/blog.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Blog < AbstractBlog
5
+ def initialize(posts_path, archive_path)
6
+ @posts_path = posts_path
7
+ @archive_path = archive_path
8
+ end
9
+
10
+ def posts
11
+ @posts ||= Dir.glob("#{@posts_path}/**/*.md").map { |post_path| Post.new(post_path) }.sort_by(&:creation)
12
+ end
13
+
14
+ def report_recent
15
+ recent_posts(:blog_recent_posts).each(&:report_field_summary)
16
+ end
17
+
18
+ private
19
+
20
+ def archive
21
+ Archive.new(posts, @archive_path)
22
+ end
23
+
24
+ def atom_feed
25
+ ATOMFeed.new(feed_posts)
26
+ end
27
+
28
+ def children
29
+ [archive, home, atom_feed, json_feed]
30
+ end
31
+
32
+ def feed_posts
33
+ recent_posts(:blog_feed_posts)
34
+ end
35
+
36
+ def home
37
+ Home.new(recent_posts(:blog_home_posts), @archive_path)
38
+ end
39
+
40
+ def json_feed
41
+ JSONFeed.new(feed_posts)
42
+ end
43
+
44
+ def recent_posts(conf_key)
45
+ posts.last(Aardi.config[conf_key]).reverse
46
+ end
47
+
48
+ def write_target = nil
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Config
5
+ def initialize
6
+ @data = {}
7
+ end
8
+
9
+ def [](key) = @data[key]
10
+
11
+ # :reek:TooManyStatements
12
+ def load(path)
13
+ config_hash = YAML.safe_load_file(path)
14
+ config_hash.transform_keys!(&:to_sym)
15
+ config_hash[:markup_options]&.transform_keys!(&:to_sym)
16
+ @data.merge!(config_hash)
17
+ @data.freeze
18
+ self
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Content
5
+ def initialize(src_content)
6
+ @src_content = src_content.strip
7
+ end
8
+
9
+ def output
10
+ @output ||= @src_content
11
+ end
12
+
13
+ def output_hash
14
+ @output_hash ||= Zlib.crc32(output)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class ContentHashes
5
+ def initialize(path)
6
+ @path = path
7
+ read_hashes
8
+ end
9
+
10
+ def [](path)
11
+ @hashes[path]
12
+ end
13
+
14
+ def []=(path, hash)
15
+ @hashes[path] = hash
16
+ end
17
+
18
+ def write
19
+ return if new_hashes == @original_hashes
20
+
21
+ File.write(@path, @new_hashes)
22
+ puts "Wrote: #{@path}\n"
23
+ end
24
+
25
+ private
26
+
27
+ def new_hashes
28
+ @new_hashes ||= @hashes.sort.map! { |path, hash| "#{path}: #{hash}\n" }.join
29
+ end
30
+
31
+ def read_hashes
32
+ @original_hashes = File.exist?(@path) ? File.read(@path) : ""
33
+ @hashes = @original_hashes.split("\n").to_h do |line|
34
+ path, hash = line.split(":", 2)
35
+ [path.strip, hash.to_i]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class CustomRenderer < Redcarpet::Render::HTML
5
+ include Redcarpet::Render::SmartyPants
6
+
7
+ HEADER_SQUEEZE = /&#.*?;|&quot;|[^a-z0-9\-_]/
8
+
9
+ def header(text, header_level)
10
+ squeezed_text = text.squeeze(" ")
11
+ id = header_id(text)
12
+ "<h#{header_level} id=\"#{id}\">#{squeezed_text}</h#{header_level}>"
13
+ end
14
+
15
+ def link(link, title, content)
16
+ link_title = " title=\"#{title}\"" if title
17
+ "<a href=\"#{link}\"#{link_title}>#{content}</a>"
18
+ end
19
+
20
+ def reset
21
+ ids.clear
22
+ end
23
+
24
+ private
25
+
26
+ def header_id(text)
27
+ stub_id = header_stub_id(text)
28
+ count = ids[stub_id] += 1
29
+
30
+ return "#{stub_id}-#{count - 1}" if count > 1
31
+
32
+ stub_id
33
+ end
34
+
35
+ def header_stub_id(text)
36
+ text.downcase.strip.tr(" ", "-").gsub(HEADER_SQUEEZE, "").squeeze("-")
37
+ end
38
+
39
+ def ids
40
+ @ids ||= Hash.new(0)
41
+ end
42
+ end
43
+ end
data/lib/aardi/day.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ # :reek:TooManyInstanceVariables
5
+ class Day < AbstractBlog
6
+ def initialize(year, month, key, archive_path)
7
+ @year = year
8
+ @month = month
9
+ @key = key
10
+ @archive_path = archive_path
11
+ @posts = []
12
+ end
13
+
14
+ def <<(post)
15
+ @posts << post
16
+ end
17
+
18
+ def content
19
+ @content ||= begin
20
+ sorted_posts = @posts.sort_by(&:creation)
21
+ posts_content = sorted_posts.reverse!.map!(&:content)
22
+ "# #{title}\n#{posts_content.join}"
23
+ end
24
+ end
25
+
26
+ def count = @posts.count
27
+
28
+ def target_path
29
+ "./#{@archive_path}/#{@year}/#{@month}/#{self}/index.html"
30
+ end
31
+
32
+ def title = date.strftime("%A, %-e %B %Y")
33
+
34
+ private
35
+
36
+ def children
37
+ @posts
38
+ end
39
+
40
+ def date = Date.new(@year.key, @month.key, @key)
41
+
42
+ def to_s = @key.to_s.rjust(2, "0")
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class FileTarget
5
+ def initialize(src, target_path)
6
+ @src = src
7
+ @path = target_path
8
+ @content_hashes = Aardi.ledger[:content_hashes]
9
+ end
10
+
11
+ # :reek:TooManyStatements
12
+ def write
13
+ do_write = should_write?
14
+ update_hash
15
+ return unless do_write
16
+
17
+ FileUtils.mkdir_p(File.dirname(@path))
18
+ File.write(@path, "#{@src.output}\n")
19
+ puts("Wrote #{@path}")
20
+ end
21
+
22
+ private
23
+
24
+ def file_exists?
25
+ File.exist? @path
26
+ end
27
+
28
+ def output_hash_changed?
29
+ @src.output_hash != @content_hashes[@path]
30
+ end
31
+
32
+ def should_write?
33
+ return true unless file_exists?
34
+ return false unless output_hash_changed?
35
+
36
+ @src.output != File.read(@path).strip
37
+ end
38
+
39
+ def update_hash
40
+ @content_hashes[@path] = @src.output_hash
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Folder
5
+ def initialize(path)
6
+ @path = path
7
+ @normalized_path = "#{path.sub(/^\./, "")}/"
8
+ end
9
+
10
+ def mtime = children.max_by(&:mtime).mtime
11
+
12
+ def render
13
+ children.each(&:render)
14
+
15
+ update_sitemap if Aardi.config[:sitemap_entries][@normalized_path]
16
+ end
17
+
18
+ private
19
+
20
+ def children
21
+ sources + folders
22
+ end
23
+
24
+ def folders
25
+ @folders ||= paths.filter_map { |path| Folder.new(path) if File.directory?(path) }
26
+ end
27
+
28
+ def paths
29
+ @paths ||= FileList["#{@path}/*"].exclude(Aardi.config[:files_to_exclude])
30
+ end
31
+
32
+ def sources
33
+ @sources ||= paths.filter_map { |path| Page.new(path) if path.end_with?(".md") }
34
+ end
35
+
36
+ def update_sitemap
37
+ # '.' is the top level so skip it since the homepage will register itself
38
+ Aardi.ledger[:sitemap].update_mtime(@normalized_path, mtime) unless @path == "."
39
+ end
40
+ end
41
+ end
data/lib/aardi/home.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Home < AbstractBlog
5
+ def initialize(posts, archive_path)
6
+ @posts = posts
7
+ @archive_path = archive_path
8
+ end
9
+
10
+ def content
11
+ "# #{title}\n#{post_days_content}#{content_footer}"
12
+ end
13
+
14
+ def render
15
+ Aardi.ledger[:sitemap].update_mtime("/", mtime)
16
+ write_target
17
+ end
18
+
19
+ def target_path = "./index.html"
20
+
21
+ def title = Aardi.config[:blog_home_title]
22
+
23
+ private
24
+
25
+ def children
26
+ @posts
27
+ end
28
+
29
+ def content_footer
30
+ site_url = Aardi.config[:site_url]
31
+ more_archive = "[Archive](#{site_url}/#{@archive_path}/)"
32
+ more_rss = "[RSS](#{site_url}/index.xml)"
33
+ more_json = "[JSON](#{site_url}/index.json)"
34
+ "**More:** #{more_archive}, #{more_rss}, #{more_json}"
35
+ end
36
+
37
+ def days_hash
38
+ @days_hash ||= @posts.group_by { |post| post.creation.strftime("%Y-%m-%d") }
39
+ end
40
+
41
+ def post_day_content(post_day)
42
+ date_header = post_day.first.creation.strftime("%A, %e %B %Y")
43
+ posts_content = post_day.map(&:content).join
44
+ "## #{date_header}\n#{posts_content}"
45
+ end
46
+
47
+ def post_days_content
48
+ days_hash.values.map { |day| post_day_content(day) }.join
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ site_title: My Site
2
+ site_url: http://localhost:8000
3
+ site_author: Author Name
4
+ files_to_exclude:
5
+ - ./blog
6
+ - ./posts
7
+ ignore_orphans: []
8
+ template_path: .template.html
9
+ blog_archive_path: blog
10
+ blog_posts_path: posts
11
+ blog_feed_posts: 10
12
+ blog_recent_posts: 10
13
+ blog_archive_title: Blog Archive
14
+ blog_home_title: My Site
15
+ blog_home_posts: 10
16
+ sitemap_entries:
17
+ /: daily
18
+ markup_options:
19
+ autolink: false
20
+ fenced_code_blocks: true
21
+ footnotes: true
22
+ no_intra_emphasis: true
23
+ strikethrough: true
24
+ superscript: true
25
+ tables: true
26
+ content_hashes_path: .content_hashes.txt
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class JSONFeed < AbstractFeed
5
+ def content
6
+ aardi_config = Aardi.config
7
+ feed_content = {version: "https://jsonfeed.org/version/1.1", title: aardi_config[:site_title],
8
+ home_page_url: aardi_config[:site_url], feed_url:}
9
+ feed_content[:items] = @posts.map { |post| post_details(post) }
10
+
11
+ JSON.pretty_generate(feed_content)
12
+ end
13
+
14
+ private
15
+
16
+ def feed_file
17
+ "index.json"
18
+ end
19
+
20
+ def post_details(post)
21
+ post_updated = post.updated
22
+ post_creation = post.creation
23
+
24
+ details = {id: post.name, url: post.url, title: post.title,
25
+ date_published: post_creation.iso8601, content_html: post.feed_snippet}
26
+
27
+ details[:date_modified] = post_updated.iso8601 unless post_creation == post_updated
28
+
29
+ details.compact
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Ledger
5
+ def initialize
6
+ @data = {}
7
+ end
8
+
9
+ def [](key) = @data[key]
10
+
11
+ def []=(key, value)
12
+ @data[key] = value
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Month < AbstractBlog
5
+ def initialize(year, key, archive_path)
6
+ @year = year
7
+ @key = key
8
+ @archive_path = archive_path
9
+ @index = Hash.new { |hash, day| hash[day] = Day.new(year, self, day, archive_path) }
10
+ end
11
+
12
+ def <<(post)
13
+ @index[post.day] << post
14
+ end
15
+
16
+ def archive_cell(month_fmt)
17
+ cell = count.zero? ? "" : format(month_fmt, count:, archive_path: @archive_path, year: @year, month: self)
18
+ "#{cell} |"
19
+ end
20
+
21
+ def content
22
+ @content ||= begin
23
+ days_content = days.map { |day| "##{day.content}" }.join
24
+ "# #{title}\n#{days_content}"
25
+ end
26
+ end
27
+
28
+ def count = days.sum(&:count)
29
+
30
+ def target_path
31
+ "./#{@archive_path}/#{@year}/#{self}/index.html"
32
+ end
33
+
34
+ def title = Date.new(@year.key, @key).strftime("%B %Y")
35
+
36
+ def to_s = @key.to_s.rjust(2, "0")
37
+
38
+ private
39
+
40
+ def children
41
+ days
42
+ end
43
+
44
+ def days
45
+ @days ||= @index.values.sort_by { |value| -value.key }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aardi
4
+ class Orphanage
5
+ def warn
6
+ Aardi.ledger[:html_files].each { |path| puts("Orphan: #{path}") unless ignored?(path) }
7
+ end
8
+
9
+ private
10
+
11
+ def ignored?(path)
12
+ Aardi.config[:ignore_orphans].any? { |prefix| path.start_with?(prefix) }
13
+ end
14
+ end
15
+ end