diff --git a/spec/classes/embedded_converter_spec.cr b/spec/classes/embedded_converter_spec.cr index 4b7ec07..780bc92 100644 --- a/spec/classes/embedded_converter_spec.cr +++ b/spec/classes/embedded_converter_spec.cr @@ -5,6 +5,7 @@ include Nodes describe EmbeddedConverter do context "when the mediaResource has an iframeSrc value" do it "returns an EmbeddedContent node" do + store = GistStore.new paragraph = PostResponse::Paragraph.from_json <<-JSON { "text": "", @@ -25,7 +26,7 @@ describe EmbeddedConverter do } JSON - result = EmbeddedConverter.convert(paragraph) + result = EmbeddedConverter.convert(paragraph, store) result.should eq( EmbeddedContent.new( @@ -40,6 +41,7 @@ describe EmbeddedConverter do context "when the mediaResource has a blank iframeSrc value" do context "and the href is unknown" do it "returns an EmbeddedLink node" do + store = GistStore.new paragraph = PostResponse::Paragraph.from_json <<-JSON { "text": "", @@ -60,7 +62,7 @@ describe EmbeddedConverter do } JSON - result = EmbeddedConverter.convert(paragraph) + result = EmbeddedConverter.convert(paragraph, store) result.should eq(EmbeddedLink.new(href: "https://example.com")) end @@ -68,6 +70,7 @@ describe EmbeddedConverter do context "and the href is gist.github.com" do it "returns an GithubGist node" do + store = GistStore.new paragraph = PostResponse::Paragraph.from_json <<-JSON { "text": "", @@ -88,10 +91,13 @@ describe EmbeddedConverter do } JSON - result = EmbeddedConverter.convert(paragraph) + result = EmbeddedConverter.convert(paragraph, store) result.should eq( - GithubGist.new(href: "https://gist.github.com/user/someid") + GithubGist.new( + href: "https://gist.github.com/user/someid", + gist_store: store + ) ) end end diff --git a/spec/classes/gist_scanner_spec.cr b/spec/classes/gist_scanner_spec.cr new file mode 100644 index 0000000..a5648e6 --- /dev/null +++ b/spec/classes/gist_scanner_spec.cr @@ -0,0 +1,102 @@ +require "../spec_helper" + +describe GistScanner do + it "returns gist ids from paragraphs" do + iframe = PostResponse::IFrame.new( + PostResponse::MediaResource.new( + href: "https://gist.github.com/user/123ABC", + iframeSrc: "", + iframeWidth: 0, + iframeHeight: 0 + ) + ) + paragraphs = [ + PostResponse::Paragraph.new( + text: "Check out this gist:", + type: PostResponse::ParagraphType::P, + markups: [] of PostResponse::Markup, + iframe: nil, + layout: nil, + metadata: nil + ), + PostResponse::Paragraph.new( + text: "", + type: PostResponse::ParagraphType::IFRAME, + markups: [] of PostResponse::Markup, + iframe: iframe, + layout: nil, + metadata: nil + ), + ] + + result = GistScanner.new(paragraphs).scan + + result.should eq(["https://gist.github.com/user/123ABC"]) + end + + it "returns ids without the file parameters" do + iframe = PostResponse::IFrame.new( + PostResponse::MediaResource.new( + href: "https://gist.github.com/user/123ABC?file=example.txt", + iframeSrc: "", + iframeWidth: 0, + iframeHeight: 0 + ) + ) + paragraphs = [ + PostResponse::Paragraph.new( + text: "", + type: PostResponse::ParagraphType::IFRAME, + markups: [] of PostResponse::Markup, + iframe: iframe, + layout: nil, + metadata: nil + ), + ] + + result = GistScanner.new(paragraphs).scan + + result.should eq(["https://gist.github.com/user/123ABC"]) + end + + it "returns a unique list of ids" do + iframe1 = PostResponse::IFrame.new( + PostResponse::MediaResource.new( + href: "https://gist.github.com/user/123ABC?file=example.txt", + iframeSrc: "", + iframeWidth: 0, + iframeHeight: 0 + ) + ) + iframe2 = PostResponse::IFrame.new( + PostResponse::MediaResource.new( + href: "https://gist.github.com/user/123ABC?file=other.txt", + iframeSrc: "", + iframeWidth: 0, + iframeHeight: 0 + ) + ) + paragraphs = [ + PostResponse::Paragraph.new( + text: "", + type: PostResponse::ParagraphType::IFRAME, + markups: [] of PostResponse::Markup, + iframe: iframe1, + layout: nil, + metadata: nil + ), + PostResponse::Paragraph.new( + text: "", + type: PostResponse::ParagraphType::IFRAME, + markups: [] of PostResponse::Markup, + iframe: iframe2, + layout: nil, + metadata: nil + ), + ] + + result = GistScanner.new(paragraphs).scan + + result.should eq(["https://gist.github.com/user/123ABC"]) + end +end diff --git a/spec/classes/gist_store_spec.cr b/spec/classes/gist_store_spec.cr new file mode 100644 index 0000000..182d425 --- /dev/null +++ b/spec/classes/gist_store_spec.cr @@ -0,0 +1,58 @@ +require "../spec_helper" + +describe GistStore do + describe "#store_gist_file" do + describe "adds the gist file to the gist id" do + it "calls the github client" do + store = GistStore.new + file = GistFile.new( + filename: "filename", + content: "content", + raw_url: "raw_url" + ) + + store.store_gist_file("1", file) + + store.store["1"].should eq([file]) + end + end + end + + describe "the gist does not exist in the store" do + it "returns a MissingGistFile" do + missing_file = MissingGistFile.new(id: "1", filename: "filename") + store = GistStore.new + + file = store.get_gist_files(id: "1", filename: "filename") + + file.should eq([missing_file]) + end + end + + describe "when a filename is given" do + it "returns the GistFile for that filename" do + store = GistStore.new + file1 = GistFile.new("one", "", "") + file2 = GistFile.new("two", "", "") + store.store["1"] = [file1, file2] + + gists = store.get_gist_files(id: "1", filename: "one") + + gists.should eq([file1]) + gists.should_not contain([file2]) + end + end + + describe "when a filename is NOT given" do + it "returns all GistFiles" do + store = GistStore.new + file1 = GistFile.new("one", "", "") + file2 = GistFile.new("two", "", "") + store.store["1"] = [file1, file2] + + gists = store.get_gist_files(id: "1", filename: nil) + + gists.should eq([file1, file2]) + end + end +end diff --git a/spec/classes/paragraph_converter_spec.cr b/spec/classes/paragraph_converter_spec.cr index c3e30cb..e66b824 100644 --- a/spec/classes/paragraph_converter_spec.cr +++ b/spec/classes/paragraph_converter_spec.cr @@ -4,6 +4,7 @@ include Nodes describe ParagraphConverter do it "converts a simple structure with no markups" do + gist_store = GistStore.new paragraphs = Array(PostResponse::Paragraph).from_json <<-JSON [ { @@ -18,12 +19,13 @@ describe ParagraphConverter do JSON expected = [Heading3.new(children: [Text.new(content: "Title")] of Child)] - result = ParagraphConverter.new.convert(paragraphs) + result = ParagraphConverter.new.convert(paragraphs, gist_store) result.should eq expected end it "converts a simple structure with a markup" do + gist_store = GistStore.new paragraphs = Array(PostResponse::Paragraph).from_json <<-JSON [ { @@ -54,12 +56,13 @@ describe ParagraphConverter do ] of Child), ] - result = ParagraphConverter.new.convert(paragraphs) + result = ParagraphConverter.new.convert(paragraphs, gist_store) result.should eq expected end it "groups
+
+ example
+
+
+ content
+
HTML
end
diff --git a/spec/models/gist_file_spec.cr b/spec/models/gist_file_spec.cr
new file mode 100644
index 0000000..380adcd
--- /dev/null
+++ b/spec/models/gist_file_spec.cr
@@ -0,0 +1,34 @@
+require "../spec_helper"
+
+describe GistFile do
+ it "is parsed from json" do
+ json = <<-JSON
+ {
+ "filename": "example.txt",
+ "raw_url": "https://gist.githubusercontent.com/user/1D/raw/FFF/example.txt",
+ "content": "content"
+ }
+ JSON
+
+ gist_file = GistFile.from_json(json)
+
+ gist_file.filename.should eq("example.txt")
+ gist_file.content.should eq("content")
+ gist_file.raw_url.should eq("https://gist.githubusercontent.com/user/1D/raw/FFF/example.txt")
+ end
+
+ it "returns an href for the gist's webpage" do
+ json = <<-JSON
+ {
+ "filename": "example.txt",
+ "raw_url": "https://gist.githubusercontent.com/user/1D/raw/FFF/example.txt",
+ "content": "content"
+ }
+ JSON
+ gist_file = GistFile.from_json(json)
+
+ href = gist_file.href
+
+ href.should eq("https://gist.github.com/user/1D#file-example-txt")
+ end
+end
diff --git a/spec/models/gist_params_spec.cr b/spec/models/gist_params_spec.cr
new file mode 100644
index 0000000..df711be
--- /dev/null
+++ b/spec/models/gist_params_spec.cr
@@ -0,0 +1,33 @@
+require "../spec_helper"
+
+describe GistParams do
+ it "extracts params from the gist url" do
+ url = "https://gist.github.com/user/1D?file=example.txt"
+
+ params = GistParams.extract_from_url(url)
+
+ params.id.should eq("1D")
+ params.filename.should eq("example.txt")
+ end
+
+ describe "when no file param exists" do
+ it "does not extract a filename" do
+ url = "https://gist.github.com/user/1D"
+
+ params = GistParams.extract_from_url(url)
+
+ params.id.should eq("1D")
+ params.filename.should be_nil
+ end
+ end
+
+ describe "when the URL is not a gist URL" do
+ it "raises a MissingGistId exeption" do
+ url = "https://example.com"
+
+ expect_raises(GistParams::MissingGistId, message: "https://example.com") do
+ GistParams.extract_from_url(url)
+ end
+ end
+ end
+end
diff --git a/src/classes/embedded_converter.cr b/src/classes/embedded_converter.cr
index 6848bfc..925e54f 100644
--- a/src/classes/embedded_converter.cr
+++ b/src/classes/embedded_converter.cr
@@ -1,15 +1,22 @@
class EmbeddedConverter
include Nodes
- GIST_HOST = "https://gist.github.com"
+ GIST_HOST_AND_SCHEME = "https://#{GIST_HOST}"
getter paragraph : PostResponse::Paragraph
+ getter gist_store : GistStore | RateLimitedGistStore
- def self.convert(paragraph : PostResponse::Paragraph) : Embedded | Empty
- new(paragraph).convert
+ def self.convert(
+ paragraph : PostResponse::Paragraph,
+ gist_store : GistStore | RateLimitedGistStore
+ ) : Embedded | Empty
+ new(paragraph, gist_store).convert
end
- def initialize(@paragraph : PostResponse::Paragraph)
+ def initialize(
+ @paragraph : PostResponse::Paragraph,
+ @gist_store : GistStore | RateLimitedGistStore
+ )
end
def convert : Embedded | Empty
@@ -33,8 +40,8 @@ class EmbeddedConverter
end
private def custom_embed(media : PostResponse::MediaResource) : Embedded
- if media.href.starts_with?(GIST_HOST)
- GithubGist.new(href: media.href)
+ if media.href.starts_with?(GIST_HOST_AND_SCHEME)
+ GithubGist.new(href: media.href, gist_store: gist_store)
else
EmbeddedLink.new(href: media.href)
end
diff --git a/src/classes/gist_scanner.cr b/src/classes/gist_scanner.cr
new file mode 100644
index 0000000..9fd6852
--- /dev/null
+++ b/src/classes/gist_scanner.cr
@@ -0,0 +1,28 @@
+class GistScanner
+ GIST_HOST_AND_SCHEME = "https://#{GIST_HOST}"
+
+ getter paragraphs : Array(PostResponse::Paragraph)
+
+ def initialize(@paragraphs : Array(PostResponse::Paragraph))
+ end
+
+ def scan
+ maybe_urls = paragraphs.compact_map do |paragraph|
+ Monads::Try(PostResponse::IFrame).new(->{ paragraph.iframe })
+ .to_maybe
+ .fmap(->(iframe : PostResponse::IFrame) { iframe.mediaResource })
+ .fmap(->(media : PostResponse::MediaResource) { media.href })
+ .value_or(nil)
+ end
+ maybe_urls
+ .select { |url| url.starts_with?(GIST_HOST_AND_SCHEME) }
+ .map { |url| url_without_params(url) }
+ .uniq
+ end
+
+ def url_without_params(url)
+ uri = URI.parse(url)
+ uri.query = nil
+ uri.to_s
+ end
+end
diff --git a/src/classes/gist_store.cr b/src/classes/gist_store.cr
new file mode 100644
index 0000000..9fd148a
--- /dev/null
+++ b/src/classes/gist_store.cr
@@ -0,0 +1,45 @@
+alias GistFiles = Array(GistFile)
+alias GistHash = Hash(String, GistFiles)
+
+class GistStore
+ property store : GistHash
+
+ def initialize(@store : GistHash = {} of String => GistFiles)
+ end
+
+ def store_gist_file(id : String, file : GistFile)
+ store[id] ||= [] of GistFile
+ store[id] << file
+ end
+
+ def get_gist_files(id : String, filename : String?) : Array(GistFile) | Array(MissingGistFile)
+ files = store[id]?
+ missing_file = MissingGistFile.new(id: id, filename: filename)
+ if files
+ if filename
+ find_gist_file(files, filename, missing_file)
+ else
+ files
+ end
+ else
+ return [missing_file]
+ end
+ end
+
+ private def find_gist_file(
+ files : Array(GistFile),
+ filename : String,
+ missing_file : MissingGistFile
+ ) : Array(GistFile) | Array(MissingGistFile)
+ gist_file = files.find { |file| file.filename == filename }
+ if gist_file
+ [gist_file]
+ else
+ [missing_file]
+ end
+ end
+
+ private def client_class
+ GithubClient
+ end
+end
diff --git a/src/classes/page_converter.cr b/src/classes/page_converter.cr
index 70f932f..a277f96 100644
--- a/src/classes/page_converter.cr
+++ b/src/classes/page_converter.cr
@@ -3,11 +3,12 @@ class PageConverter
title, content = title_and_content(data)
author = data.post.creator
created_at = Time.unix_ms(data.post.createdAt)
+ gist_store = gist_store(content)
Page.new(
title: title,
author: author,
created_at: Time.unix_ms(data.post.createdAt),
- nodes: ParagraphConverter.new.convert(content)
+ nodes: ParagraphConverter.new.convert(content, gist_store)
)
end
@@ -17,4 +18,20 @@ class PageConverter
non_content_paragraphs = paragraphs.reject { |para| para.text == title }
{title, non_content_paragraphs}
end
+
+ private def gist_store(paragraphs) : GistStore | RateLimitedGistStore
+ store = GistStore.new
+ gist_urls = GistScanner.new(paragraphs).scan
+ gist_responses = gist_urls.map do |url|
+ params = GistParams.extract_from_url(url)
+ response = GithubClient.get_gist_response(params.id)
+ if response.is_a?(GithubClient::RateLimitedResponse)
+ return RateLimitedGistStore.new
+ end
+ JSON.parse(response.data.body)["files"].as_h.values.map do |json_any|
+ store.store_gist_file(params.id, GistFile.from_json(json_any.to_json))
+ end
+ end
+ store
+ end
end
diff --git a/src/classes/paragraph_converter.cr b/src/classes/paragraph_converter.cr
index d62185f..00a76f4 100644
--- a/src/classes/paragraph_converter.cr
+++ b/src/classes/paragraph_converter.cr
@@ -1,7 +1,10 @@
class ParagraphConverter
include Nodes
- def convert(paragraphs : Array(PostResponse::Paragraph)) : Array(Child)
+ def convert(
+ paragraphs : Array(PostResponse::Paragraph),
+ gist_store : GistStore | RateLimitedGistStore
+ ) : Array(Child)
if paragraphs.first?.nil?
return [Empty.new] of Child
else
@@ -24,7 +27,7 @@ class ParagraphConverter
node = Heading3.new(children: children)
when PostResponse::ParagraphType::IFRAME
paragraph = paragraphs.shift
- node = EmbeddedConverter.convert(paragraph)
+ node = EmbeddedConverter.convert(paragraph, gist_store)
when PostResponse::ParagraphType::IMG
paragraph = paragraphs.shift
node = convert_img(paragraph)
@@ -60,7 +63,7 @@ class ParagraphConverter
node = Empty.new
end
- [node, convert(paragraphs)].flatten.reject(&.empty?)
+ [node, convert(paragraphs, gist_store)].flatten.reject(&.empty?)
end
end
diff --git a/src/classes/rate_limited_gist_store.cr b/src/classes/rate_limited_gist_store.cr
new file mode 100644
index 0000000..d5a2516
--- /dev/null
+++ b/src/classes/rate_limited_gist_store.cr
@@ -0,0 +1,5 @@
+class RateLimitedGistStore
+ def get_gist_files(id : String, filename : String?)
+ [RateLimitedGistFile.new(id: id, filename: filename)]
+ end
+end
diff --git a/src/clients/github_client.cr b/src/clients/github_client.cr
new file mode 100644
index 0000000..b6a5610
--- /dev/null
+++ b/src/clients/github_client.cr
@@ -0,0 +1,37 @@
+class GithubClient
+ class SuccessfulResponse
+ getter data : HTTP::Client::Response
+
+ def initialize(@data : HTTP::Client::Response)
+ end
+ end
+
+ class RateLimitedResponse
+ end
+
+ def self.get_gist_response(id : String) : SuccessfulResponse | RateLimitedResponse
+ new.get_gist_response(id)
+ end
+
+ def get_gist_response(id : String) : SuccessfulResponse | RateLimitedResponse
+ client = HTTP::Client.new("api.github.com", tls: true)
+ if username && password
+ client.basic_auth(username, password)
+ end
+ response = client.get("/gists/#{id}")
+ if response.status == HTTP::Status::FORBIDDEN &&
+ response.headers["X-RateLimit-Remaining"] == "0"
+ RateLimitedResponse.new
+ else
+ SuccessfulResponse.new(response)
+ end
+ end
+
+ private def username
+ ENV["GITHUB_USERNAME"]?
+ end
+
+ private def password
+ ENV["GITHUB_PERSONAL_ACCESS_TOKEN"]?
+ end
+end
diff --git a/src/components/page_content.cr b/src/components/page_content.cr
index 7ffb41f..2849bbc 100644
--- a/src/components/page_content.cr
+++ b/src/components/page_content.cr
@@ -77,8 +77,19 @@ class PageContent < BaseComponent
end
end
- def render_child(child : GithubGist)
- script src: child.src
+ def render_child(gist : GithubGist)
+ gist.files.map { |gist_file| render_child(gist_file) }
+ end
+
+ def render_child(gist_file : GistFile | MissingGistFile | RateLimitedGistFile)
+ para do
+ code do
+ a gist_file.filename, href: gist_file.href
+ end
+ end
+ pre class: "gist" do
+ code gist_file.content
+ end
end
def render_child(node : Heading1)
diff --git a/src/constants.cr b/src/constants.cr
index a136d90..f750574 100644
--- a/src/constants.cr
+++ b/src/constants.cr
@@ -1,2 +1,4 @@
# https://stackoverflow.com/questions/2669690/
JSON_HIJACK_STRING = "])}while(1);"
+
+GIST_HOST = "gist.github.com"
diff --git a/src/models/gist_file.cr b/src/models/gist_file.cr
new file mode 100644
index 0000000..18bced1
--- /dev/null
+++ b/src/models/gist_file.cr
@@ -0,0 +1,89 @@
+class GistFile
+ include JSON::Serializable
+
+ getter filename : String
+ getter content : String
+ getter raw_url : String
+
+ def initialize(@filename : String, @content : String, @raw_url : String)
+ end
+
+ def href
+ uri = URI.parse(raw_url)
+ uri.host = GIST_HOST
+ path_and_file_anchor = path_and_file_anchor(uri)
+ uri.path = path_and_file_anchor.path
+ uri.fragment = path_and_file_anchor.file_anchor
+ uri.to_s
+ end
+
+ private def path_and_file_anchor(uri : URI)
+ path_parts = uri.path.split("/")
+ PathAndFileAnchor.new(
+ path: [path_parts[1], path_parts[2]].join("/"),
+ filename: path_parts[-1]
+ )
+ end
+
+ class PathAndFileAnchor
+ getter file_anchor : String
+ getter path : String
+
+ def initialize(@path : String, filename : String)
+ @file_anchor = "file-" + filename.tr(" ", "-").tr(".", "-")
+ end
+ end
+end
+
+class MissingGistFile
+ GIST_HOST_AND_SCHEME = "https://#{GIST_HOST}"
+
+ def initialize(@id : String, @filename : String?)
+ end
+
+ def content
+ <<-TEXT
+ Gist file missing.
+ Click on filename to go to gist.
+ TEXT
+ end
+
+ def href
+ GIST_HOST_AND_SCHEME + "/#{@id}"
+ end
+
+ def filename
+ @filename || "Unknown filename"
+ end
+
+ def ==(other : MissingGistFile)
+ other.filename == filename && other.href == href
+ end
+end
+
+class RateLimitedGistFile
+ GIST_HOST_AND_SCHEME = "https://#{GIST_HOST}"
+
+ def initialize(@id : String, @filename : String?)
+ end
+
+ def content
+ <<-TEXT
+ Can't fetch gist.
+ GitHub rate limit reached.
+ Click on filename to go to gist.
+ TEXT
+ end
+
+ def href
+ GIST_HOST_AND_SCHEME + "/#{@id}"
+ end
+
+ def filename
+ @filename || "Unknown filename"
+ end
+
+ def ==(other : RateLimitedGistFile)
+ other.filename == filename && other.href == href
+ end
+end
diff --git a/src/models/gist_params.cr b/src/models/gist_params.cr
new file mode 100644
index 0000000..c8d5dfe
--- /dev/null
+++ b/src/models/gist_params.cr
@@ -0,0 +1,30 @@
+class GistParams
+ class MissingGistId < Exception
+ end
+
+ GIST_ID_REGEX = /[a-f\d]+$/i
+
+ getter id : String
+ getter filename : String?
+
+ def self.extract_from_url(href : String)
+ uri = URI.parse(href)
+ maybe_id = Monads::Try(Regex::MatchData)
+ .new(->{ uri.path.match(GIST_ID_REGEX) })
+ .to_maybe
+ .fmap(->(matches : Regex::MatchData) { matches[0] })
+ case maybe_id
+ in Monads::Just
+ id = maybe_id.value!
+ in Monads::Nothing, Monads::Maybe
+ raise MissingGistId.new(href)
+ end
+
+ filename = uri.query_params["file"]?
+
+ new(id: id, filename: filename)
+ end
+
+ def initialize(@id : String, @filename : String?)
+ end
+end
diff --git a/src/models/nodes.cr b/src/models/nodes.cr
index 49fee91..0ddce68 100644
--- a/src/models/nodes.cr
+++ b/src/models/nodes.cr
@@ -217,15 +217,21 @@ module Nodes
end
class GithubGist
- def initialize(@href : String)
+ getter gist_store : GistStore | RateLimitedGistStore
+
+ def initialize(@href : String, @gist_store : GistStore | RateLimitedGistStore)
end
- def src
- "#{@href}.js"
+ def files : Array(GistFile) | Array(MissingGistFile) | Array(RateLimitedGistFile)
+ gist_store.get_gist_files(params.id, params.filename)
+ end
+
+ private def params
+ GistParams.extract_from_url(@href)
end
def ==(other : GithubGist)
- other.src == src
+ other.gist_store == gist_store
end
def empty?
diff --git a/src/models/post_response.cr b/src/models/post_response.cr
index ff960d0..aedf898 100644
--- a/src/models/post_response.cr
+++ b/src/models/post_response.cr
@@ -38,6 +38,16 @@ class PostResponse
property iframe : IFrame?
property layout : String?
property metadata : Metadata?
+
+ def initialize(
+ @text : String?,
+ @type : ParagraphType,
+ @markups : Array(Markup),
+ @iframe : IFrame?,
+ @layout : String?,
+ @metadata : Metadata?
+ )
+ end
end
enum ParagraphType
@@ -80,6 +90,9 @@ class PostResponse
class IFrame < Base
property mediaResource : MediaResource
+
+ def initialize(@mediaResource : MediaResource)
+ end
end
class MediaResource < Base
@@ -87,6 +100,14 @@ class PostResponse
property iframeSrc : String
property iframeWidth : Int32
property iframeHeight : Int32
+
+ def initialize(
+ @href : String,
+ @iframeSrc : String,
+ @iframeWidth : Int32,
+ @iframeHeight : Int32
+ )
+ end
end
class Metadata < Base
diff --git a/src/version.cr b/src/version.cr
index efa2c88..c22052a 100644
--- a/src/version.cr
+++ b/src/version.cr
@@ -1,3 +1,3 @@
module Scribe
- VERSION = "2022-01-08"
+ VERSION = "2022-01-23"
end