|
| 1 | +require 'asciidoctor' |
| 2 | +require 'asciidoctor/extensions' |
| 3 | +require 'asciidoctor/converter/html5' |
| 4 | +require 'parslet' |
| 5 | + |
| 6 | +module Git |
| 7 | + module Documentation |
| 8 | + class AdocSynopsisQuote < Parslet::Parser |
| 9 | + # parse a string like "git add -p [--root=<path>]" as series of tokens keywords, grammar signs and placeholders |
| 10 | + # where placeholders are UTF-8 words separated by '-', enclosed in '<' and '>' |
| 11 | + rule(:space) { match('[\s\t\n ]').repeat(1) } |
| 12 | + rule(:space?) { space.maybe } |
| 13 | + rule(:keyword) { match('[-a-zA-Z0-9:+=~@,\./_\^\$\'"\*%!{}#]').repeat(1) } |
| 14 | + rule(:placeholder) { str('<') >> match('[[:word:]]|-').repeat(1) >> str('>') } |
| 15 | + rule(:opt_or_alt) { match('[\[\] |()]') >> space? } |
| 16 | + rule(:ellipsis) { str('...') >> match('\]|$').present? } |
| 17 | + rule(:grammar) { opt_or_alt | ellipsis } |
| 18 | + rule(:ignore) { match('[\'`]') } |
| 19 | + |
| 20 | + rule(:token) do |
| 21 | + grammar.as(:grammar) | placeholder.as(:placeholder) | space.as(:grammar) | |
| 22 | + ignore.as(:ignore) | keyword.as(:keyword) |
| 23 | + end |
| 24 | + rule(:tokens) { token.repeat(1) } |
| 25 | + root(:tokens) |
| 26 | + end |
| 27 | + |
| 28 | + class EscapedSynopsisQuote < Parslet::Parser |
| 29 | + rule(:space) { match('[\s\t\n ]').repeat(1) } |
| 30 | + rule(:space?) { space.maybe } |
| 31 | + rule(:keyword) { match('[-a-zA-Z0-9:+=~@,\./_\^\$\'"\*%!{}#]').repeat(1) } |
| 32 | + rule(:placeholder) { str('<') >> match('[[:word:]]|-').repeat(1) >> str('>') } |
| 33 | + rule(:opt_or_alt) { match('[\[\] |()]') >> space? } |
| 34 | + rule(:ellipsis) { str('...') >> match('\]|$').present? } |
| 35 | + rule(:grammar) { opt_or_alt | ellipsis } |
| 36 | + rule(:ignore) { match('[\'`]') } |
| 37 | + |
| 38 | + rule(:token) do |
| 39 | + grammar.as(:grammar) | placeholder.as(:placeholder) | space.as(:grammar) | |
| 40 | + ignore.as(:ignore) | keyword.as(:keyword) |
| 41 | + end |
| 42 | + rule(:tokens) { token.repeat(1) } |
| 43 | + root(:tokens) |
| 44 | + end |
| 45 | + |
| 46 | + class SynopsisQuoteToAdoc < Parslet::Transform |
| 47 | + rule(grammar: simple(:grammar)) { grammar.to_s } |
| 48 | + rule(keyword: simple(:keyword)) { "{empty}`#{keyword}`{empty}" } |
| 49 | + rule(placeholder: simple(:placeholder)) { "__#{placeholder}__" } |
| 50 | + rule(ignore: simple(:ignore)) { '' } |
| 51 | + end |
| 52 | + |
| 53 | + class SynopsisQuoteToHtml5 < Parslet::Transform |
| 54 | + rule(grammar: simple(:grammar)) { grammar.to_s } |
| 55 | + rule(keyword: simple(:keyword)) { "<code>#{keyword}</code>" } |
| 56 | + rule(placeholder: simple(:placeholder)) { "<em>#{placeholder}</em>" } |
| 57 | + rule(ignore: simple(:ignore)) { '' } |
| 58 | + end |
| 59 | + |
| 60 | + class SynopsisConverter |
| 61 | + def convert(parslet_parser, parslet_transform, reader, logger = nil) |
| 62 | + reader.lines.map do |l| |
| 63 | + parslet_transform.apply(parslet_parser.parse(l)).join |
| 64 | + end.join("\n") |
| 65 | + rescue Parslet::ParseFailed |
| 66 | + logger&.info "synopsis parsing failed for '#{reader.lines.join(' ')}'" |
| 67 | + reader.lines.map do |l| |
| 68 | + parslet_transform.apply(placeholder: l) |
| 69 | + end.join("\n") |
| 70 | + end |
| 71 | + end |
| 72 | + |
| 73 | + class SynopsisBlock < Asciidoctor::Extensions::BlockProcessor |
| 74 | + use_dsl |
| 75 | + named :synopsis |
| 76 | + parse_content_as :simple |
| 77 | + |
| 78 | + def process(parent, reader, attrs) |
| 79 | + outlines = SynopsisConverter.new.convert( |
| 80 | + AdocSynopsisQuote.new, |
| 81 | + SynopsisQuoteToAdoc.new, |
| 82 | + reader, |
| 83 | + parent.document.logger |
| 84 | + ) |
| 85 | + create_block parent, :verse, outlines, attrs |
| 86 | + end |
| 87 | + end |
| 88 | + |
| 89 | + # register a html5 converter that takes in charge |
| 90 | + # to convert monospaced text into Git style synopsis |
| 91 | + class GitHTMLConverter < Asciidoctor::Converter::Html5Converter |
| 92 | + extend Asciidoctor::Converter::Config |
| 93 | + register_for 'html5' |
| 94 | + |
| 95 | + def convert_inline_quoted(node) |
| 96 | + if node.type == :monospaced |
| 97 | + SynopsisConverter.new.convert( |
| 98 | + EscapedSynopsisQuote.new, |
| 99 | + SynopsisQuoteToHtml5.new, |
| 100 | + node.text, |
| 101 | + node.document.logger |
| 102 | + ) |
| 103 | + else |
| 104 | + open, close, tag = QUOTE_TAGS[node.type] |
| 105 | + if node.id |
| 106 | + class_attr = node.role ? %( class="#{node.role}") : '' |
| 107 | + if tag |
| 108 | + %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close}) |
| 109 | + else |
| 110 | + %(<span id="#{node.id}"#{class_attr}>#{open}#{node.text}#{close}</span>) |
| 111 | + end |
| 112 | + elsif node.role |
| 113 | + if tag |
| 114 | + %(#{open.chop} class="#{node.role}">#{node.text}#{close}) |
| 115 | + else |
| 116 | + %(<span class="#{node.role}">#{open}#{node.text}#{close}</span>) |
| 117 | + end |
| 118 | + else |
| 119 | + %(#{open}#{node.text}#{close}) |
| 120 | + end |
| 121 | + end |
| 122 | + end |
| 123 | + end |
| 124 | + end |
| 125 | +end |
| 126 | + |
| 127 | +Asciidoctor::Extensions.register do |
| 128 | + block Git::Documentation::SynopsisBlock |
| 129 | +end |
0 commit comments