Files
Redmine/lib/redmine/markdownizer.rb
2026-03-21 08:11:46 +00:00

117 lines
3.9 KiB
Ruby

# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006- Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'fileutils'
require 'shellwords'
require 'tempfile'
require 'timeout'
module Redmine
module Markdownizer
extend Redmine::Utils::Shell
COMMAND = (Redmine::Configuration['pandoc_command'] || 'pandoc').freeze
PREVIEW_GENERATION_TIMEOUT = Redmine::Configuration['markdownized_preview_generation_timeout'].to_i
MAX_SOURCE_SIZE = Redmine::Configuration['markdownized_preview_max_source_size'].to_i
MAX_OUTPUT_SIZE = Redmine::Configuration['markdownized_preview_max_output_size'].to_i
def self.supports?(filename)
markdownizable_extensions.include?(File.extname(filename.to_s).downcase)
end
def self.convert(source, target)
return nil unless available?
return target if File.exist?(target)
if File.size(source) > MAX_SOURCE_SIZE
logger.warn("Markdownized preview generation skipped because source file is too large (#{File.size(source)} bytes): #{source}")
return nil
end
directory = File.dirname(target)
FileUtils.mkdir_p(directory)
args = [COMMAND, source, "-t", "gfm"]
pid = nil
output = Tempfile.new('markdownized-preview')
begin
Timeout.timeout(PREVIEW_GENERATION_TIMEOUT) do
pid = Process.spawn(*args, out: output.path)
_, status = Process.wait2(pid)
unless status.success?
logger.error("Markdownized preview generation failed (#{status.exitstatus}):\nCommand: #{args.shelljoin}")
return nil
end
end
rescue Timeout::Error
if pid
Process.kill('KILL', pid)
Process.detach(pid)
end
logger.error("Markdownized preview generation timed out:\nCommand: #{args.shelljoin}")
return nil
rescue => e
logger.error("Markdownized preview generation failed:\nCommand: #{args.shelljoin}\nException was: #{e.message}")
return nil
ensure
output.close
end
preview = File.binread(output.path, MAX_OUTPUT_SIZE + 1) || +""
File.binwrite(target, preview.byteslice(0, MAX_OUTPUT_SIZE))
target
ensure
output&.unlink
end
def self.available?
return @available if defined?(@available)
begin
@pandoc_version = `#{shell_quote COMMAND} --version`[/pandoc\s+([\d.]+)/, 1].split('.').map(&:to_i)
@available = $?.success?
rescue
@available = false
end
logger.warn("Pandoc binary (#{COMMAND}) not available") unless @available
@available
end
def self.markdownizable_extensions
return @markdownizable_extensions if defined?(@markdownizable_extensions)
if available?
# Microsoft Word and LibreOffice Writer files are supported by a wide
# range of Pandoc versions
@markdownizable_extensions = %w[.docx .odt]
else
return (@markdownizable_extensions = [])
end
# Pandoc >= 3.8.3 supports Microsoft Excel and PowerPoint files
@markdownizable_extensions += %w[.xlsx .pptx] if (@pandoc_version <=> [3, 8, 3]) >= 0
@markdownizable_extensions
end
def self.logger
Rails.logger
end
end
end