- <%= chapter.number %> + <%= book.hide_chapter_numbers ? '' : chapter.number %> <%= chapter.title %>
<%= book.title %>
+<%= book.title %>
+ + <%= book.version_description %> + -<%= book.description %>
+<%= book.short_description %>
+diff --git a/Gemfile b/Gemfile
index b3609d7..ab95612 100644
--- a/Gemfile
+++ b/Gemfile
@@ -13,7 +13,7 @@ gem 'cli-ui', '~> 1.3'
gem 'thor', '~> 1.0', '>= 1.0.1'
# Markdown processing
-gem 'redcarpet', '~> 3.5'
+gem 'commonmarker'
# HTTP Client
gem 'faraday'
diff --git a/Gemfile.lock b/Gemfile.lock
index e9c09d4..fc3738d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -41,6 +41,8 @@ GEM
benchmark (0.1.0)
cli-ui (1.3.0)
coderay (1.1.3)
+ commonmarker (0.21.0)
+ ruby-enum (~> 0.5)
concurrent-ruby (1.1.6)
debase (0.2.4.1)
debase-ruby_core_source (>= 0.10.2)
@@ -115,24 +117,25 @@ GEM
rbnacl (7.1.1)
ffi
rchardet (1.8.0)
- redcarpet (3.5.0)
regexp_parser (1.7.1)
reverse_markdown (2.0.0)
nokogiri
rexml (3.2.4)
- rubocop (0.89.0)
+ rubocop (0.89.1)
parallel (~> 1.10)
parser (>= 2.7.1.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml
- rubocop-ast (>= 0.1.0, < 1.0)
+ rubocop-ast (>= 0.3.0, < 1.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.3.0)
parser (>= 2.7.1.4)
ruby-debug-ide (0.7.2)
rake (>= 0.8.1)
+ ruby-enum (0.8.0)
+ i18n
ruby-progressbar (1.10.1)
ruby2_keywords (0.0.2)
sassc (2.4.0)
@@ -147,7 +150,7 @@ GEM
rack-protection (= 2.0.8.1)
tilt (~> 2.0)
slack-notifier (2.3.2)
- solargraph (0.39.13)
+ solargraph (0.39.15)
backport (~> 1.1)
benchmark
bundler (>= 1.17.2)
@@ -178,6 +181,7 @@ DEPENDENCIES
activesupport (~> 6.0)
aws-sdk-s3 (~> 1.64)
cli-ui (~> 1.3)
+ commonmarker
concurrent-ruby (~> 1.1)
debase (~> 0.2.4.1)
faraday
@@ -188,7 +192,6 @@ DEPENDENCIES
octokit!
rack-livereload
rbnacl
- redcarpet (~> 3.5)
rubocop (~> 0.81)
ruby-debug-ide (~> 0.7.2)
sassc
diff --git a/app/commands/robles_cli.rb b/app/commands/robles_cli.rb
index 6eafb74..2e40147 100644
--- a/app/commands/robles_cli.rb
+++ b/app/commands/robles_cli.rb
@@ -16,6 +16,7 @@ def self.exit_on_failure?
def render
book = runner.render(publish_file: options['publish_file'], local: options['local'])
p book.sections.first.chapters.last
+ p book.contributors.to_json
end
desc 'serve', 'starts local preview server'
diff --git a/app/lib/image_provider/markdown_image_extractor.rb b/app/lib/image_provider/markdown_image_extractor.rb
index 400d819..e60b90e 100644
--- a/app/lib/image_provider/markdown_image_extractor.rb
+++ b/app/lib/image_provider/markdown_image_extractor.rb
@@ -3,25 +3,6 @@
module ImageProvider
# Takes a markdown file, and returns all the images URLs
class MarkdownImageExtractor
- # Process markdown, and extract a list of images
- class MarkdownRenderer < Redcarpet::Render::Base
- attr_reader :images
-
- def initialize
- super
- @images = []
- end
-
- def reset
- @images = []
- end
-
- def image(link, _title, _alt_text)
- images << link
- nil
- end
- end
-
include Util::PathExtraction
def self.images_from(file)
@@ -29,21 +10,23 @@ def self.images_from(file)
end
def images
- md_renderer.reset
- renderer.render(markdown)
- md_renderer.images.map { |path| { relative_path: cleanpath(path), absolute_path: cleanpath(apply_path(path)) } }
+ [].tap do |images|
+ doc.walk do |node|
+ images << image_record(node.url) if node.type == :image
+ end
+ end
end
- def cleanpath(path)
- Pathname.new(path).cleanpath.to_s
+ def image_record(url)
+ { relative_path: cleanpath(url), absolute_path: cleanpath(apply_path(url)) }
end
- def renderer
- @renderer ||= Redcarpet::Markdown.new(md_renderer)
+ def cleanpath(path)
+ Pathname.new(path).cleanpath.to_s
end
- def md_renderer
- @md_renderer ||= MarkdownRenderer.new
+ def doc
+ @doc ||= CommonMarker.render_doc(markdown)
end
def markdown
diff --git a/app/lib/linting/file_existence_checker.rb b/app/lib/linting/file_existence_checker.rb
index 74ae0b9..141df99 100644
--- a/app/lib/linting/file_existence_checker.rb
+++ b/app/lib/linting/file_existence_checker.rb
@@ -5,6 +5,7 @@ module Linting
module FileExistenceChecker
# This method shouldn't depend on the underlying filesystem
def file_exists?(path, case_insensitive: false)
+ path = path.to_s
directory_contents = Dir[Pathname.new(path).dirname + '*']
return directory_contents.include?(path) unless case_insensitive
diff --git a/app/lib/linting/linter.rb b/app/lib/linting/linter.rb
index 2dd0bf6..bbecd86 100644
--- a/app/lib/linting/linter.rb
+++ b/app/lib/linting/linter.rb
@@ -43,6 +43,14 @@ def lint_with_ui(options:, show_ui: true) # rubocop:disable Metrics/MethodLength
with_spinner(title: 'Validating image references', show: show_ui) do
annotations.concat(Linting::ImageLinter.new(book: book).lint)
end
+
+ if file_exists?(vend_file)
+ with_spinner(title: 'Validating {{bold:vend.yaml}}', show: show_ui) do
+ annotations.concat(Linting::VendLinter.new(file: vend_file).lint)
+ end
+ else
+ puts CLI::UI.fmt('{{x}} Unable to find {{bold:vend.yaml}}--skipping validation.')
+ end
end
def with_spinner(title:, show: true)
@@ -87,6 +95,10 @@ def check_publish_file_exists
false
end
+ def vend_file
+ Pathname.new(file).dirname + 'vend.yaml'
+ end
+
def book # rubocop:disable Metrics/MethodLength
@book ||= begin
parser = Parser::Publish.new(file: file)
diff --git a/app/lib/linting/metadata/publish_attributes.rb b/app/lib/linting/metadata/publish_attributes.rb
index 492e95c..52854a5 100644
--- a/app/lib/linting/metadata/publish_attributes.rb
+++ b/app/lib/linting/metadata/publish_attributes.rb
@@ -4,8 +4,9 @@ module Linting
module Metadata
# Check for the required attributes in the publish.yaml file
class PublishAttributes
- REQUIRED_ATTRIBUTES = %i[sku edition title description released_at authors segments materials_url
- cover_image version_description difficulty platform language editor].freeze
+ REQUIRED_ATTRIBUTES = %i[sku edition title description_md released_at authors segments materials_url
+ cover_image version_description difficulty platform language editor
+ short_description].freeze
attr_reader :file, :attributes
diff --git a/app/lib/linting/vend_linter.rb b/app/lib/linting/vend_linter.rb
new file mode 100644
index 0000000..21ba6d0
--- /dev/null
+++ b/app/lib/linting/vend_linter.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+module Linting
+ # Lints vend.yaml
+ class VendLinter # rubocop:disable Metrics/ClassLength
+ VALID_PRICE_BANDS = %w(free 2020_full_book 2020_short_book 2020_deprecated_book).freeze
+ attr_reader :file, :vend_file
+
+ def initialize(file:)
+ @file = file
+ @annotations = []
+ end
+
+ def lint
+ load_file
+ return @annotations unless @annotations.empty?
+
+ check_price_band
+ check_total_percentage
+ return @annotations unless @annotations.empty?
+
+ check_for_razeware_user
+ @annotations
+ end
+
+ def check_price_band # rubocop:disable Metrics/MethodLength
+ if vend_file[:price_band].blank?
+ @annotations.push(
+ Annotation.new(
+ absolute_path: file,
+ annotation_level: 'warning',
+ start_line: 0,
+ end_line: 0,
+ title: 'Missing price_band attribute.',
+ message: 'The price_band attribute allows this book to be sold individually and should be included for all books.'
+ )
+ )
+ elsif !VALID_PRICE_BANDS.include?(vend_file[:price_band])
+ @annotations.push(
+ Annotation.new(
+ absolute_path: file,
+ annotation_level: 'failure',
+ start_line: 0,
+ end_line: 0,
+ title: 'Invalid price_band attribute.',
+ message: "price_band must be in (#{VALID_PRICE_BANDS.join(', ')})"
+ )
+ )
+ end
+ end
+
+ def check_total_percentage # rubocop:disable Metrics/MethodLength
+ if vend_file[:contributors].blank?
+ @annotations.push(
+ Annotation.new(
+ absolute_path: file,
+ annotation_level: 'warning',
+ start_line: 0,
+ end_line: 0,
+ title: 'Missing contributors attribute.',
+ message: 'The contributors attribute should be included for all books.'
+ )
+ )
+ return
+ end
+
+ total_contributor_percentage = vend_file[:contributors].sum(&:percentage)
+ return if total_contributor_percentage == 100
+
+ @annotations.push(
+ Annotation.new(
+ absolute_path: file,
+ annotation_level: 'failure',
+ start_line: 0,
+ end_line: 0,
+ message: "The sum of all contributor percentages should be 100. It is currently #{total_contributor_percentage}.",
+ title: 'Incorrect contribution sum.'
+ )
+ )
+ end
+
+ def check_for_razeware_user # rubocop:disable Metrics/MethodLength
+ return unless vend_file[:contributors].any? { |contributor| contributor.username == 'razeware' }
+
+ @annotations.push(
+ Annotation.new(
+ locate_razeware_username.merge(
+ absolute_path: file,
+ annotation_level: 'warning',
+ message: "The publisher's contribution should be attributed to the _razeware user, not the razeware user.",
+ title: 'Probable incorrect user.'
+ )
+ )
+ )
+ end
+
+ private
+
+ def load_file # rubocop:disable Metrics/MethodLength
+ @vend_file ||= begin
+ parser = Parser::Vend.new(file: file)
+ parser.parse
+ end
+ rescue Parser::Error => e
+ line_number = (e.message.match(/at line (\d+)/)&.captures&.first&.to_i || 0) + 1
+ @annotations.push(
+ Annotation.new(
+ absolute_path: e.file,
+ annotation_level: 'failure',
+ start_line: line_number,
+ end_line: line_number,
+ message: e.message,
+ title: 'Unable to parse contributors.'
+ )
+ )
+ end
+
+ def locate_razeware_username
+ IO.foreach(file).with_index do |line, line_number|
+ next unless line[/username:\s*razeware/]
+
+ start_column = line.index('razeware')
+ return {
+ start_line: line_number + 1,
+ end_line: line_number + 1,
+ start_column: start_column + 1,
+ end_column: start_column + 'razeware'.length + 1
+ }
+ end
+ end
+ end
+end
diff --git a/app/lib/parser/publish.rb b/app/lib/parser/publish.rb
index a0cf7c5..5afb96d 100644
--- a/app/lib/parser/publish.rb
+++ b/app/lib/parser/publish.rb
@@ -4,17 +4,20 @@ module Parser
# Parses a publish.yaml file, and returns a Book model object
class Publish
include Util::PathExtraction
+ include Linting::FileExistenceChecker
VALID_BOOK_ATTRIBUTES = %i[sku edition title description released_at materials_url
cover_image gallery_image twitter_card_image trailer_video_url
version_description professional difficulty platform
language editor domains categories who_is_this_for_md
- covered_concepts_md hide_chapter_numbers in_flux].freeze
+ covered_concepts_md hide_chapter_numbers in_flux forum_url
+ pages short_description recommended_skus].freeze
attr_reader :book
def parse
load_book_segments
+ load_vend_file
apply_additonal_metadata
update_authors_on_chapters
book
@@ -25,6 +28,10 @@ def load_book_segments
@book = Parser::BookSegments.new(file: segment_file).parse
end
+ def load_vend_file
+ book.assign_attributes(vend_file)
+ end
+
def apply_additonal_metadata
book.assign_attributes(additional_attributes)
book.root_path = root_directory
@@ -44,12 +51,20 @@ def publish_file
@publish_file ||= Psych.load_file(file).deep_symbolize_keys
end
+ def vend_file_path
+ Pathname.new(file).dirname + 'vend.yaml'
+ end
+
def authors
@authors = publish_file[:authors].map do |author|
Author.new(author)
end
end
+ def vend_file
+ @vend_file ||= file_exists?(vend_file_path) ? Parser::Vend.new(file: vend_file_path).parse : {}
+ end
+
def additional_attributes
@additional_attributes ||= publish_file.slice(*VALID_BOOK_ATTRIBUTES)
.assert_valid_keys(*VALID_BOOK_ATTRIBUTES)
diff --git a/app/lib/parser/vend.rb b/app/lib/parser/vend.rb
new file mode 100644
index 0000000..6ff03d6
--- /dev/null
+++ b/app/lib/parser/vend.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Parser
+ # Parses a vend.yaml file, and returns a hash of contributors & price_band
+ class Vend
+ include Util::PathExtraction
+
+ def parse
+ {
+ contributors: contributors,
+ price_band: vend_file[:price_band]
+ }
+ end
+
+ def contributors
+ return [] unless vend_file[:contributors].present?
+
+ vend_file[:contributors].map do |contributor_attributes|
+ Contributor.new(contributor_attributes)
+ end
+ end
+
+ private
+
+ def vend_file
+ @vend_file ||= Psych.load_file(file).deep_symbolize_keys
+ end
+ end
+end
diff --git a/app/lib/renderer/markdown_file_renderer.rb b/app/lib/renderer/markdown_file_renderer.rb
index 464bd19..9fe4e04 100644
--- a/app/lib/renderer/markdown_file_renderer.rb
+++ b/app/lib/renderer/markdown_file_renderer.rb
@@ -4,6 +4,7 @@ module Renderer
# Read a file and render the markdown
class MarkdownFileRenderer
include Util::Logging
+ include Parser::FrontmatterMetadataFinder
attr_reader :path
attr_reader :image_provider
@@ -15,27 +16,43 @@ def initialize(path:, image_provider: nil)
def render
logger.debug 'MarkdownFileRenderer::render'
- redcarpet.render(raw_content)
+ remove_h1(doc)
+ rw_renderer.render(doc)
+ end
+
+ def rw_renderer
+ @rw_renderer ||= Renderer::RWMarkdownRenderer.new(
+ options: %i[TABLE_PREFER_STYLE_ATTRIBUTES],
+ extensions: %i[table strikethrough autolink],
+ image_provider: image_provider,
+ root_path: root_directory
+ )
end
def raw_content
@raw_content ||= File.read(path)
end
- def redcarpet_renderer
- @redcarpet_renderer ||= RWMarkdownRenderer.new(with_toc_data: true,
- image_provider: image_provider,
- root_path: root_directory)
+ def preproccessed_markdown
+ @preproccessed_markdown ||= begin
+ removing_pagesetting_notation = raw_content.gsub(/\$\[=[=sp]=\]/, '')
+ without_metadata(removing_pagesetting_notation.each_line)
+ end
+ end
+
+ def doc
+ @doc ||= CommonMarker.render_doc(
+ preproccessed_markdown,
+ %i[SMART STRIKETHROUGH_DOUBLE_TILDE],
+ %i[table strikethrough autolink]
+ )
end
- def redcarpet
- @redcarpet ||= Redcarpet::Markdown.new(redcarpet_renderer,
- fenced_code_blocks: true,
- disable_indented_code_blocks: true,
- autolink: true,
- strikethrough: true,
- tables: true,
- hightlight: true)
+ def remove_h1(document)
+ document.walk do |node|
+ node.delete if node.type == :header && node.header_level.to_i == 1
+ end
+ document
end
def root_directory
diff --git a/app/lib/renderer/markdown_string_renderer.rb b/app/lib/renderer/markdown_string_renderer.rb
index 92ed234..e4e0e26 100644
--- a/app/lib/renderer/markdown_string_renderer.rb
+++ b/app/lib/renderer/markdown_string_renderer.rb
@@ -13,21 +13,15 @@ def initialize(content:)
def render
logger.debug 'MarkdownStringRenderer::render'
- redcarpet.render(content)
- end
-
- def redcarpet_renderer
- @redcarpet_renderer ||= RWMarkdownRenderer.new(with_toc_data: true)
- end
-
- def redcarpet
- @redcarpet ||= Redcarpet::Markdown.new(redcarpet_renderer,
- fenced_code_blocks: true,
- disable_indented_code_blocks: true,
- autolink: true,
- strikethrough: true,
- tables: true,
- hightlight: true)
+ doc = CommonMarker.render_doc(
+ content,
+ %i[SMART STRIKETHROUGH_DOUBLE_TILDE],
+ %i[table strikethrough autolink]
+ )
+ doc.to_html(
+ %i[TABLE_PREFER_STYLE_ATTRIBUTES],
+ %i[table strikethrough autolink]
+ )
end
end
end
diff --git a/app/lib/renderer/rw_markdown_renderer.rb b/app/lib/renderer/rw_markdown_renderer.rb
index cbc541b..cf75208 100644
--- a/app/lib/renderer/rw_markdown_renderer.rb
+++ b/app/lib/renderer/rw_markdown_renderer.rb
@@ -2,45 +2,32 @@
module Renderer
# Custom implementation of a markdown renderer for RW books
- class RWMarkdownRenderer < Redcarpet::Render::HTML
- include Redcarpet::Render::SmartyPants
- include Parser::FrontmatterMetadataFinder
+ class RWMarkdownRenderer < CommonMarker::HtmlRenderer
include Renderer::ImageAttributes
include Util::Logging
attr_reader :root_path
- def initialize(attributes = {})
+ def initialize(options: :DEFAULT, extensions: [], image_provider:, root_path:)
logger.debug 'RWMarkdownRenderer::initialize'
- super
- @image_provider = attributes[:image_provider]
- @root_path = attributes[:root_path]
+ super(options: options, extensions: extensions)
+ @image_provider = image_provider
+ @root_path = root_path
end
- def header(text, header_level)
- return nil if header_level == 1
+ def image(node)
+ return super(node) if image_provider.blank?
- "<%= section.title %>
+
+ <% section.chapters.each do |chapter| %>
+
+ <% end %>
+ <% end %>
+
<%= book.description %>
+<%= book.short_description %>
+