From 63ff821c7c0df53e836bab1044cda65749dc72ed Mon Sep 17 00:00:00 2001 From: Yaroslav Konoplov Date: Sun, 30 Jul 2017 14:29:18 +0300 Subject: [PATCH] Allow to switch used Jade/Pug version! --- .gitignore | 2 + .rubocop.yml | 65 ++++++++++++ .yardopts | 4 + Gemfile | 4 +- Gemfile.lock | 6 ++ Rakefile | 4 +- lib/jade-pug/base.rb | 116 ++++++++++++++++++++++ lib/jade-pug/compiler.rb | 91 +++++++++++++++++ lib/jade-pug/config.rb | 56 +++++++++++ lib/jade-pug/errors/compilation-error.rb | 14 +++ lib/jade-pug/errors/compiler-error.rb | 13 +++ lib/jade-pug/errors/executable-error.rb | 13 +++ lib/jade-pug/shipped-compiler.rb | 71 +++++++++++++ lib/jade-pug/system-compiler.rb | 107 ++++++++++++++++++++ lib/jade-ruby/compilation-essentials.rb | 24 +++++ lib/jade-ruby/compile.rb | 79 --------------- lib/jade-ruby/config.rb | 24 ++--- lib/jade-ruby/errors/compilation-error.rb | 14 +++ lib/jade-ruby/errors/compiler-error.rb | 13 +++ lib/jade-ruby/errors/executable-error.rb | 13 +++ lib/jade-ruby/shipped-compiler.rb | 15 +++ lib/jade-ruby/system-compiler.rb | 47 +++++++++ lib/pug-ruby.rb | 55 +++++++++- lib/pug-ruby/compilation-essentials.rb | 28 ++++++ lib/pug-ruby/compile.rb | 88 ---------------- lib/pug-ruby/config.rb | 29 +++--- lib/pug-ruby/errors/compilation-error.rb | 14 +++ lib/pug-ruby/errors/compiler-error.rb | 13 +++ lib/pug-ruby/errors/executable-error.rb | 13 +++ lib/pug-ruby/shipped-compiler.rb | 15 +++ lib/pug-ruby/system-compiler.rb | 68 +++++++++++++ pug-ruby.gemspec | 25 +++-- test/helper.rb | 16 ++- test/test-jade.rb | 50 ++++++---- test/test-pug.rb | 53 ++++++---- 35 files changed, 1005 insertions(+), 257 deletions(-) create mode 100644 .rubocop.yml create mode 100644 .yardopts create mode 100644 lib/jade-pug/base.rb create mode 100644 lib/jade-pug/compiler.rb create mode 100644 lib/jade-pug/config.rb create mode 100644 lib/jade-pug/errors/compilation-error.rb create mode 100644 lib/jade-pug/errors/compiler-error.rb create mode 100644 lib/jade-pug/errors/executable-error.rb create mode 100644 lib/jade-pug/shipped-compiler.rb create mode 100644 lib/jade-pug/system-compiler.rb create mode 100644 lib/jade-ruby/compilation-essentials.rb delete mode 100644 lib/jade-ruby/compile.rb create mode 100644 lib/jade-ruby/errors/compilation-error.rb create mode 100644 lib/jade-ruby/errors/compiler-error.rb create mode 100644 lib/jade-ruby/errors/executable-error.rb create mode 100644 lib/jade-ruby/shipped-compiler.rb create mode 100644 lib/jade-ruby/system-compiler.rb create mode 100644 lib/pug-ruby/compilation-essentials.rb delete mode 100644 lib/pug-ruby/compile.rb create mode 100644 lib/pug-ruby/errors/compilation-error.rb create mode 100644 lib/pug-ruby/errors/compiler-error.rb create mode 100644 lib/pug-ruby/errors/executable-error.rb create mode 100644 lib/pug-ruby/shipped-compiler.rb create mode 100644 lib/pug-ruby/system-compiler.rb diff --git a/.gitignore b/.gitignore index c0f7d8c..b499f4a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ pkg/ tmp/ /.idea/ /node_modules/ +/.yardoc/ +/doc/ diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..97fb2ae --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,65 @@ +Style/StringLiterals: + EnforcedStyle: double_quotes + +Metrics/LineLength: + Enabled: false + Max: 110 + +Style/FileName: + Regex: !ruby/regexp /\A[-a-z0-9]+\z/ + +Layout/CaseIndentation: + EnforcedStyle: end + +Layout/AccessModifierIndentation: + EnforcedStyle: outdent + +Layout/SpaceInsideArrayPercentLiteral: + Enabled: false + +Layout/EmptyLinesAroundClassBody: + Enabled: false + +Style/PerlBackrefs: + Enabled: false + +Bundler/OrderedGems: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Style/EmptyMethod: + Enabled: false + +Lint/UselessAssignment: + Enabled: false + +Lint/EndAlignment: + EnforcedStyleAlignWith: variable + +Lint/UnusedBlockArgument: + Enabled: false + +Metrics/MethodLength: + Max: 20 + +Layout/ExtraSpacing: + Enabled: false + AllowForAlignment: true + ForceEqualSignAlignment: true + +Style/PercentLiteralDelimiters: + PreferredDelimiters: + default: '[]' + '%i': '[]' + '%': '{}' + +Layout/AlignParameters: + Enabled: false + +Lint/UnusedMethodArgument: + Enabled: false + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..2d05792 --- /dev/null +++ b/.yardopts @@ -0,0 +1,4 @@ +--protected +--private +- +lib/**/*.rb diff --git a/Gemfile b/Gemfile index ec878ee..198a32d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,8 @@ # encoding: UTF-8 # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" gemspec -gem 'test-unit', '~> 3.1', require: 'test/unit' +gem "test-unit", "~> 3.1", require: "test/unit" diff --git a/Gemfile.lock b/Gemfile.lock index d5a0fde..2055ee3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,12 +2,18 @@ PATH remote: . specs: pug-ruby (1.0.2) + execjs (~> 2.0) + memoist (~> 0.15) + regexp-match-polyfill (~> 1.0) GEM remote: https://rubygems.org/ specs: + execjs (2.7.0) + memoist (0.16.0) power_assert (1.0.2) rake (10.5.0) + regexp-match-polyfill (1.0.1) test-unit (3.2.5) power_assert diff --git a/Rakefile b/Rakefile index 619791e..744fd8b 100644 --- a/Rakefile +++ b/Rakefile @@ -13,9 +13,9 @@ namespace "javascripts" do require "open3" def run(*args) - puts *args + puts(*args) stdout, stderr, exit_status = Open3.capture3(*args) - fail stderr.strip.empty? ? stdout : stderr unless exit_status.success? + fail stderr.strip.empty? ? stdout : stderr unless exit_status.success? stdout end diff --git a/lib/jade-pug/base.rb b/lib/jade-pug/base.rb new file mode 100644 index 0000000..7569c48 --- /dev/null +++ b/lib/jade-pug/base.rb @@ -0,0 +1,116 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +require "memoist" + +module JadePug + extend Memoist + + # + # Compiles the template. + # + # @param source [String, #read] + # @param options [Hash] + # @return [String] + def compile(source, options = {}) + compiler(@version).compile(source, options) + end + + # + # Returns engine compiler for given version. + # Compilers are cached. + # + # @param version [String, :system] + # @return [Jade::SystemCompiler, Jade::ShippedCompiler, Pug::SystemCompiler, Pug::ShippedCompiler] + def compiler(version = @version) + (@compilers ||= {})["#{name}-#{version}"] ||= begin + case version + when :system then self::SystemCompiler.new + else self::ShippedCompiler.new(version) + end + end + end + + # + # Switches compiler version. + # + # - If you want to switch compiler to one of that shipped with gem simple pass version as a string. + # - If you want to switch compiler to system pass :system as a version. + # + # Pass block to temporarily switch the version. Without block the version is switched permanently. + # + # @param wanted_version [String, :system] + # @return [void] + def use(wanted_version) + previous_version = @version + @version = wanted_version + did_switch_version(previous_version, wanted_version) + + if block_given? + begin + yield + ensure + @version = previous_version + did_switch_version(wanted_version, previous_version) + end + end + end + + # + # Executed after compiler version switched. + # Outputs some useful information about version being used. + # + # @param version_from [String, :system] + # @param version_to [String, :system] + # @return [void] + def did_switch_version(version_from, version_to) + if version_from != version_to + if Symbol === version_to + puts "Using #{version_to} #{name}." + else + puts "Using #{name} #{version_to}. NOTE: Advanced features like includes, extends and blocks will not work." + end + end + nil + end + + # + # Returns the list of all available engine compiler versions shipped with gem. + # + # @return [Array] + def versions + Dir[File.expand_path("../../../vendor/assets/javascripts/**/#{name.downcase}-*.js", __FILE__)].map do |path| + File.basename(path).match(/\A#{name.downcase}-(?!runtime-)(?.+)\.min\.js\z/)&.send(:[], :v) + end.compact.sort + end + memoize :versions + + # + # Returns the list of all available engine runtime versions shipped with gem. + # + # @return [Array] + def runtime_versions + Dir[File.expand_path("../../../vendor/assets/javascripts/**/#{name.downcase}-*.js", __FILE__)].map do |path| + File.basename(path).match(/\A#{name.downcase}-runtime-(?.+)\.js\z/)&.send(:[], :v) + end.compact.sort + end + memoize :runtime_versions + + # + # Returns version for system-wide installed engine compiler. + # + # @return [String] + def system_version + compiler(:system).version + end + + # + # Returns config object for engine. + # Executed only once per engine (return value is memoized). + # + # @return [Jade::Config, Pug::Config] + def config + self::Config.new + end + memoize :config +end diff --git a/lib/jade-pug/compiler.rb b/lib/jade-pug/compiler.rb new file mode 100644 index 0000000..5e78405 --- /dev/null +++ b/lib/jade-pug/compiler.rb @@ -0,0 +1,91 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module JadePug + # + # Abstraction layer for engine compiler. + # + class Compiler + + # + # Returns the engine module. + # + # Used in such cases: + # - used to compute the name for engine + # - used to refer to the error classes + # + # @return [Jade, Pug] + attr_reader :engine + + # + # Returns the version of engine compiler. + # + # @return [String] + attr_reader :version + + # + # @param engine [Jade, Pug] + # @param version [String] + def initialize(engine, version) + @engine = engine + @version = version + end + + # + # Compiles template. + # + # By default does nothing. + # + # @abstract Derived compilers must implement it. + # @param source [String, #read] + # The template source code or any object which responds to #read and returns string. + # @param options [Hash] + # @return [String] + def compile(source, options = {}) + + end + + protected + + # + # Reads the template source code. + # Responds for pre-processing source code. + # Actually, it just checks if source code responds to #read and if so + # + # @param source [String, #read] + # @return [String] + def prepare_source(source) + source.respond_to?(:read) ? source.read : source + end + + # + # Responds for preparing compilation options. + # + # The next steps are executed: + # - is merges options into the engine config + # - it camelizes and symbolizes every option key + # - it removes nil values from the options + # + # @param options [Hash] + # @return [Hash] + def prepare_options(options) + options = engine.config.to_hash.merge(options) + options.keys.each { |k, v| options[k.to_s.gsub(/_([a-z])/) { $1.upcase }.to_sym] = options[k] } + options.delete_if { |k, v| v.nil? } + end + + # + # Responds for post-processing compilation result. + # + # By default returns the result without any processing. + # Derived compilers may override it for it's own special behaviors. + # + # @param source [String] The source code of template. + # @param result [String] The compiled code of template. + # @param options [Hash] The compilation options. + # @return [String] + def process_result(source, result, options) + result + end + end +end diff --git a/lib/jade-pug/config.rb b/lib/jade-pug/config.rb new file mode 100644 index 0000000..292c5bd --- /dev/null +++ b/lib/jade-pug/config.rb @@ -0,0 +1,56 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +require "regexp-match-polyfill" + +module JadePug + # + # Defines template engine compiler configuration. + # + class Config + # + # Allows to dynamically set config attributes. + # + def method_missing(name, *args, &block) + return super if block + + case args.size + when 0 + + # config.client? + if name =~ /\A(\w+)\?\z/ + !!(respond_to?($1) ? send($1) : instance_variable_get("@#{$1}")) + + # config.client + elsif name =~ /\A(\w+)\z/ + instance_variable_get("@#{$1}") + + else + super + end + + when 1 + # config.client= + if name =~ /\A(\w+)=\z/ + instance_variable_set("@#{$1}", args.first) + else + super + end + else + super + end + end + + def respond_to_missing?(name, include_all) + name.match?(/\A\w+[=?]?\z/) + end + + # + # Transforms config to the hash with all keys symbolized. + # + # @return [Hash] + def to_hash + instance_variables.map { |var| [var[1..-1].to_sym, instance_variable_get(var)] }.to_h + end + end +end diff --git a/lib/jade-pug/errors/compilation-error.rb b/lib/jade-pug/errors/compilation-error.rb new file mode 100644 index 0000000..a209440 --- /dev/null +++ b/lib/jade-pug/errors/compilation-error.rb @@ -0,0 +1,14 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module JadePug + # + # Used for template compilation errors, for example: + # - any template engine errors + # - syntax errors in template + # - any JavaScript exceptions (they are caught by ExecJS) + # + class CompilationError < StandardError + + end +end diff --git a/lib/jade-pug/errors/compiler-error.rb b/lib/jade-pug/errors/compiler-error.rb new file mode 100644 index 0000000..079a5c1 --- /dev/null +++ b/lib/jade-pug/errors/compiler-error.rb @@ -0,0 +1,13 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module JadePug + # + # Used when something is wrong with shipped engine compiler, for example: + # - when compiler source couldn't be read (file is missing or permissions problem?) + # - when compiler couldn't be compiled (when ExecJS fails to compile JavaScript code) + # + class CompilerError < StandardError + + end +end diff --git a/lib/jade-pug/errors/executable-error.rb b/lib/jade-pug/errors/executable-error.rb new file mode 100644 index 0000000..40f69e5 --- /dev/null +++ b/lib/jade-pug/errors/executable-error.rb @@ -0,0 +1,13 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module JadePug + # + # Used when something is wrong with engine compiler executable, for example: + # - when executable couldn't be found + # - when executable unexpectedly returned non-zero exit during version check + # + class ExecutableError < StandardError + + end +end diff --git a/lib/jade-pug/shipped-compiler.rb b/lib/jade-pug/shipped-compiler.rb new file mode 100644 index 0000000..e58d753 --- /dev/null +++ b/lib/jade-pug/shipped-compiler.rb @@ -0,0 +1,71 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +require "execjs" + +module JadePug + # + # Abstraction layer for shipped engine compiler. + # + class ShippedCompiler < Compiler + # + # @param engine [Jade, Pug] The Jade or Pug module. + # @param version [String] + def initialize(engine, version) + super + @execjs = compile_compiler_source(read_compiler_source(path_to_compiler_source)) + end + + # + # Compiles template. + # + # @param source [String, #read] + # @param options [Hash] + # @return [String] + def compile(source, options = {}) + source = prepare_source(source) + options = prepare_options(options) + result = if block_given? + yield + else + @execjs.call("#{engine.name.downcase}.compile#{"Client" if options[:client]}", source, options) + end + process_result(source, result, options) + rescue ExecJS::ProgramError => e + raise engine::CompilationError, e.message + end + + protected + + # + # Returns absolute path to the file with compiler source. + # + # @return [String] + def path_to_compiler_source + File.expand_path("../../../vendor/assets/javascripts/#{engine.name.downcase}-#{version}.min.js", __FILE__) + end + + # + # Reads the compiler source from a file and returns it. + # + # @param path [String] + # @return [String] + def read_compiler_source(path) + unless File.readable?(path) + raise engine::CompilerError, "Couldn't read compiler source: #{path}" + end + File.read(path) + end + + # + # Compiles the compiler from source and returns it as {ExecJS::Runtime}. + # + # @param source [String] + # @return [ExecJS::Runtime] + def compile_compiler_source(source) + ExecJS.compile(source).tap do |compiler| + raise engine::CompilerError, "Failed to compile compiler" unless compiler + end + end + end +end diff --git a/lib/jade-pug/system-compiler.rb b/lib/jade-pug/system-compiler.rb new file mode 100644 index 0000000..742c600 --- /dev/null +++ b/lib/jade-pug/system-compiler.rb @@ -0,0 +1,107 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +require "open3" + +module JadePug + # + # Abstraction layer for engine command line utility. + # + class SystemCompiler < Compiler + # + # @param engine [Jade, Pug] + def initialize(engine) + super(engine, nil) + check_executable! + end + + # + # Compiles the template. + # + # @param source [String, #read] + # @param options [hash] + # @return [String] + def compile(source, options = {}) + source = prepare_source(source) + options = prepare_options(options) + command = yield(source, options) + stdout, stderr, exit_status = Open3.capture3(*command, stdin_data: source) + raise engine::CompilationError, stderr unless exit_status.success? + process_result(source, stdout, options) + end + + # + # Checks if executable exists in $PATH. + # + # The method of check is described in this Stack Overflow answer: + # {https://stackoverflow.com/a/677212/2369428} + # + # @raise {Jade::ExecutableError, Pug::ExecutableError} + # If no executable found in the system. + # @return [void] + def check_executable! + return if @executable_checked + + stdout, stderr, exit_status = Open3.capture3("hash #{executable}") + + if exit_status.success? + @executable_checked = true + else + raise engine::ExecutableError, \ + %{No #{engine.name} executable found in your system. Did you forget to "npm install --global #{package}"?} + end + nil + end + + # + # Returns version of engine installed system-wide. + # + # @return [String, nil] + def version + @version ||= begin + check_executable! + + stdout, stderr, exit_status = Open3.capture3("#{executable} --version") + + if exit_status.success? + extract_version(stdout.strip) + else + raise engine::ExecutableError, \ + %{Failed to retrieve #{engine.name} version. Perhaps, the problem with Node.js runtime.} + end + end + end + + protected + + # + # Responds for the extraction of engine version + # from output of the command "pug --version". + # + # @param output [String] + # @return [String] + def extract_version(output) + output + end + + # + # Returns executable name for the engine. + # By default it tries to guess the name. + # Derived compilers may override it for custom behavior. + # + # @return [String] + def executable + engine.name.downcase + end + + # + # Returns name for the engine in NPM registry. + # By default it tries to guess the name. + # Derived compilers may override it for custom behavior. + # + # @return [String] + def package + engine.name.downcase + end + end +end diff --git a/lib/jade-ruby/compilation-essentials.rb b/lib/jade-ruby/compilation-essentials.rb new file mode 100644 index 0000000..b383f08 --- /dev/null +++ b/lib/jade-ruby/compilation-essentials.rb @@ -0,0 +1,24 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Jade + # + # Used to share common things between compilers. + # + module CompilationEssentials + # + # Responds for post-processing compilation result. + # + # @param source [String] The source code of template. + # @param result [String] The compiled code of template. + # @param options [Hash] The compilation options. + # @return [String] + def process_result(source, result, options) + if options[:client] + %{ (function(jade) { #{result}; return #{options[:name]}; }).call(this, jade); } + else + super + end + end + end +end diff --git a/lib/jade-ruby/compile.rb b/lib/jade-ruby/compile.rb deleted file mode 100644 index 436efa8..0000000 --- a/lib/jade-ruby/compile.rb +++ /dev/null @@ -1,79 +0,0 @@ -# encoding: UTF-8 -# frozen_string_literal: true - -require 'open3' -require 'json' -require 'shellwords' - -module Jade - class << self - def compile(source, options = {}) - check_executable! - - source = source.read if source.respond_to?(:read) - options = config.to_hash.merge(options) - - # http://web.archive.org/web/*/http://jade-lang.com/command-line/ - cmd = ['jade'] - - options[:compileDebug] = options[:compile_debug] - options[:nameAfterFile] = options[:name_after_file] - - options.respond_to?(:compact) ? options.compact : options.delete_if { |k, v| v.nil? } - - # Command line arguments take precedence over json options - # https://github.com/jadejs/jade/blob/master/bin/jade.js - cmd.push('--obj', JSON.generate(options)) - - cmd.push('--out', escape(options[:out])) if options[:out] - cmd.push('--path', escape(options[:filename])) if options[:filename] - cmd.push('--basedir', escape(options[:basedir])) if options[:basedir] - cmd.push('--pretty') if options[:pretty] - cmd.push('--client') if options[:client] - cmd.push('--name', escape(options[:name])) if options[:name] - cmd.push('--no-debug') unless options[:compile_debug] - cmd.push('--extension', escape(options[:extension])) if options[:extension] - cmd.push('--hierarchy', escape(options[:hierarchy])) if options[:hierarchy] - cmd.push('--name-after-file', escape(options[:name_after_file])) if options[:name_after_file] - cmd.push('--doctype', escape(options[:doctype])) if options[:doctype] - - stdout, stderr, exit_status = Open3.capture3(*cmd, stdin_data: source) - raise CompileError, stderr unless exit_status.success? - - if options[:client] - %{ (function(jade) { #{stdout}; return #{options[:name]}; }).call(this, jade); } - else - stdout - end - end - - def version - @version ||= begin - version = `jade --version` - version if $?.success? - end - end - - def check_executable! - unless @executable_checked - if version - puts "jade version: #{version}" - else - raise ExecutableError, 'No jade executable found in your system. Did you forget to "npm install --global jade"?' - end - @executable_checked = true - end - end - - protected - def escape(string) - string.shellescape - end - end - - class CompileError < StandardError - end - - class ExecutableError < StandardError - end -end diff --git a/lib/jade-ruby/config.rb b/lib/jade-ruby/config.rb index 76dcc5e..232a379 100644 --- a/lib/jade-ruby/config.rb +++ b/lib/jade-ruby/config.rb @@ -2,9 +2,14 @@ # frozen_string_literal: true module Jade - class Config - # http://web.archive.org/web/20160404025722/http://jade-lang.com/api/ - # http://web.archive.org/web/20160618141847/http://jade-lang.com/command-line/ + # + # Defines Jade compiler configuration. + # + # The documentation for Jade compiler can be found here: + # - {http://web.archive.org/web/20160404025722/http://jade-lang.com/api/} + # - {http://web.archive.org/web/20160618141847/http://jade-lang.com/command-line/} + # + class Config < JadePug::Config attr_accessor :filename attr_accessor :doctype attr_accessor :pretty @@ -30,22 +35,11 @@ def initialize @cache = false @globals = [] @client = false - @name = 'template' + @name = "template" @name_after_file = nil @out = nil @extension = nil @hierarchy = false end - - def to_hash - %i( filename doctype pretty - self debug compile_debug - cache globals client - name name_after_file out - extension hierarchy ).each_with_object({}) { |x, y| y[x] = send(x) } - end end - - class << self; attr_accessor :config; end - self.config = Config.new end diff --git a/lib/jade-ruby/errors/compilation-error.rb b/lib/jade-ruby/errors/compilation-error.rb new file mode 100644 index 0000000..f185e06 --- /dev/null +++ b/lib/jade-ruby/errors/compilation-error.rb @@ -0,0 +1,14 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Jade + # + # Used for template compilation errors, for example: + # - any template engine errors + # - syntax errors in template + # - any JavaScript exceptions (they are caught by ExecJS) + # + class CompilationError < JadePug::CompilationError + + end +end diff --git a/lib/jade-ruby/errors/compiler-error.rb b/lib/jade-ruby/errors/compiler-error.rb new file mode 100644 index 0000000..165a292 --- /dev/null +++ b/lib/jade-ruby/errors/compiler-error.rb @@ -0,0 +1,13 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Jade + # + # Used when something is wrong with shipped Jade compiler, for example: + # - when compiler source couldn't be read (file is missing or permissions problem?) + # - when compiler couldn't be compiled (when ExecJS fails to compile JavaScript code) + # + class CompilerError < JadePug::CompilerError + + end +end diff --git a/lib/jade-ruby/errors/executable-error.rb b/lib/jade-ruby/errors/executable-error.rb new file mode 100644 index 0000000..741c0b5 --- /dev/null +++ b/lib/jade-ruby/errors/executable-error.rb @@ -0,0 +1,13 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Jade + # + # Used when something is wrong with Jade executable, for example: + # - when executable couldn't be found + # - when executable unexpectedly returned non-zero exit during version check + # + class ExecutableError < JadePug::ExecutableError + + end +end diff --git a/lib/jade-ruby/shipped-compiler.rb b/lib/jade-ruby/shipped-compiler.rb new file mode 100644 index 0000000..42c686a --- /dev/null +++ b/lib/jade-ruby/shipped-compiler.rb @@ -0,0 +1,15 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Jade + # + # Abstraction layer for shipped Jade compiler. + # + class ShippedCompiler < JadePug::ShippedCompiler + include CompilationEssentials + + def initialize(version) + super Jade, version + end + end +end diff --git a/lib/jade-ruby/system-compiler.rb b/lib/jade-ruby/system-compiler.rb new file mode 100644 index 0000000..df9f938 --- /dev/null +++ b/lib/jade-ruby/system-compiler.rb @@ -0,0 +1,47 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +require "json" + +module Jade + # + # Abstraction layer for Jade command line utility. + # + class SystemCompiler < JadePug::SystemCompiler + include CompilationEssentials + + def initialize + super Jade + end + + # + # Compiles Jade template. + # + # @param source [String, #read] + # @param options [Hash] + # @return [String] + def compile(source, options = {}) + super do |src, opts| + # http://web.archive.org/web/*/http://jade-lang.com/command-line/ + cmd = [executable] + + # Command line arguments take precedence over json options + # https://github.com/jadejs/jade/blob/master/bin/jade.js + cmd.push("--obj", JSON.generate(opts)) + + cmd.push("--out", opts[:out]) if opts[:out] + cmd.push("--path", opts[:filename]) if opts[:filename] + cmd.push("--basedir", opts[:basedir]) if opts[:basedir] + cmd.push("--pretty") if opts[:pretty] + cmd.push("--client") if opts[:client] + cmd.push("--name", opts[:name]) if opts[:name] + cmd.push("--no-debug") unless opts[:compile_debug] + cmd.push("--extension", opts[:extension]) if opts[:extension] + cmd.push("--hierarchy", opts[:hierarchy]) if opts[:hierarchy] + cmd.push("--name-after-file", opts[:name_after_file]) if opts[:name_after_file] + cmd.push("--doctype", opts[:doctype]) if opts[:doctype] + cmd + end + end + end +end diff --git a/lib/pug-ruby.rb b/lib/pug-ruby.rb index 1b56ee1..3dc5264 100644 --- a/lib/pug-ruby.rb +++ b/lib/pug-ruby.rb @@ -1,7 +1,54 @@ # encoding: UTF-8 # frozen_string_literal: true -require 'jade-ruby/config' -require 'jade-ruby/compile' -require 'pug-ruby/config' -require 'pug-ruby/compile' +require "jade-pug/base" + +# +# This module contains common thing related Jade and Pug. +# +module JadePug + autoload :Config, "jade-pug/config" + autoload :Compiler, "jade-pug/compiler" + autoload :ShippedCompiler, "jade-pug/shipped-compiler" + autoload :SystemCompiler, "jade-pug/system-compiler" + autoload :CompilationError, "jade-pug/errors/compilation-error" + autoload :CompilerError, "jade-pug/errors/compiler-error" + autoload :ExecutableError, "jade-pug/errors/executable-error" +end + +# +# This module contains all stuff related to Jade template engine. +# See the list below. +# +module Jade + extend JadePug + + autoload :Config, "jade-ruby/config" + autoload :Compiler, "jade-ruby/compiler" + autoload :ShippedCompiler, "jade-ruby/shipped-compiler" + autoload :SystemCompiler, "jade-ruby/system-compiler" + autoload :CompilationEssentials, "jade-ruby/compilation-essentials" + autoload :CompilationError, "jade-ruby/errors/compilation-error" + autoload :CompilerError, "jade-ruby/errors/compiler-error" + autoload :ExecutableError, "jade-ruby/errors/executable-error" +end + +# +# This module contains all stuff related to Pug template engine. +# See the list below. +# +module Pug + extend JadePug + + autoload :Config, "pug-ruby/config" + autoload :Compiler, "pug-ruby/compiler" + autoload :ShippedCompiler, "pug-ruby/shipped-compiler" + autoload :SystemCompiler, "pug-ruby/system-compiler" + autoload :CompilationEssentials, "pug-ruby/compilation-essentials" + autoload :CompilationError, "pug-ruby/errors/compilation-error" + autoload :CompilerError, "pug-ruby/errors/compiler-error" + autoload :ExecutableError, "pug-ruby/errors/executable-error" +end + +Jade.use :system +Pug.use :system diff --git a/lib/pug-ruby/compilation-essentials.rb b/lib/pug-ruby/compilation-essentials.rb new file mode 100644 index 0000000..47e12a7 --- /dev/null +++ b/lib/pug-ruby/compilation-essentials.rb @@ -0,0 +1,28 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Pug + # + # Used to share common things between compilers. + # + module CompilationEssentials + # + # Responds for post-processing compilation result. + # + # @param source [String] The source code of template. + # @param result [String] The compiled code of template. + # @param options [Hash] The compilation options. + # @return [String] + def process_result(source, result, options) + if options[:client] + if options[:inline_runtime_functions] + %{ (function() { #{result}; return #{options[:name]}; }).call(this); } + else + %{ (function(pug) { #{result}; return #{options[:name]}; }).call(this, pug); } + end + else + super + end + end + end +end diff --git a/lib/pug-ruby/compile.rb b/lib/pug-ruby/compile.rb deleted file mode 100644 index b87edf1..0000000 --- a/lib/pug-ruby/compile.rb +++ /dev/null @@ -1,88 +0,0 @@ -# encoding: UTF-8 -# frozen_string_literal: true - -require 'open3' -require 'json' -require 'shellwords' - -module Pug - class << self - def compile(source, options = {}) - check_executable! - - source = source.read if source.respond_to?(:read) - options = config.to_hash.merge(options) - - # https://github.com/pugjs/pug-cli - cmd = ['pug'] - - options[:compileDebug] = options[:compile_debug] - options[:nameAfterFile] = options[:name_after_file] - options[:inlineRuntimeFunctions] = options[:inline_runtime_functions] - - options.respond_to?(:compact) ? options.compact : options.delete_if { |k, v| v.nil? } - - # Command line arguments take precedence over json options - # https://github.com/pugjs/pug-cli/blob/master/index.js - cmd.push('--obj', JSON.generate(options)) - - cmd.push('--out', escape(options[:out])) if options[:out] - cmd.push('--path', escape(options[:filename])) if options[:filename] - cmd.push('--basedir', escape(options[:basedir])) if options[:basedir] - cmd.push('--pretty') if options[:pretty] - cmd.push('--client') if options[:client] - cmd.push('--name', escape(options[:name])) if options[:name] - cmd.push('--no-debug') unless options[:compile_debug] - cmd.push('--extension', escape(options[:extension])) if options[:extension] - cmd.push('--silent') if options[:silent] - cmd.push('--name-after-file', escape(options[:name_after_file])) if options[:name_after_file] - cmd.push('--doctype', escape(options[:doctype])) if options[:doctype] - - stdout, stderr, exit_status = Open3.capture3(*cmd, stdin_data: source) - - raise CompileError, stderr unless exit_status.success? - - if options[:client] - if options[:inline_runtime_functions] - %{ (function() { #{stdout}; return #{options[:name]}; }).call(this); } - else - %{ (function(pug) { #{stdout}; return #{options[:name]}; }).call(this, pug); } - end - else - stdout - end - end - - def version - @version ||= begin - output = `pug --version` - if $?.success? - output.split(/\n\r?/)[0].to_s['pug version: '.size..-1] - end - end - end - - def check_executable! - unless @executable_checked - if version - puts "pug version: #{version}" - else - raise ExecutableError, 'No pug executable found in your system. Did you forget to "npm install --global pug-cli"?' - end - @executable_checked = true - end - end - - protected - def escape(string) - string.shellescape - end - end - - class CompileError < StandardError - end - - class ExecutableError < StandardError - end -end - diff --git a/lib/pug-ruby/config.rb b/lib/pug-ruby/config.rb index b06d607..04ebe76 100644 --- a/lib/pug-ruby/config.rb +++ b/lib/pug-ruby/config.rb @@ -2,9 +2,14 @@ # frozen_string_literal: true module Pug - class Config - # https://pugjs.org/api/reference.html - # https://github.com/pugjs/pug-cli + # + # Defines Pug compiler configuration. + # + # The documentation for Pug configuration can be found here: + # - {https://pugjs.org/api/reference.html} + # - {https://github.com/pugjs/pug-cli} + # + class Config < JadePug::Config attr_accessor :filename attr_accessor :basedir attr_accessor :doctype @@ -21,32 +26,24 @@ class Config attr_accessor :silent def initialize + super @filename = nil @basedir = nil @doctype = nil @pretty = false @self = false + @debug = false @compile_debug = false @globals = [] @inline_runtime_functions = true - @name = 'template' + + @name = "template" + @name_after_file = nil @out = nil @extension = nil @silent = true end - - def to_hash - %i( filename basedir doctype - pretty self debug - compile_debug globals inline_runtime_functions - name name_after_file out - extension silent ).each_with_object({}) { |x, y| y[x] = send(x) } - end end - - singleton_class.class_exec { attr_accessor :config } - self.config = Config.new end - diff --git a/lib/pug-ruby/errors/compilation-error.rb b/lib/pug-ruby/errors/compilation-error.rb new file mode 100644 index 0000000..e81424d --- /dev/null +++ b/lib/pug-ruby/errors/compilation-error.rb @@ -0,0 +1,14 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Pug + # + # Used for template compilation errors, for example: + # - any template engine errors + # - syntax errors in template + # - any JavaScript exceptions (they are caught by ExecJS) + # + class CompilationError < JadePug::CompilationError + + end +end diff --git a/lib/pug-ruby/errors/compiler-error.rb b/lib/pug-ruby/errors/compiler-error.rb new file mode 100644 index 0000000..275da57 --- /dev/null +++ b/lib/pug-ruby/errors/compiler-error.rb @@ -0,0 +1,13 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Pug + # + # Used when something is wrong with shipped Pug compiler, for example: + # - when compiler source couldn't be read (file is missing or permissions problem?) + # - when compiler couldn't be compiled (when ExecJS fails to compile JavaScript code) + # + class CompilerError < JadePug::CompilerError + + end +end diff --git a/lib/pug-ruby/errors/executable-error.rb b/lib/pug-ruby/errors/executable-error.rb new file mode 100644 index 0000000..ed84d39 --- /dev/null +++ b/lib/pug-ruby/errors/executable-error.rb @@ -0,0 +1,13 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Pug + # + # Used when something is wrong with Pug executable, for example: + # - when executable couldn't be found + # - when executable unexpectedly returned non-zero exit during version check + # + class ExecutableError < JadePug::ExecutableError + + end +end diff --git a/lib/pug-ruby/shipped-compiler.rb b/lib/pug-ruby/shipped-compiler.rb new file mode 100644 index 0000000..bb1fc38 --- /dev/null +++ b/lib/pug-ruby/shipped-compiler.rb @@ -0,0 +1,15 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module Pug + # + # Abstraction layer for shipped Pug compiler. + # + class ShippedCompiler < JadePug::ShippedCompiler + include CompilationEssentials + + def initialize(version) + super Pug, version + end + end +end diff --git a/lib/pug-ruby/system-compiler.rb b/lib/pug-ruby/system-compiler.rb new file mode 100644 index 0000000..5f28f7b --- /dev/null +++ b/lib/pug-ruby/system-compiler.rb @@ -0,0 +1,68 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +require "json" + +module Pug + # + # Abstraction layer for Pug command line utility. + # + class SystemCompiler < JadePug::SystemCompiler + include CompilationEssentials + + def initialize + super Pug + end + + # + # Compiles Pug template. + # + # @param source [String, #read] + # @param options [Hash] + # @return [String] + def compile(source, options = {}) + super do |src, opts| + + # https://github.com/pugjs/pug-cli + cmd = [executable] + + # Command line arguments take precedence over json options + # https://github.com/pugjs/pug-cli/blob/master/index.js + cmd.push("--obj", JSON.generate(opts)) + + cmd.push("--out", opts[:out]) if opts[:out] + cmd.push("--path", opts[:filename]) if opts[:filename] + cmd.push("--basedir", opts[:basedir]) if opts[:basedir] + cmd.push("--pretty") if opts[:pretty] + cmd.push("--client") if opts[:client] + cmd.push("--name", opts[:name]) if opts[:name] + cmd.push("--no-debug") unless opts[:compile_debug] + cmd.push("--extension", opts[:extension]) if opts[:extension] + cmd.push("--silent") if opts[:silent] + cmd.push("--name-after-file", opts[:name_after_file]) if opts[:name_after_file] + cmd.push("--doctype", opts[:doctype]) if opts[:doctype] + cmd + end + end + + protected + + # + # Returns name for the engine in NPM registry. + # + # @return [String] + def package + "pug-cli" + end + + # + # Responds for the extraction of engine version + # from output of the command "pug --version". + # + # @param output [String] + # @return [String] + def extract_version(output) + output.split(/\n\r?/)[0].to_s["pug version: ".size..-1] + end + end +end diff --git a/pug-ruby.gemspec b/pug-ruby.gemspec index 39e9052..89bdd5b 100644 --- a/pug-ruby.gemspec +++ b/pug-ruby.gemspec @@ -2,19 +2,22 @@ # frozen_string_literal: true Gem::Specification.new do |s| - s.name = 'pug-ruby' - s.version = '1.0.2' - s.author = 'Yaroslav Konoplov' - s.email = 'eahome00@gmail.com' - s.summary = 'Ruby wrapper for the Pug/Jade template engine.' - s.description = 'Ruby wrapper for the Pug/Jade template engine.' - s.homepage = 'https://github.com/yivo/pug-ruby' - s.license = 'MIT' + s.name = "pug-ruby" + s.version = "1.0.2" + s.author = "Yaroslav Konoplov" + s.email = "eahome00@gmail.com" + s.summary = "Ruby wrapper for the Pug/Jade template engine." + s.description = "Ruby wrapper for the Pug/Jade template engine." + s.homepage = "https://github.com/yivo/pug-ruby" + s.license = "MIT" s.files = `git ls-files -z`.split("\x0") s.test_files = `git ls-files -z -- {test,spec,features}/*`.split("\x0") - s.require_paths = ['lib'] + s.require_paths = ["lib"] - s.add_development_dependency 'rake', '~> 10.0' - s.add_development_dependency 'test-unit', '~> 3.1' + s.add_dependency "execjs", "~> 2.0" + s.add_dependency "memoist", "~> 0.15" + s.add_dependency "regexp-match-polyfill", "~> 1.0" + s.add_development_dependency "rake", "~> 10.0" + s.add_development_dependency "test-unit", "~> 3.1" end diff --git a/test/helper.rb b/test/helper.rb index 544de49..b94a9e2 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,2 +1,16 @@ +# encoding: UTF-8 +# frozen_string_literal: true + Bundler.require -require 'stringio' +require "stringio" + +def each_jade_version + ([:system] + Jade.versions).each { |v| yield(v) } +end + +def each_pug_version + ([:system] + Pug.versions).each do |v| + next if v.match?(/alpha/) + yield(v) + end +end diff --git a/test/test-jade.rb b/test/test-jade.rb index 42efec9..8ac9850 100644 --- a/test/test-jade.rb +++ b/test/test-jade.rb @@ -1,43 +1,51 @@ # encoding: UTF-8 # frozen_string_literal: true -require_relative 'helper' +require_relative "helper" class JadeTest < Test::Unit::TestCase + each_jade_version do |version| + define_method "test_compilation_#{version}" do + Jade.use version + file = expand_path("index.jade") + template = File.read(file) + result = Jade.compile(template, client: true) + assert_match_template_function(result) + assert_no_match_doctype(result) + end - def test_compile - file = expand_path('index.jade') - template = File.read(file) - result = Jade.compile(template, client: true) - assert_match_template_function(result) - assert_no_match_doctype(result) - end + define_method "test_compilation_with_io_#{version}" do + Jade.use version + template = "div\n | Hello, world!" + io = StringIO.new(template) + assert_equal(Pug.compile(template), Pug.compile(io)) + end - def test_compile_with_io - io = StringIO.new("div\n | Hello, world!") - assert_equal(Jade.compile("div\n | Hello, world!"), Jade.compile(io)) - end - - def test_compilation_error - assert_raise(Jade::CompileError) { Jade.compile("else\n div") } + define_method "test_compilation_error_#{version}" do + Jade.use version + assert_raise(Jade::CompilationError) { Jade.compile("else\n div") } + end end def test_includes - file = expand_path('includes/index.jade') + Jade.use :system + file = expand_path("includes/index.jade") template = File.read(file) result = Jade.compile(template, filename: file, client: false) assert_match_doctype(result) end def test_extends - file = expand_path('extends/layout.jade') + Jade.use :system + file = expand_path("extends/layout.jade") template = File.read(file) result = Jade.compile(template, filename: file, client: false) assert_match_doctype(result) end def test_includes_client - file = expand_path('includes/index.jade') + Jade.use :system + file = expand_path("includes/index.jade") template = File.read(file) result = Jade.compile(template, filename: file, client: true) assert_match_template_function(result) @@ -45,7 +53,8 @@ def test_includes_client end def test_extends_client - file = expand_path('extends/layout.jade') + Jade.use :system + file = expand_path("extends/layout.jade") template = File.read(file) result = Jade.compile(template, filename: file, client: true) assert_match_template_function(result) @@ -53,8 +62,9 @@ def test_extends_client end protected + def expand_path(relative_path) - File.expand_path(File.join('..', 'jade', relative_path), __FILE__) + File.expand_path(File.join("../jade", relative_path), __FILE__) end def assert_match_doctype(string) diff --git a/test/test-pug.rb b/test/test-pug.rb index 4880676..5ec186b 100644 --- a/test/test-pug.rb +++ b/test/test-pug.rb @@ -1,43 +1,56 @@ # encoding: UTF-8 # frozen_string_literal: true -require_relative 'helper' +require_relative "helper" class PugTest < Test::Unit::TestCase + each_pug_version do |version| + define_method "test_compiler_switching_#{version}" do + Pug.use version + assert_equal(version == :system ? "2.0.0-beta6" : version, Pug.compiler.version) + end - def test_compile - file = expand_path('index.pug') - template = File.read(file) - result = Pug.compile(template, client: true) - assert_match_template_function(result) - assert_no_match_doctype(result) - end + define_method "test_compilation_#{version}" do + Pug.use version + file = expand_path("index.pug") + template = File.read(file) + result = Pug.compile(template, client: true) + assert_match_template_function(result) + assert_no_match_doctype(result) + end - def test_compile_with_io - io = StringIO.new("div\n | Hello, world!") - assert_equal(Pug.compile("div\n | Hello, world!"), Pug.compile(io)) - end + define_method "test_compilation_with_io_#{version}" do + Pug.use version + template = "div\n | Hello, world!" + io = StringIO.new(template) + assert_equal(Pug.compile(template), Pug.compile(io)) + end - def test_compilation_error - assert_raise(Pug::CompileError) { Pug.compile("else\n div") } + define_method "test_compilation_error_#{version}" do + Pug.use version + assert_raise(Pug::CompilationError) { Pug.compile("else\n div") } + end end def test_includes - file = expand_path('includes/index.pug') + Pug.use :system + file = expand_path("includes/index.pug") template = File.read(file) result = Pug.compile(template, filename: file, client: false) assert_match_doctype(result) end def test_extends - file = expand_path('extends/layout.pug') + Pug.use :system + file = expand_path("extends/index.pug") template = File.read(file) result = Pug.compile(template, filename: file, client: false) assert_match_doctype(result) end def test_includes_client - file = expand_path('includes/index.pug') + Pug.use :system + file = expand_path("includes/index.pug") template = File.read(file) result = Pug.compile(template, filename: file, client: true) assert_match_template_function(result) @@ -45,7 +58,8 @@ def test_includes_client end def test_extends_client - file = expand_path('extends/layout.pug') + Pug.use :system + file = expand_path("extends/index.pug") template = File.read(file) result = Pug.compile(template, filename: file, client: true) assert_match_template_function(result) @@ -53,8 +67,9 @@ def test_extends_client end protected + def expand_path(relative_path) - File.expand_path(File.join('..', 'pug', relative_path), __FILE__) + File.expand_path(File.join("../pug", relative_path), __FILE__) end def assert_match_doctype(string)