# frozen_string_literal: true module Onebox module Mixins module GitBlobOnebox def self.included(klass) klass.include(Onebox::Engine) klass.include(Onebox::LayoutSupport) klass.matches_regexp(klass.git_regexp) klass.always_https klass.include(InstanceMethods) end EXPAND_AFTER = 0b001 EXPAND_BEFORE = 0b010 EXPAND_NONE = 0b0 DEFAULTS = { EXPAND_ONE_LINER: EXPAND_AFTER | EXPAND_BEFORE, #set how to expand a one liner. user EXPAND_NONE to disable expand LINES_BEFORE: 10, LINES_AFTER: 10, SHOW_LINE_NUMBER: true, MAX_LINES: 20, MAX_CHARS: 5000 } module InstanceMethods def initialize(url, timeout = nil) super url, timeout # merge engine options from global Onebox.options interface # self.options = Onebox.options["GithubBlobOnebox"] # self.class.name.split("::").last.to_s # self.options = Onebox.options[self.class.name.split("::").last.to_s] #We can use this a more generic approach. extract the engine class name automatically self.options = DEFAULTS @selected_lines_array = nil @selected_one_liner = 0 @model_file = nil # Define constant after merging options set in Onebox.options # We can define constant automatically. options.each_pair do |constant_name, value| constant_name_u = constant_name.to_s.upcase if constant_name_u == constant_name.to_s #define a constant if not already defined unless self.class.const_defined? constant_name_u.to_sym Onebox::Mixins::GitBlobOnebox.const_set constant_name_u.to_sym , options[constant_name_u.to_sym] end end end end private def calc_range(m, contents_lines_size) truncated = false from = /\d+/.match(m[:from]) #get numeric should only match a positive interger to = /\d+/.match(m[:to]) #get numeric should only match a positive interger range_provided = !(from.nil? && to.nil?) #true if "from" or "to" provided in URL from = from.nil? ? 1 : from[0].to_i #if from not provided default to 1st line to = to.nil? ? -1 : to[0].to_i #if to not provided default to undefiend to be handled later in the logic if to === -1 && range_provided #case "from" exists but no valid "to". aka ONE_LINER one_liner = true to = from else one_liner = false end unless range_provided #case no range provided default to 1..MAX_LINES from = 1 to = MAX_LINES truncated = true if contents_lines_size > MAX_LINES #we can technically return here end from, to = [from, to].sort #enforce valid range. [from < to] from = 1 if from > contents_lines_size #if "from" out of TOP bound set to 1st line to = contents_lines_size if to > contents_lines_size #if "to" is out of TOP bound set to last line. if one_liner @selected_one_liner = from if EXPAND_ONE_LINER != EXPAND_NONE if (EXPAND_ONE_LINER & EXPAND_BEFORE != 0) # check if EXPAND_BEFORE flag is on from = [1, from - LINES_BEFORE].max # make sure expand before does not go out of bound end if (EXPAND_ONE_LINER & EXPAND_AFTER != 0) # check if EXPAND_FLAG flag is on to = [to + LINES_AFTER, contents_lines_size].min # make sure expand after does not go out of bound end from = contents_lines_size if from > contents_lines_size #if "from" is out of the content top bound # to = contents_lines_size if to > contents_lines_size #if "to" is out of the content top bound else #no expand show the one liner solely end end if to - from > MAX_LINES && !one_liner #if exceed the MAX_LINES limit correct unless range was produced by one_liner which it expand setting will allow exceeding the line limit truncated = true to = from + MAX_LINES - 1 end { from: from, #calculated from from_minus_one: from - 1, #used for getting currect ol>li numbering with css used in template to: to, #calculated to one_liner: one_liner, #boolean if a one-liner selected_one_liner: @selected_one_liner, #if a one liner is provided we create a reference for it. range_provided: range_provided, #boolean if range provided truncated: truncated } end #minimize/compact leading indentation while preserving overall indentation def removeLeadingIndentation(str) min_space = 100 a_lines = str.lines a_lines.each do |l| l = l.chomp("\n") # remove new line m = l.match(/^[ ]*/) # find leading spaces 0 or more unless m.nil? || l.size == m[0].size || l.size == 0 # no match | only spaces in line | empty line m_str_length = m[0].size if m_str_length <= 1 # minimum space is 1 or nothing we can break we found our minimum min_space = m_str_length break #stop iteration end if m_str_length < min_space min_space = m_str_length end else next # SKIP no match or line is only spaces end end a_lines.each do |l| re = Regexp.new "^[ ]{#{min_space}}" #match the minimum spaces of the line l.gsub!(re, "") end a_lines.join end def line_number_helper(lines, start, selected) lines = removeLeadingIndentation(lines.join).lines # A little ineffeicent we could modify removeLeadingIndentation to accept array and return array, but for now it is only working with a string hash_builder = [] output_builder = [] lines.map.with_index { |line, i| lnum = (i.to_i + start) hash_builder.push(line_number: lnum, data: line.gsub("\n", ""), selected: (selected == lnum) ? true : false) output_builder.push "#{lnum}: #{line}" } { output: output_builder.join(), array: hash_builder } end def raw return @raw if defined?(@raw) m = @url.match(self.raw_regexp) if m from = /\d+/.match(m[:from]) #get numeric should only match a positive interger to = /\d+/.match(m[:to]) #get numeric should only match a positive interger @file = m[:file] @lang = Onebox::FileTypeFinder.from_file_name(m[:file]) if @lang == "stl" && link.match?(/^https?:\/\/(www\.)?github\.com.*\/blob\//) @model_file = @lang.dup @raw = "https://render.githubusercontent.com/view/solid?url=" + self.raw_template(m) else contents = URI.parse(self.raw_template(m)).open(read_timeout: timeout).read if contents.encoding == Encoding::BINARY || contents.bytes.include?(0) @raw = nil @binary = true return end contents_lines = contents.lines #get contents lines contents_lines_size = contents_lines.size #get number of lines cr = calc_range(m, contents_lines_size) #calculate the range of lines for output selected_one_liner = cr[:selected_one_liner] #if url is a one-liner calc_range will return it from = cr[:from] to = cr[:to] @truncated = cr[:truncated] range_provided = cr[:range_provided] @cr_results = cr if range_provided #if a range provided (single line or more) if SHOW_LINE_NUMBER lines_result = line_number_helper(contents_lines[(from - 1)..(to - 1)], from, selected_one_liner) #print code with prefix line numbers in case range provided contents = lines_result[:output] @selected_lines_array = lines_result[:array] else contents = contents_lines[(from - 1)..(to - 1)].join() end else contents = contents_lines[(from - 1)..(to - 1)].join() end if contents.length > MAX_CHARS #truncate content chars to limits contents = contents[0..MAX_CHARS] @truncated = true end @raw = contents end end end def data @data ||= { title: title, link: link, # IMPORTANT NOTE: All of the other class variables are populated # as *side effects* of the `raw` method! They must all appear # AFTER the call to `raw`! Don't get bitten by this like I did! content: raw, binary: @binary, lang: "lang-#{@lang}", lines: @selected_lines_array , has_lines: !@selected_lines_array.nil?, selected_one_liner: @selected_one_liner, cr_results: @cr_results, truncated: @truncated, model_file: @model_file, width: 480, height: 360 } end end end end end