require "asciidoctor/extensions" require "fileutils" require "uuidtools" module Asciidoctor module Standoc class AltTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor use_dsl named :alt parse_content_as :text using_format :short def process(parent, _target, attrs) out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert %{#{out}} end end class DeprecatedTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor use_dsl named :deprecated parse_content_as :text using_format :short def process(parent, _target, attrs) out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert %{#{out}} end end class DomainTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor use_dsl named :domain parse_content_as :text using_format :short def process(parent, _target, attrs) out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert %{#{out}} end end class ConceptInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor use_dsl named :concept name_positional_attributes "id", "word", "term" #match %r{concept:(?[^\[]*)\[(?|.*?[^\\])\]$} match /\{\{(?|.*?[^\\])\}\}$/ using_format :short # deal with locality attrs and their disruption of positional attrs def preprocess_attrs(attrs) attrs.delete("term") if attrs["term"] and !attrs["word"] attrs.delete(3) if attrs[3] == attrs["term"] a = attrs.keys.reject { |k| k.is_a? String or [1, 2].include? k } attrs["word"] ||= attrs[a[0]] if a.length() > 0 attrs["term"] ||= attrs[a[1]] if a.length() > 1 attrs end def process(parent, _target, attr) attr = preprocess_attrs(attr) localities = attr.keys.reject { |k| %w(id word term).include? k }. reject { |k| k.is_a? Numeric }. map { |k| "#{k}=#{attr[k]}" }.join(",") text = [localities, attr["word"]].reject{ |k| k.nil? || k.empty? }. join(",") out = Asciidoctor::Inline.new(parent, :quoted, text).convert %{#{out}} end end class PseudocodeBlockMacro < Asciidoctor::Extensions::BlockProcessor use_dsl named :pseudocode on_context :example, :sourcecode def init_indent(s) /^(?[ \t]*)(?.*)$/ =~ s prefix = prefix.gsub(/\t/, "\u00a0\u00a0\u00a0\u00a0"). gsub(/ /, "\u00a0") prefix + suffix end def supply_br(lines) lines.each_with_index do |l, i| next if l.empty? || l.match(/ \+$/) next if i == lines.size - 1 || i < lines.size - 1 && lines[i+1].empty? lines[i] += " +" end lines end def prevent_smart_quotes(m) m.gsub(/'/, "'").gsub(/"/, """) end def process parent, reader, attrs attrs['role'] = 'pseudocode' lines = reader.lines.map { |m| prevent_smart_quotes(init_indent(m)) } create_block(parent, :example, supply_br(lines), attrs, content_model: :compound) end end class HTML5RubyMacro < Asciidoctor::Extensions::InlineMacroProcessor use_dsl named :ruby parse_content_as :text option :pos_attrs, %w(rpbegin rt rpend) def process(parent, target, attributes) rpbegin = '(' rpend = ')' if attributes.size == 1 and attributes.key?("text") rt = attributes["text"] elsif attributes.size == 2 and attributes.key?(1) and attributes.key?("rpbegin") # for example, html5ruby:楽聖少女[がくせいしょうじょ] rt = attributes[1] || "" else rpbegin = attributes['rpbegin'] rt = attributes['rt'] rpend = attributes['rpend'] end "#{target}#{rpbegin}#{rt}"\ "#{rpend}" end end class ToDoAdmonitionBlock < Extensions::BlockProcessor use_dsl named :TODO on_contexts :example, :paragraph def process parent, reader, attrs attrs['name'] = 'todo' attrs['caption'] = 'TODO' create_block parent, :admonition, reader.lines, attrs, content_model: :compound end end class ToDoInlineAdmonitionBlock < Extensions::Treeprocessor def process document (document.find_by context: :paragraph).each do |para| next unless /^TODO: /.match para.lines[0] parent = para.parent para.set_attr("name", "todo") para.set_attr("caption", "TODO") para.lines[0].sub!(/^TODO: /, "") todo = Block.new parent, :admonition, attributes: para.attributes, source: para.lines, content_model: :compound parent.blocks[parent.blocks.index(para)] = todo end end end class PlantUMLBlockMacroBackend # https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby def self.plantuml_installed? cmd = "plantuml" exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| exts.each do |ext| exe = File.join(path, "#{cmd}#{ext}") return exe if File.executable?(exe) && !File.directory?(exe) end end nil end def self.run umlfile, outfile system "plantuml #{umlfile.path}" or (warn $? and return false) i = 0 until !Gem.win_platform? || File.exist?(outfile) || i == 15 sleep(1) i += 1 end File.exist?(outfile) end # if no :imagesdir: leave image file in plantuml # sleep need for windows because dot works in separate process and # plantuml process may finish earlier then dot, as result png file # maybe not created yet after plantuml finish def self.generate_file parent, reader localdir = Utils::localdir(parent.document) imagesdir = parent.document.attr('imagesdir') umlfile, outfile = save_plantuml parent, reader, localdir # TODO: this should raise failure if there is no image output!! run(umlfile, outfile) or return umlfile.unlink path = Pathname.new(localdir) + (imagesdir || "plantuml") path.mkpath # TODO: this should raise failure if the destination path already exists! File.exist?(path) or return # TODO: this should raise failure if the destination path is not writable! File.writable?(path) or return # Warning for Heisenbug: metanorma/metanorma-standoc#187 # Windows Ruby 2.4 will crash if a Tempfile is "mv"ed. # This is why we need to copy and then unlink. filename = File.basename(outfile.to_s) FileUtils.cp(outfile, path) && outfile.unlink imagesdir ? filename : File.join(path, filename) end def self.save_plantuml parent, reader, localdir src = reader.source reader.lines.first.sub(/\s+$/, "").match /^@startuml($| )/ or src = "@startuml\n#{src}\n@enduml\n" /^@startuml (?[^\n]+)\n/ =~ src Tempfile.open(["plantuml", ".pml"], :encoding => "utf-8") do |f| f.write(src) [f, File.join(File.dirname(f.path), (fn || File.basename(f.path, ".pml")) + ".png")] end end def self.generate_attrs attrs through_attrs = %w(id align float title role width height alt). inject({}) do |memo, key| memo[key] = attrs[key] if attrs.has_key? key memo end end end class PlantUMLBlockMacro < Asciidoctor::Extensions::BlockProcessor use_dsl named :plantuml on_context :literal parse_content_as :raw def abort(parent, reader, attrs, msg) # TODO: Abort should really raise an error warn msg attrs["language"] = "plantuml" create_listing_block parent, reader.source, attrs.reject { |k, v| k == 1 } end def process(parent, reader, attrs) PlantUMLBlockMacroBackend.plantuml_installed? or return abort(parent, reader, attrs, "PlantUML not installed") filename = PlantUMLBlockMacroBackend.generate_file(parent, reader) or return abort(parent, reader, attrs, "Failed to process PlantUML") through_attrs = PlantUMLBlockMacroBackend.generate_attrs attrs through_attrs["target"] = filename create_image_block parent, through_attrs end end end end