diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 8cda4fcee..55781c0f3 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -7,7 +7,9 @@ For a detailed view of what has changed, refer to the {url-repo}/commits/main[co == Unreleased -_No changes since previous release._ +Improvements:: + +* replace OpenStruct with internal ThemeData class for storing theme data (#2535) == 2.3.18 (2024-07-27) - @mojavelinux diff --git a/Gemfile b/Gemfile index e46a41e10..fc22f4e55 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem 'asciidoctor', ENV['ASCIIDOCTOR_VERSION'], require: false if ENV.key? 'ASCII gem 'asciidoctor-diagram', ENV['ASCIIDOCTOR_DIAGRAM_VERSION'], require: false if ENV.key? 'ASCIIDOCTOR_DIAGRAM_VERSION' gem 'asciidoctor-kroki', ENV['ASCIIDOCTOR_KROKI_VERSION'], require: false if ENV.key? 'ASCIIDOCTOR_KROKI_VERSION' gem 'coderay', '~> 1.1.0', require: false +gem 'logger', require: false if (Gem::Version.new RUBY_VERSION) > (Gem::Version.new '3.3.4') gem 'open-uri-cached', '~> 1.0.0', require: false gem 'prawn-gmagick', ENV['PRAWN_GMAGICK_VERSION'], require: false if (ENV.key? 'PRAWN_GMAGICK_VERSION') && RUBY_ENGINE == 'ruby' gem 'pygments.rb', ENV['PYGMENTS_VERSION'], require: false if ENV.key? 'PYGMENTS_VERSION' @@ -35,5 +36,6 @@ end group :coverage do gem 'deep-cover-core', '~> 1.1.0', require: false + gem 'json', '~> 2.7', require: false if (Gem::Version.new RUBY_VERSION) > (Gem::Version.new '3.3.4') gem 'simplecov', '~> 0.22.0', require: false end diff --git a/lib/asciidoctor/pdf/theme_data.rb b/lib/asciidoctor/pdf/theme_data.rb new file mode 100644 index 000000000..b487cfc6d --- /dev/null +++ b/lib/asciidoctor/pdf/theme_data.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Asciidoctor + module PDF + class ThemeData + attr_reader :table + + def initialize data = nil + @table = (data || {}).transform_keys(&:to_sym) + end + + def [] name + @table[name.to_sym] + end + + def []= name, value + @table[name.to_sym] = value + end + + def each_pair &block + @table.each_pair(&block) + end + + def eql? other + @table.to_h.eql? other.to_h + end + + def delete_field name + @table.delete name + end + + def dup + ThemeData.new @table + end + + def method_missing name, *args + if (name_str = name.to_s).end_with? '=' + @table[name_str.chop.to_sym] = args[0] + else + @table[name] + end + end + + def respond_to? name, _include_all = false + @table.key? name.to_sym + end + + def respond_to_missing? name, _include_all = false + @table.key? name.to_sym + end + + def to_h + @table + end + end + end +end diff --git a/lib/asciidoctor/pdf/theme_loader.rb b/lib/asciidoctor/pdf/theme_loader.rb index 12dffc8d2..30613aa06 100644 --- a/lib/asciidoctor/pdf/theme_loader.rb +++ b/lib/asciidoctor/pdf/theme_loader.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'ostruct' +require_relative 'theme_data' require_relative 'measurements' module Asciidoctor @@ -69,7 +69,7 @@ def self.resolve_theme_asset asset_path, theme_dir = nil # NOTE: base theme is loaded "as is" (no post-processing) def self.load_base_theme ::File.open BaseThemePath, mode: 'r:UTF-8' do |io| - (::OpenStruct.new ::YAML.safe_load io, filename: BaseThemePath).tap {|theme| theme.__dir__ = ThemesDir } + (ThemeData.new ::YAML.safe_load io, filename: BaseThemePath).tap {|theme| theme.__dir__ = ThemesDir } end end @@ -78,7 +78,7 @@ def self.load_theme theme_name = nil, theme_dir = nil if theme_path == BaseThemePath load_base_theme else - theme_data = load_file theme_path, (::OpenStruct.new base_font_size: 12), theme_dir + theme_data = load_file theme_path, (ThemeData.new base_font_size: 12), theme_dir unless (::File.dirname theme_path) == ThemesDir theme_data.base_text_align ||= 'left' theme_data.base_line_height ||= 1 @@ -102,12 +102,12 @@ def self.load_file filename, theme_data = nil, theme_dir = nil line.sub(HexColorEntryRx) { %(#{(m = $~)[:k]}: #{m[:h] || (m[:k].end_with? 'color') ? "'#{m[:v]}'" : m[:v]}) } end.join unless (::File.dirname filename) == ThemesDir yaml_data = ::YAML.safe_load data, aliases: true, filename: filename - (loaded = (theme_data ||= ::OpenStruct.new).__loaded__ ||= ::Set.new).add filename + (loaded = (theme_data ||= ThemeData.new).__loaded__ ||= ::Set.new).add filename if ::Hash === yaml_data && (extends = yaml_data.delete 'extends') (Array extends).each do |extend_path| extend_path = extend_path.slice 0, extend_path.length - 11 if (force = extend_path.end_with? ' !important') if extend_path == 'base' - theme_data = ::OpenStruct.new theme_data.to_h.merge load_base_theme.to_h if (loaded.add? 'base') || force + theme_data = ThemeData.new theme_data.to_h.merge load_base_theme.to_h if (loaded.add? 'base') || force next elsif BundledThemeNames.include? extend_path extend_path, extend_theme_dir = resolve_theme_file extend_path, ThemesDir @@ -123,7 +123,7 @@ def self.load_file filename, theme_data = nil, theme_dir = nil end def load hash, theme_data = nil - ::Hash === hash ? hash.reduce(theme_data || ::OpenStruct.new) {|data, (key, val)| process_entry key, val, data, true } : (theme_data || ::OpenStruct.new) + ::Hash === hash ? hash.reduce(theme_data || ThemeData.new) {|data, (key, val)| process_entry key, val, data, true } : (theme_data || ThemeData.new) end private diff --git a/spec/theme_loader_spec.rb b/spec/theme_loader_spec.rb index 2916bd938..68b8b21b7 100644 --- a/spec/theme_loader_spec.rb +++ b/spec/theme_loader_spec.rb @@ -9,26 +9,26 @@ it 'should not fail if theme data is empty' do theme = subject.new.load '' (expect theme).not_to be_nil - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.to_h).to be_empty end it 'should not fail if theme data is falsy' do theme = subject.new.load false (expect theme).not_to be_nil - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.to_h).to be_empty end # NOTE: this API is not used by the converter it 'should use specified theme data if raw theme data is nil' do - theme_data = OpenStruct.new + theme_data = Asciidoctor::PDF::ThemeData.new theme_data.base_font_color = '222222' theme = subject.new.load nil, theme_data (expect theme).to be theme_data end - it 'should store flattened keys in OpenStruct' do + it 'should store flattened keys in Asciidoctor::PDF::ThemeData' do theme_data = YAML.safe_load <<~'EOS' page: size: A4 @@ -41,7 +41,7 @@ font_style: bold EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme).to respond_to :page_size (expect theme).to respond_to :base_font_family (expect theme).to respond_to :base_border_width @@ -62,7 +62,7 @@ size: 24 EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.admonition_icon_tip).to be_a Hash (expect theme.admonition_icon_tip).to eql name: 'far-lightbulb', stroke_color: 'FFFF00', size: 24 (expect theme.admonition_icon_note).to be_a Hash @@ -76,7 +76,7 @@ advice: ~ EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.admonition_icon_advice).to be_nil end @@ -93,7 +93,7 @@ stroke-color: FFFF00 EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme).to respond_to :page_size (expect theme).to respond_to :base_font_family (expect theme).to respond_to :abstract_title_font_size @@ -112,7 +112,7 @@ color: 0000ff EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme).to respond_to 'role_flaming-red_font_color' (expect theme['role_flaming-red_font_color']).to eql 'FF0000' (expect theme).to respond_to 'role_so-very-blue_font_color' @@ -126,7 +126,7 @@ font-style: bold EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme).to respond_to 'role_BOLD_font_style' (expect theme['role_BOLD_font_style']).to eql 'bold' end @@ -148,7 +148,7 @@ content: 2 * 2 EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.menu_caret_content).to eql '[">"]' (expect theme.ulist_marker_disc_content).to eql '0' (expect theme.footer_recto_left_content).to eql 'true' @@ -172,7 +172,7 @@ text-align: $heading-align EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.base_align).to be_nil (expect theme.base_text_align).to eql 'center' (expect theme.heading_align).to be_nil @@ -197,7 +197,7 @@ end: $table-caption-side EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.table_caption_side).to be_nil (expect theme.table_caption_end).to eql 'bottom' (expect theme.image_caption_end).to eql 'bottom' @@ -214,7 +214,7 @@ item-spacing: $outline_list_item_spacing / 2 EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.outline_list_item_spacing).to be_nil (expect theme.list_item_spacing).to eql 6 (expect theme.footnotes_margin_top).to eql theme.list_item_spacing @@ -232,7 +232,7 @@ font-color: $blockquote-font-color EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.blockquote_font_color).to be_nil (expect theme.quote_font_color).to eql '4A4A4A' (expect theme.quote_border_color).to eql theme.quote_font_color @@ -249,7 +249,7 @@ font-color: $key-border-color EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.key_border_color).to be_nil (expect theme.kbd_border_color).to eql 'CCCCCC' (expect theme.kbd_font_color).to eql theme.kbd_border_color @@ -265,7 +265,7 @@ font-family: $literal-font-family EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.literal_font_family).to be_nil (expect theme.codespan_font_family).to eql 'M+ 1mn' (expect theme.verse_font_family).to eql 'M+ 1mn' @@ -284,7 +284,7 @@ padding: [6, 12, -6, 14] EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.example_padding).to eql [12, 12, 12, 12] (expect theme.quote_padding).to eql [0, 12, 0, 14] (expect theme.sidebar_padding).to eql [12, 12, 12, 12] @@ -297,7 +297,7 @@ padding: [-3, 12, -3, 14] EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.quote_padding).to eql [-3, 12, -3, 14] end @@ -315,7 +315,7 @@ content: $page_size EOS theme = subject.new.load theme_data - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.footer_verso_left_content).to eql '2 * 12' (expect theme.footer_verso_right_content).to eql 'A4' end @@ -477,16 +477,16 @@ describe '.load_file' do it 'should not fail if theme file is empty' do theme = subject.load_file fixture_file 'empty-theme.yml' - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData theme.delete_field :__loaded__ - (expect theme).to eql OpenStruct.new + (expect theme).to eql Asciidoctor::PDF::ThemeData.new end it 'should not fail if theme file resolves to nil' do theme = subject.load_file fixture_file 'nil-theme.yml' - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData theme.delete_field :__loaded__ - (expect theme).to eql OpenStruct.new + (expect theme).to eql Asciidoctor::PDF::ThemeData.new end it 'should throw error that includes filename and reason if theme is indented using tabs' do @@ -726,7 +726,7 @@ it 'should load base theme if theme name is base' do theme = subject.load_theme 'base' (expect theme).not_to be_nil - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.base_font_family).to eql 'Helvetica' (expect theme.codespan_font_family).to eql 'Courier' (expect theme).to eql subject.load_base_theme @@ -735,7 +735,7 @@ it 'should load default theme if no arguments are given' do theme = subject.load_theme (expect theme).not_to be_nil - (expect theme).to be_an OpenStruct + (expect theme).to be_an Asciidoctor::PDF::ThemeData (expect theme.base_font_family).to eql 'Noto Serif' (expect theme.link_font_color).to eql '428BCA' end