From 0ef25b3c65dd87a2ee08a426a548f4173252ced1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asbj=C3=B8rn=20Ulsberg?= Date: Mon, 13 Dec 2021 13:40:27 +0100 Subject: [PATCH 1/3] DX-1660: Use Jekyll's source directory Use Jekyll's source directory instead of its destination directory as a base directory for PlantUML themes to simplify the integration with Jekyll a whole lot. --- README.md | 2 +- .../jekyll_page_processor.rb | 103 ------------------ lib/kramdown-plantuml/jekyll_provider.rb | 56 +--------- lib/kramdown-plantuml/theme.rb | 6 +- lib/kramdown_html.rb | 24 ++-- spec/jekyll_page_processor_spec.rb | 99 ----------------- spec/jekyll_provider_spec.rb | 22 ---- spec/options_spec.rb | 32 +++--- spec/plantuml_diagram_spec.rb | 18 ++- spec/plantuml_error_spec.rb | 4 +- spec/theme_spec.rb | 22 ++-- 11 files changed, 62 insertions(+), 326 deletions(-) delete mode 100644 lib/kramdown-plantuml/jekyll_page_processor.rb delete mode 100644 spec/jekyll_page_processor_spec.rb diff --git a/README.md b/README.md index c5d3d6d7..20875ddc 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ kramdown: plantuml: theme: name: my-custom-theme - directory: path/to/themes + directory: spec/examples ``` ### Dimensions and Styling diff --git a/lib/kramdown-plantuml/jekyll_page_processor.rb b/lib/kramdown-plantuml/jekyll_page_processor.rb deleted file mode 100644 index 14aa0e13..00000000 --- a/lib/kramdown-plantuml/jekyll_page_processor.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -require 'htmlentities' -require 'json' -require_relative 'log_wrapper' - -module Kramdown - module PlantUml - # Processes Jekyll pages. - class JekyllPageProcessor - PROCESSED_KEY = :kramdown_plantuml_processed - - def initialize(page) - raise ArgumentError, 'page cannot be nil' if page.nil? - - @page = page - end - - def process(site_destination_directory) - @page.output = do_process - @page.data[PROCESSED_KEY] = true - @page.write(site_destination_directory) - end - - def should_process? - return false unless @page.output_ext == '.html' - - if !@page.data.nil? && @page.data.key?(PROCESSED_KEY) && @page.data[PROCESSED_KEY] - logger.debug "Skipping #{@page.path} because it has already been processed." - return false - end - - true - end - - class << self - def needle(plantuml, options) - hash = { 'plantuml' => plantuml, 'options' => options.to_h } - - <<~NEEDLE - - #{hash.to_json} - - NEEDLE - rescue StandardError => e - raise e if options.nil? || options.raise_errors? - - logger.error 'Error while placing needle.' - logger.error e.to_s - logger.debug_multiline plantuml - end - - def logger - @logger ||= ::Kramdown::PlantUml::LogWrapper.init - end - end - - private - - def do_process - logger.debug "Replacing Jekyll needles in #{@page.path}" - - html = @page.output - - return html if html.nil? || html.empty? || !html.is_a?(String) - - html.gsub(/(?.*?)/m) do - json = $LAST_MATCH_INFO ? $LAST_MATCH_INFO[:json] : nil - replace_needle(json) unless json.nil? - end - end - - def replace_needle(json) - logger.debug 'Replacing Jekyll needle.' - - needle_hash = JSON.parse(json) - options_hash = needle_hash['options'] - options = ::Kramdown::PlantUml::Options.new({ plantuml: options_hash }) - - begin - decode_and_convert(needle_hash, options) - rescue StandardError => e - raise e if options.raise_errors? - - logger.error 'Error while replacing Jekyll needle.' - logger.error e.to_s - logger.debug_multiline json - end - end - - def decode_and_convert(hash, options) - encoded_plantuml = hash['plantuml'] - plantuml = HTMLEntities.new.decode encoded_plantuml - diagram = ::Kramdown::PlantUml::PlantUmlDiagram.new(plantuml, options) - diagram.svg - end - - def logger - @logger ||= ::Kramdown::PlantUml::LogWrapper.init - end - end - end -end diff --git a/lib/kramdown-plantuml/jekyll_provider.rb b/lib/kramdown-plantuml/jekyll_provider.rb index 8c318c26..934d51e3 100644 --- a/lib/kramdown-plantuml/jekyll_provider.rb +++ b/lib/kramdown-plantuml/jekyll_provider.rb @@ -1,14 +1,13 @@ # frozen_string_literal: true require_relative 'log_wrapper' -require_relative 'jekyll_page_processor' module Kramdown module PlantUml # Provides an instance of Jekyll if available. module JekyllProvider class << self - attr_reader :site_destination_dir + attr_reader :site_source_dir def jekyll return @jekyll if defined? @jekyll @@ -16,60 +15,17 @@ def jekyll @jekyll = load_jekyll end - def install - return @installed = false if jekyll.nil? - - find_site_destination_dir - register_hook - @installed = true - end - - def installed? - @installed - end - - def needle(plantuml, options) - JekyllPageProcessor.needle(plantuml, options) - end - private - def find_site_destination_dir + def find_site_source_dir if jekyll.sites.nil? || jekyll.sites.empty? - logger.debug 'Jekyll detected, hooking into :site:post_write.' + logger.warn 'Jekyll detected, but no sites found.' return nil end - @site_destination_dir = jekyll.sites.first.dest - logger.debug "Jekyll detected, hooking into :site:post_write of '#{@site_destination_dir}'." - @site_destination_dir - end - - def register_hook - Jekyll::Hooks.register :site, :post_write do |site| - site_post_write(site) - end - end - - def site_post_write(site) - logger.debug 'Jekyll:site:post_write triggered.' - @site_destination_dir ||= site.dest - - site.pages.each do |page| - processor = JekyllPageProcessor.new(page) - - next unless processor.should_process? - - processor.process(site.dest) - end - - site.posts.each do |post| - processor = JekyllPageProcessor.new(post) - - next unless processor.should_process? - - processor.process(site.dest) - end + @site_source_dir = jekyll.sites.first.source + logger.debug "Jekyll detected, using '#{@site_source_dir}' as base directory." + @site_source_dir end def load_jekyll diff --git a/lib/kramdown-plantuml/theme.rb b/lib/kramdown-plantuml/theme.rb index 7d89d4f2..f63ed029 100644 --- a/lib/kramdown-plantuml/theme.rb +++ b/lib/kramdown-plantuml/theme.rb @@ -35,9 +35,9 @@ def apply(plantuml) def resolve(directory) jekyll = JekyllProvider - return directory if directory.nil? || directory.empty? || !jekyll.installed? + return directory if directory.nil? || directory.empty? - directory = File.absolute_path(directory, jekyll.site_destination_dir) + directory = File.absolute_path(directory, jekyll.site_source_dir) log_or_raise "The theme directory '#{directory}' cannot be found" unless Dir.exist?(directory) @@ -51,7 +51,7 @@ def resolve(directory) def log_or_raise(message) raise IOError, message if @raise_errors - logger.warn message + @logger.warn message end def theme(plantuml) diff --git a/lib/kramdown_html.rb b/lib/kramdown_html.rb index 09c8d1ae..d89f0728 100644 --- a/lib/kramdown_html.rb +++ b/lib/kramdown_html.rb @@ -6,7 +6,6 @@ require_relative 'kramdown-plantuml/plantuml_error' require_relative 'kramdown-plantuml/options' require_relative 'kramdown-plantuml/plantuml_diagram' -require_relative 'kramdown-plantuml/jekyll_provider' module Kramdown module Converter @@ -16,19 +15,9 @@ class Html alias super_convert_codeblock convert_codeblock def convert_codeblock(element, indent) - return super_convert_codeblock(element, indent) unless plantuml?(element) + return super_convert_codeblock(element, indent) unless plantuml? element - jekyll = ::Kramdown::PlantUml::JekyllProvider - - # If Jekyll is successfully loaded, we'll wait with converting the - # PlantUML diagram to SVG since a theme may be configured that needs to - # be copied to the assets directory before the PlantUML conversion can - # be performed. We therefore place a needle in the haystack that we will - # convert in the :site:pre_render hook. - options = ::Kramdown::PlantUml::Options.new(@options) - return jekyll.needle(element.value, options) if jekyll.installed? - - convert_plantuml(element.value, options) + convert_plantuml(element.value) end private @@ -37,14 +26,15 @@ def plantuml?(element) element.attr['class'] == 'language-plantuml' end - def convert_plantuml(plantuml, options) - diagram = ::Kramdown::PlantUml::PlantUmlDiagram.new(plantuml, options) + def convert_plantuml(plantuml) + puml_opts = ::Kramdown::PlantUml::Options.new(@options) + diagram = ::Kramdown::PlantUml::PlantUmlDiagram.new(plantuml, puml_opts) diagram.svg.to_s rescue StandardError => e - raise e if options.raise_errors? + raise e if puml_opts.nil? || puml_opts.raise_errors? logger = ::Kramdown::PlantUml::LogWrapper.init - logger.error "Error while replacing needle: #{e.inspect}" + logger.error "Error while converting diagram: #{e.inspect}" end end end diff --git a/spec/jekyll_page_processor_spec.rb b/spec/jekyll_page_processor_spec.rb deleted file mode 100644 index 97d6db98..00000000 --- a/spec/jekyll_page_processor_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require 'rspec/its' -require 'kramdown-plantuml/options' -require 'kramdown-plantuml/jekyll_page_processor' - -Options ||= Kramdown::PlantUml::Options -JekyllPageProcessor = ::Kramdown::PlantUml::JekyllPageProcessor - -describe JekyllPageProcessor do - let(:page) do - page = double('page') - allow(page).to receive(:output).and_return(output) - allow(page).to receive(:output=) - allow(page).to receive(:output_ext).and_return(output_ext) - allow(page).to receive(:data).and_return({}) - allow(page).to receive(:write) - allow(page).to receive(:path).and_return(File.join(__dir__, 'page.md')) - page - end - - subject { JekyllPageProcessor.new(page) } - - describe '#initialize' do - context 'when page is nil' do - let(:page) { nil } - it { expect { subject }.to raise_error(ArgumentError, 'page cannot be nil') } - end - end - - describe '#needle' do - subject { JekyllPageProcessor.needle(plantuml, options) } - - context 'with nil :options' do - let(:options) { nil } - let(:plantuml) { "@startuml\n@enduml" } - - it do - is_expected.to eq <<~NEEDLE - - {"plantuml":"@startuml\\n@enduml","options":{}} - - NEEDLE - end - end - - context 'with nil :plantuml' do - let(:options) { Options.new } - let(:plantuml) { nil } - - it do - is_expected.to eq <<~NEEDLE - - {"plantuml":null,"options":{}} - - NEEDLE - end - end - - context 'with valid :options and :plantuml' do - let(:options) { { theme: { name: 'custom' } } } - let(:plantuml) { "@startuml\n@enduml" } - - it do - is_expected.to eq <<~NEEDLE - - {"plantuml":"@startuml\\n@enduml","options":{"theme":{"name":"custom"}}} - - NEEDLE - end - end - end - - describe '#process' do - before { subject.process(__dir__) } - - context 'with HTML output' do - let(:output) { '

Hello

' } - let(:output_ext) { '.html' } - its(:should_process?) { is_expected.to eq false } - it { expect(page.output).to eq output } - end - end - - describe '#should_process?' do - context 'with HTML output' do - let(:output) { '

Hello

' } - let(:output_ext) { '.html' } - its(:should_process?) { is_expected.to eq true } - it { expect(page.output).to eq output } - end - - context 'with CSS output' do - let(:output) { 'body { display: none }' } - let(:output_ext) { '.css' } - its(:should_process?) { is_expected.to eq false } - end - end -end diff --git a/spec/jekyll_provider_spec.rb b/spec/jekyll_provider_spec.rb index 8af389ae..a3392463 100644 --- a/spec/jekyll_provider_spec.rb +++ b/spec/jekyll_provider_spec.rb @@ -15,32 +15,10 @@ context 'without jekyll' do its (:jekyll) { is_expected.to be_nil } - its (:install) { is_expected.to be false } - its (:installed?) { is_expected.to be false } - - describe '#needle' do - subject { JekyllProvider.needle(plantuml, options) } - - context 'when plantuml is nil' do - it { is_expected.to match(/.*/m) } - end - - context 'when plantuml is valid' do - let (:plantuml) { plantuml_file_contents } - it { is_expected.to match(/.*@startuml.*@enduml.*/m) } - end - - context 'when options has theme' do - let (:options) { Options.new({ plantuml: { theme: { name: 'spacelab'} } }) } - it { is_expected.to match(/.*spacelab.*/m) } - end - end end context 'with jekyll', :jekyll do its (:jekyll) { is_expected.not_to be_nil } - its (:install) { is_expected.to be true } - its (:installed?) { is_expected.to be true } describe 'jekyll build' do jekyll_source = File.join(__dir__, 'examples') diff --git a/spec/options_spec.rb b/spec/options_spec.rb index 74811f4c..0aa9dc42 100644 --- a/spec/options_spec.rb +++ b/spec/options_spec.rb @@ -56,13 +56,13 @@ end context 'with :theme :name' do - let(:hash) { { plantuml: { theme: { name: 'custom' } } } } - its(:to_h) { is_expected.to eq({ theme: { name: 'custom' } }) } + let(:hash) { { plantuml: { theme: { name: 'c2a3b0' } } } } + its(:to_h) { is_expected.to eq({ theme: { name: 'c2a3b0' } }) } end context 'with nil :directory' do - let(:hash) { { plantuml: { theme: { name: 'custom', directory: nil } } } } - its(:to_h) { is_expected.to eq({ theme: { name: 'custom', directory: nil } }) } + let(:hash) { { plantuml: { theme: { name: 'c2a3b0', directory: nil } } } } + its(:to_h) { is_expected.to eq({ theme: { name: 'c2a3b0', directory: nil } }) } end context 'with nil :raise_errors' do @@ -76,29 +76,29 @@ end context 'with symbolic option keys' do - let(:hash) { { plantuml: { theme: { name: 'custom', directory: 'path/to/themes' }, raise_errors: false, scale: 0.8 } } } - its(:theme_name) { is_expected.to eq 'custom' } - its(:theme_directory) { is_expected.to eq 'path/to/themes' } + let(:hash) { { plantuml: { theme: { name: 'c2a3b0', directory: 'spec/examples' }, raise_errors: false, scale: 0.8 } } } + its(:theme_name) { is_expected.to eq 'c2a3b0' } + its(:theme_directory) { is_expected.to eq 'spec/examples' } its(:raise_errors?) { is_expected.to be false } its(:scale) { is_expected.to eq 0.8 } - its(:to_h) { is_expected.to eq({ theme: { name: 'custom', directory: 'path/to/themes'}, raise_errors: false, scale: 0.8 }) } + its(:to_h) { is_expected.to eq({ theme: { name: 'c2a3b0', directory: 'spec/examples'}, raise_errors: false, scale: 0.8 }) } end context 'with mixed option keys' do - let(:hash) { { plantuml: { theme: { 'name' => 'custom', 'directory' => 'path/to/themes' }, 'raise_errors' => false, scale: '0.8' } } } - its(:theme_name) { is_expected.to eq 'custom' } - its(:theme_directory) { is_expected.to eq 'path/to/themes' } + let(:hash) { { plantuml: { theme: { 'name' => 'c2a3b0', 'directory' => 'spec/examples' }, 'raise_errors' => false, scale: '0.8' } } } + its(:theme_name) { is_expected.to eq 'c2a3b0' } + its(:theme_directory) { is_expected.to eq 'spec/examples' } its(:raise_errors?) { is_expected.to be false } its(:scale) { is_expected.to eq '0.8' } - its(:to_h) { is_expected.to eq({ theme: { name: 'custom', directory: 'path/to/themes'}, raise_errors: false, scale: '0.8' }) } + its(:to_h) { is_expected.to eq({ theme: { name: 'c2a3b0', directory: 'spec/examples'}, raise_errors: false, scale: '0.8' }) } end context 'with string option keys' do - let(:hash) { { 'plantuml' => { 'theme' => { 'name' => 'custom', 'directory' => 'path/to/themes' }, 'raise_errors' => false, 'scale' => '0.8' } } } - its(:theme_name) { is_expected.to eq 'custom' } - its(:theme_directory) { is_expected.to eq 'path/to/themes' } + let(:hash) { { 'plantuml' => { 'theme' => { 'name' => 'c2a3b0', 'directory' => 'spec/examples' }, 'raise_errors' => false, 'scale' => '0.8' } } } + its(:theme_name) { is_expected.to eq 'c2a3b0' } + its(:theme_directory) { is_expected.to eq 'spec/examples' } its(:raise_errors?) { is_expected.to be false } its(:scale) { is_expected.to eq '0.8' } - its(:to_h) { is_expected.to eq({ theme: { name: 'custom', directory: 'path/to/themes' }, raise_errors: false, scale: '0.8' })} + its(:to_h) { is_expected.to eq({ theme: { name: 'c2a3b0', directory: 'spec/examples' }, raise_errors: false, scale: '0.8' })} end end diff --git a/spec/plantuml_diagram_spec.rb b/spec/plantuml_diagram_spec.rb index 93390df8..20037928 100644 --- a/spec/plantuml_diagram_spec.rb +++ b/spec/plantuml_diagram_spec.rb @@ -63,12 +63,26 @@ end end - context 'with non-existing theme' do + context 'with non-existing theme directory' do + let(:plantuml) { "@startuml\n@enduml" } + let(:hash) { { plantuml: { theme: { name: 'xyz', directory: 'spec/examples' } } } } + + its(:svg) do + will raise_error(IOError, /The theme '.*spec\/examples\/puml-theme-xyz.puml' cannot be found/) + end + + context ('with raise_errors: false') do + let(:hash) { { plantuml: { raise_errors: false } } } + its(:svg) { will_not raise_error } + end + end + + context 'with non-existing theme directory' do let(:plantuml) { "@startuml\n@enduml" } let(:hash) { { plantuml: { theme: { name: 'xyz', directory: 'assets' } } } } its(:svg) do - will raise_error(Kramdown::PlantUml::PlantUmlError, /theme 'xyz' can't be found in the directory 'assets'/) + will raise_error(IOError, /The theme directory '.*\/assets' cannot be found/) end context ('with raise_errors: false') do diff --git a/spec/plantuml_error_spec.rb b/spec/plantuml_error_spec.rb index 150fa005..f2253a4d 100644 --- a/spec/plantuml_error_spec.rb +++ b/spec/plantuml_error_spec.rb @@ -39,7 +39,7 @@ end context 'non-existent theme' do - let(:options) { Options.new({ plantuml: { theme: { name: 'xyz', directory: 'assets' } }}) } + let(:options) { Options.new({ plantuml: { theme: { name: 'xyz', directory: 'assets' }, raise_errors: false } }) } let(:stderr) { <<~STDERR java.lang.NullPointerException at java.base/java.io.Reader.(Reader.java:167) @@ -64,7 +64,7 @@ } its(:message) { - is_expected.to match(/theme 'xyz' can't be found in the directory 'assets'/) + is_expected.to match(/theme 'xyz' can't be found in the directory '.*assets'/) is_expected.to match(/The error received from PlantUML was:/) } end diff --git a/spec/theme_spec.rb b/spec/theme_spec.rb index d5a5c431..fdbba2ee 100644 --- a/spec/theme_spec.rb +++ b/spec/theme_spec.rb @@ -13,23 +13,23 @@ subject { Theme.new(options) } context 'with symbolic option keys' do - let(:options) { Options.new({ plantuml: { theme: { name: 'custom', directory: 'path/to/themes' }, scale: 0.8 } }) } - its(:name) { is_expected.to eq('custom') } - its(:directory) { is_expected.to eq('path/to/themes') } + let(:options) { Options.new({ plantuml: { theme: { name: 'c2a3b0', directory: 'spec/examples' }, scale: 0.8 } }) } + its(:name) { is_expected.to eq('c2a3b0') } + its(:directory) { is_expected.to match(/spec\/examples$/) } its(:scale) { is_expected.to eq(0.8) } end context 'with mixed option keys' do - let(:options) { Options.new({ plantuml: { theme: { 'name' => 'custom', 'directory' => 'path/to/themes' }, scale: '0.8' } }) } - its(:name) { is_expected.to eq('custom') } - its(:directory) { is_expected.to eq('path/to/themes') } + let(:options) { Options.new({ plantuml: { theme: { 'name' => 'c2a3b0', 'directory' => 'spec/examples' }, scale: '0.8' } }) } + its(:name) { is_expected.to eq('c2a3b0') } + its(:directory) { is_expected.to match(/spec\/examples$/) } its(:scale) { is_expected.to eq('0.8') } end context 'with string option keys' do - let(:options) { Options.new({ 'plantuml' => { 'theme' => { 'name' => 'custom', 'directory' => 'path/to/themes' }, 'scale' => '0.8' } }) } - its(:name) { is_expected.to eq('custom') } - its(:directory) { is_expected.to eq('path/to/themes') } + let(:options) { Options.new({ 'plantuml' => { 'theme' => { 'name' => 'c2a3b0', 'directory' => 'spec/examples' }, 'scale' => '0.8' } }) } + its(:name) { is_expected.to eq('c2a3b0') } + its(:directory) { is_expected.to match(/spec\/examples$/) } its(:scale) { is_expected.to eq('0.8') } end end @@ -61,9 +61,9 @@ end context 'with custom theme' do - let(:options) { Options.new({ plantuml: { theme: { name: 'custom', directory: 'path/to/themes' } } }) } + let(:options) { Options.new({ plantuml: { theme: { name: 'c2a3b0', directory: 'spec/examples' } } }) } let(:plantuml) { "@startuml\nactor A\n@enduml" } - it { is_expected.to eq("@startuml\n!theme custom from path/to/themes\nactor A\n@enduml") } + it { is_expected.to match(/@startuml\n!theme c2a3b0 from .*spec\/examples\nactor A\n@enduml/) } end context 'with scale' do From 22826a377aa33da68ea257fedaca753a09555832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asbj=C3=B8rn=20Ulsberg?= Date: Mon, 13 Dec 2021 13:49:30 +0100 Subject: [PATCH 2/3] DX-1660: Strip PlantUML diagram of spaces Extranous spaces around diagrams makes PlantUML choke, so let's have them removed. --- lib/kramdown-plantuml/plantuml_diagram.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kramdown-plantuml/plantuml_diagram.rb b/lib/kramdown-plantuml/plantuml_diagram.rb index 87d60aa1..de627429 100644 --- a/lib/kramdown-plantuml/plantuml_diagram.rb +++ b/lib/kramdown-plantuml/plantuml_diagram.rb @@ -18,7 +18,7 @@ def initialize(plantuml, options) raise ArgumentError, 'options cannot be nil' if options.nil? raise ArgumentError, "options must be a '#{Options}'." unless options.is_a?(Options) - @plantuml = plantuml + @plantuml = plantuml.strip unless plantuml.nil? @options = options @theme = Theme.new(options) @logger = LogWrapper.init From 6d11dac55435931696ee2d5a0d56f9dd7cce942d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asbj=C3=B8rn=20Ulsberg?= Date: Mon, 13 Dec 2021 16:08:33 +0100 Subject: [PATCH 3/3] DX-1660: Remove JekyllProvider.install There's no need to install ourselves into Jekyll anymore. --- lib/kramdown-plantuml.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/kramdown-plantuml.rb b/lib/kramdown-plantuml.rb index 47308d58..b5d6e2ca 100644 --- a/lib/kramdown-plantuml.rb +++ b/lib/kramdown-plantuml.rb @@ -1,6 +1,3 @@ # frozen_string_literal: true require_relative 'kramdown_html' -require_relative 'kramdown-plantuml/jekyll_provider' - -::Kramdown::PlantUml::JekyllProvider.install