Skip to content

Commit 87aab82

Browse files
committed
manpages: prepare for new manpage format
This commit adds a upcoming manpage format to the AsciiDoc backend. The new format changes are: * The synopsis is now a section with a dedicated style. This "synopsis" style allows to automatically format the keywords as monospaced and <placeholders> as italic. * the backticks are now used to format synopsis-like syntax in inline elements. The parsing of synopsis is done with a new AsciiDoc extension that makes use of the PEG parser parslet. All the asciidoc manpages sources are processed with this extension. It may upset the formatting for older manpages, making it not consistent across a page, but this will be a mild side effect, as this was not really consistent before. Signed-off-by: Jean-Noël Avila <[email protected]>
1 parent d9070e6 commit 87aab82

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed

Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ gem "rss"
77
gem "asciidoctor", "~> 2.0.0"
88
gem "nokogiri"
99
gem "diffy"
10+
gem "parslet"

script/asciidoctor-extensions.rb

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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('&lt;') >> match('[[:word:]]|-').repeat(1) >> str('&gt;') }
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

script/update-docs.rb

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'yaml'
1010
require 'diffy'
1111
require_relative "version"
12+
require_relative 'asciidoctor-extensions'
1213

1314
SITE_ROOT = File.join(File.expand_path(File.dirname(__FILE__)), '../')
1415
DOCS_INDEX_FILE = "#{SITE_ROOT}external/docs/content/docs/_index.html"

0 commit comments

Comments
 (0)