Last active
July 19, 2019 00:35
-
-
Save teddy-ma/ee3a62556dd9e74bfead9698dbeceda3 to your computer and use it in GitHub Desktop.
tdd org blog parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ArticleGeneratorTest < Minitest::Test | |
BLOG_TEXT = <<EOF | |
* single post | |
:PROPERTIES: | |
:EXPORT_FILE_NAME: demo-article-1 | |
:END: | |
** Today is Friday | |
Friday Line _one_ | |
Friday Line _two_ | |
* series post | |
:PROPERTIES: | |
:BASE_EXPORT_TITLE: series-post | |
:BASE_FILE_PATH: series title | |
:END: | |
** Today is Friday | |
:PROPERTIES: | |
:EXPORT_FILE_NAME: friday | |
:END: | |
Friday Line _one_ | |
Friday Line _two_ | |
** Today is Sunday | |
:PROPERTIES: | |
:EXPORT_FILE_NAME: sunday | |
:END: | |
Sunday Line _one_ | |
Sunday Line _two_ | |
EOF | |
TEST_BASE_FILE_PATH = "out" | |
def single_post_items_fixture | |
[ | |
Item.new(:article_begin), | |
Item.new(:title_begin), | |
Item.new("* single post"), | |
Item.new(:properties_begin), | |
Item.new(":EXPORT_FILE_NAME: demo-article-1"), | |
Item.new(:properties_end), | |
Item.new("** Today is Friday"), | |
Item.new("Line _one_"), | |
Item.new("Line _two_") | |
] | |
end | |
def series_posts_items_fixture | |
[ | |
Item.new(:article_begin), | |
Item.new(:title_begin), | |
Item.new("* series post"), | |
Item.new(:properties_begin), | |
Item.new(":EXPORT_FILE_NAME: series_page"), | |
Item.new(:properties_end), | |
Item.new("** Chapter1"), | |
Item.new(:properties_begin), | |
Item.new(":EXPORT_FILE_NAME: chapter1"), | |
Item.new(:properties_end), | |
Item.new("Line _one_"), | |
Item.new("Line _two_"), | |
Item.new("** Chapter2"), | |
Item.new(:properties_begin), | |
Item.new(":EXPORT_FILE_NAME: chapter2"), | |
Item.new(:properties_end), | |
Item.new("Line _one_"), | |
Item.new("Line _two_") | |
] | |
end | |
def single_article_fixture | |
article = Article.new | |
article.set_title("hello") | |
article.set_finished?(true) | |
article.append_properties(EXPORT_FILE_NAME: "demo-article-1") | |
article.append_body("** post title") | |
article.append_body("foobar") | |
article | |
end | |
def series_article_fixture | |
article = Article.new | |
article.set_title("welcome") | |
article.set_finished?(true) | |
article.append_properties(BASE_EXPORT_TITLE: "series title") | |
article.append_properties(BASE_FILE_PATH: "series_path") | |
article.append_body("** post title1") | |
article.append_body("foobar1") | |
article.posts.first.append_properties(EXPORT_FILE_NAME: "chapter1") | |
article.append_body("** post title2") | |
article.append_body("foobar2") | |
article.posts.last.append_properties(EXPORT_FILE_NAME: "chapter2") | |
article | |
end | |
def pages_fixture | |
series_data_array = [ | |
["series/series_post_1", "post 1 title"], | |
["series/series_post_2", "post 2 title"], | |
["series/series_post_3", "post 3 title"], | |
] | |
pages = series_data_array.map do |data| | |
Page.new(data[0], data[1], "foobar") | |
end | |
[Page.new("series", "series title", series_data_array), Page.new("single_post", "hello", "xyz")] + pages | |
end | |
def test_item_parse | |
finished_title_item = Item.new("* DONE single post") | |
assert_equal "single post", finished_title_item.to_title | |
assert finished_title_item.to_finished? | |
unfinished_title_item = Item.new("* DOING single post") | |
refute unfinished_title_item.to_finished? | |
default_title_item = Item.new("* single post") | |
refute default_title_item.to_finished? | |
property_item = Item.new(":EXPORT_FILE_NAME: demo-article-1") | |
except_property = { EXPORT_FILE_NAME: "demo-article-1" } | |
assert_equal except_property, property_item.to_property | |
raw_content_item = Item.new("_lol_") | |
assert_equal "_lol_", raw_content_item.to_raw_content | |
end | |
def test_article | |
article = Article.new | |
article.set_title("hello") | |
assert_equal "hello", article.title | |
article.append_properties({foo: 'bar'}) | |
article.append_properties({abc: 'xyz'}) | |
except_properties = {foo: 'bar', abc: 'xyz'} | |
assert_equal except_properties, article.properties | |
end | |
def test_build_single_post | |
builder = Builder.new | |
items = single_post_items_fixture | |
builder.parse(items) | |
article = builder.article | |
assert_equal "single post", article.title | |
refute article.finished | |
assert_equal 1, article.posts.count | |
only_post = article.current_post | |
except_properties = { EXPORT_FILE_NAME: "demo-article-1" } | |
assert_equal "Today is Friday", only_post.title | |
assert_equal except_properties, article.properties | |
assert_equal ["** Today is Friday", "Line _one_", "Line _two_"], only_post.body | |
end | |
def test_build_series_post | |
builder = Builder.new | |
items = series_posts_items_fixture | |
builder.parse(items) | |
article = builder.article | |
assert_equal "series post", article.title | |
refute article.finished | |
assert_equal 2, article.posts.count | |
except_article_properties = { EXPORT_FILE_NAME: "series_page" } | |
except_post1_properties = { EXPORT_FILE_NAME: "chapter1" } | |
except_post2_properties = { EXPORT_FILE_NAME: "chapter2" } | |
assert_equal except_article_properties, article.properties | |
assert_equal except_post1_properties, article.posts.first.properties | |
assert_equal except_post2_properties, article.posts.last.properties | |
assert_equal ["** Chapter1", "Line _one_", "Line _two_"], article.posts.first.body | |
assert_equal ["** Chapter2", "Line _one_", "Line _two_"], article.posts.last.body | |
assert_equal "Chapter1", article.posts.first.title | |
assert_equal "Chapter2", article.posts.last.title | |
end | |
def test_stream | |
stream = Stream.new(BLOG_TEXT) | |
stream.parse | |
articles = stream.articles | |
assert_equal 2, articles.count | |
end | |
def test_single_post_writer | |
article = single_article_fixture | |
writer = WriterFactory.build(article) | |
refute writer.draft | |
page = writer.build_pages.first | |
assert_equal article.posts.first.body.join("\n"), page.content | |
assert_equal 'post title', page.title | |
assert_equal 'demo-article-1', page.file_path | |
end | |
def test_series_posts_writer | |
article = series_article_fixture | |
writer = SeriesWriter.new(article) | |
refute writer.draft | |
pages = writer.build_pages | |
page1 = pages.shift | |
assert_equal article.posts.first.body.join("\n"), page1.content | |
assert_equal 'post title1', page1.title | |
assert_equal 'series_path/chapter1', page1.file_path | |
page2 = pages.shift | |
assert_equal article.posts.last.body.join("\n"), page2.content | |
assert_equal 'post title2', page2.title | |
assert_equal 'series_path/chapter2', page2.file_path | |
index_page = pages.shift | |
assert_equal 'series title', index_page.title | |
assert_equal 'series_path', index_page.file_path | |
except_index_page_content = [ | |
['series_path/chapter1', 'post title1'], | |
['series_path/chapter2', 'post title2'] | |
] | |
assert_equal except_index_page_content, index_page.content | |
end | |
def test_blog_generator | |
pages = pages_fixture | |
generator = BlogGenerator.new(TEST_BASE_FILE_PATH, pages) | |
generator.generator_archive_page | |
archive_pages = generator.archive_pages | |
assert_equal 2, archive_pages.count | |
assert_equal pages[0..1], archive_pages | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Article | |
attr_accessor :title, :properties, :finished, :posts | |
def initialize | |
@properties = {} | |
@body = [] | |
@finished = false | |
@posts = [] | |
end | |
def set_title(title) | |
@title = title | |
end | |
def set_finished?(boolean) | |
@finished = boolean | |
end | |
def append_properties(kv) | |
if current_post | |
current_post.append_properties(kv) | |
else | |
@properties.merge!(kv) | |
end | |
end | |
def append_body(line) | |
posts << Post.new(line) if line.start_with?("** ") | |
current_post.append_body(line) | |
end | |
def current_post | |
posts.last | |
end | |
end | |
class Post | |
attr_accessor :title, :body, :properties, :finished | |
def initialize(title) | |
parse_title(title) | |
@body = [] | |
@properties = {} | |
end | |
def parse_title(title) | |
@title = title.gsub("** ", "") | |
end | |
def append_properties(kv) | |
@properties.merge!(kv) | |
end | |
def append_body(line) | |
body << line | |
end | |
end | |
class Stream | |
attr_accessor :text, :builders, :items, :articles | |
def initialize(text) | |
@text = text | |
@items = [] | |
@builders = [] | |
end | |
def parse | |
lines = text.split("\n") | |
parse_line(lines) | |
end | |
def parse_line(lines) | |
lines.each do |line| | |
if line.start_with?("* ") | |
append(:article_end) | |
append(:title_begin) | |
append(line) | |
elsif line.start_with?(":PROPERTIES:") | |
append(:properties_begin) | |
elsif line.start_with?(":END:") | |
append(:properties_end) | |
else | |
append(line) | |
end | |
end | |
clean_lines(items) | |
index_of_begin = items.each_index.select { |index| items[index].article_start?} | |
index_of_end = items.each_index.select { |index| items[index].article_end?} | |
pair = index_of_begin.zip(index_of_end) | |
items_of_article = pair.map { |b,e| items[b..e] } | |
build_articles(items_of_article) | |
end | |
def append(object) | |
items << Item.new(object) | |
end | |
def clean_lines(items) | |
first_article_end_index = items.index {|item| item.article_end?} | |
items.shift(first_article_end_index + 1) | |
end | |
def build_articles(items_of_article) | |
@articles = items_of_article.map do |items| | |
builder = Builder.new | |
builder.parse(items) | |
builder.article | |
end | |
end | |
end | |
class Item | |
attr_accessor :object | |
def initialize(object) | |
@object = object | |
end | |
def is_symbol? | |
object.is_a? Symbol | |
end | |
def to_finished? | |
object.start_with?("* DONE") | |
end | |
def to_title | |
object.gsub("* DONE ", "").gsub("* ", "") | |
end | |
def article_start? | |
is_symbol? && object == :title_begin | |
end | |
def article_end? | |
is_symbol? && object == :article_end | |
end | |
def to_property | |
key, *value = object.split(' ') | |
key, value = key.gsub(':', '').to_sym, value.join(" ") | |
{ key => value } | |
end | |
def to_raw_content | |
object.to_s | |
end | |
end | |
class Builder | |
attr_accessor :article, :current_state | |
def initialize | |
@article = Article.new | |
end | |
def parse(items) | |
items.each do |item| | |
if item.is_symbol? | |
@current_state = item.object | |
else | |
build_content(item) | |
end | |
end | |
end | |
def build_content(item) | |
case current_state | |
when :title_begin | |
article.set_title(item.to_title) | |
article.set_finished?(item.to_finished?) | |
when :properties_begin | |
article.append_properties(item.to_property) | |
when :properties_end | |
article.append_body(item.to_raw_content) | |
end | |
end | |
end | |
class WriterFactory | |
def self.build(article) | |
if article.posts.count > 1 | |
return SeriesWriter.new(article) | |
else | |
return SingleWriter.new(article) | |
end | |
end | |
end | |
class SingleWriter | |
attr_accessor :file_path, :article, :post, :draft, :content | |
def initialize(article) | |
@article = article | |
if article.finished | |
@post = article.posts.first | |
@file_path = article.properties[:EXPORT_FILE_NAME] | |
@draft = !article.finished | |
@content = @post.body.join("\n") | |
end | |
end | |
def index_url | |
file_path + "/" + "index.html" | |
end | |
def title | |
post.title | |
end | |
def build_pages | |
[ | |
Page.new(file_path, title, content) | |
] | |
end | |
end | |
class SeriesWriter | |
attr_accessor :base_file_path, :base_export_title, :article, :draft, :content, :posts | |
def initialize(article) | |
@articles = article | |
if article.finished | |
@posts = article.posts | |
else | |
@posts = [] | |
end | |
@base_file_path = article.properties[:BASE_FILE_PATH] | |
@base_export_title = article.properties[:BASE_EXPORT_TITLE] | |
end | |
def index_url | |
base_file_path | |
end | |
def build_pages | |
index_page_content = [] | |
post_pages = posts.map do |post| | |
file_path = post.properties[:EXPORT_FILE_NAME] | |
file_path = base_file_path + '/' + file_path | |
content = post.body.join("\n") | |
title = post.title | |
index_page_content << [file_path, title] | |
Page.new(file_path, title, content) | |
end | |
index_page = Page.new(base_file_path, base_export_title, index_page_content) | |
post_pages + [index_page] | |
end | |
end | |
class BlogGenerator | |
attr_accessor :base_path, :pages, :archive_pages | |
def initialize(base_path, pages) | |
@base_path = base_path | |
@pages = pages | |
@archive_pages = [] | |
end | |
def generator_archive_page | |
pages.each do |page| | |
archive_pages << page if is_root_page(page) | |
page.set_base_path(base_path) | |
end | |
end | |
def generate! | |
# write to file system | |
end | |
private | |
def is_root_page(page) | |
page.file_path.split("/").count == 1 | |
end | |
end | |
class Page | |
attr_accessor :file_path, :title, :content | |
def initialize(file_path, title, content) | |
@file_path = file_path | |
@title = title | |
@content = content | |
end | |
def set_base_path(base_path) | |
@file_path = base_path + "/" + file_path | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment