Skip to content

Commit 14bcc72

Browse files
committed
add support for git config files
Docs scraped off git-scm.com. Parser is built with pyparsing.
1 parent f64ccd1 commit 14bcc72

File tree

15 files changed

+377
-8
lines changed

15 files changed

+377
-8
lines changed

showdocs/annotators/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from showdocs import errors
22

3-
import sql, nginx
3+
import sql, nginx, gitconfig
44

5-
_all = [sql.SqlAnnotator, nginx.NginxAnnotator]
5+
_all = [sql.SqlAnnotator, nginx.NginxAnnotator, gitconfig.GitConfigAnnotator]
66
_annotators = {}
77

88
for a in _all:

showdocs/annotators/gitconfig.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import logging
2+
import pyparsing
3+
4+
from showdocs import structs, errors
5+
from showdocs.parsers import gitconfig, ast
6+
from showdocs.annotators import base
7+
8+
logger = logging.getLogger(__name__)
9+
10+
def _reraiseparseexception(e, text):
11+
# pyparsing usually sets the location to the end of the string,
12+
# which isn't entirely useful for error messages...
13+
if e.loc == len(text):
14+
e.loc -= 1
15+
raise errors.ParsingError(None, text, e.loc)
16+
17+
class GitConfigAnnotator(base.Annotator):
18+
alias = ['gitconfig']
19+
20+
def __init__(self, lang):
21+
super(GitConfigAnnotator, self).__init__(lang)
22+
23+
def format(self, text, opts):
24+
# TODO
25+
return text
26+
27+
def visit(self, node):
28+
# The root node, just visit its parts.
29+
if node.kind == 'config':
30+
for n in node.parts:
31+
self.visit(n)
32+
elif node.kind == 'section':
33+
# Add an annotation with group 'section.<name>' where name is the
34+
# sections' name.
35+
section = node.name[0].lower()
36+
subsection = None
37+
if len(node.name) == 2:
38+
subsection = node.name[1].lower()
39+
self._append(node.pos[0], node.pos[1], 'section.%s' % section,
40+
[structs.decorate.BLOCK])
41+
42+
# The alias section is made up of user-defined keys that have no
43+
# docs.
44+
if section == 'alias':
45+
return
46+
47+
# Annotate the actual keys.
48+
for n in node.parts:
49+
if n.kind == 'namevalue':
50+
name = n.name
51+
group = '%s.%s' % (section, name.value.lower())
52+
53+
self._append(name.pos[0], name.pos[1], group,
54+
[structs.decorate.BACK])
55+
56+
def annotate(self, text, dumptree=False):
57+
self.docs.add('gitconfig/git-config.html')
58+
try:
59+
parsed = gitconfig.loads(text)
60+
except pyparsing.ParseException, e:
61+
_reraiseparseexception(e, text)
62+
assert parsed.kind == 'config'
63+
64+
if dumptree:
65+
print parsed.dump()
66+
67+
self.visit(parsed)
68+
return self.annotations

showdocs/filters/gitconfig.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import logging
2+
import re, copy
3+
import lxml
4+
5+
from lxml.html import builder
6+
7+
from showdocs import structs
8+
from showdocs.filters import common
9+
10+
logger = logging.getLogger(__name__)
11+
12+
class CleanHtmlFilter(common.Filter):
13+
def process(self):
14+
for e in self.root.cssselect('.sect1 > h2'):
15+
if e.text.lower() == 'configuration file':
16+
return e.getparent()
17+
18+
raise ValueError("couldn't find 'configuration file' section")
19+
20+
class AnnotatingFilter(common.Filter):
21+
patterns = {'alias.*': 'section.alias', ' (deprecated)': ''}
22+
23+
def _addoptionsforsection(self, root, section):
24+
for e in root.cssselect('dt.hdlist1'):
25+
self.handled.add(e)
26+
name = e.text_content().lower()
27+
self._spanify(e, '%s.%s' % (section, name), structs.decorate.BACK)
28+
29+
def _spanify(self, e, group, decoration):
30+
assert e.tag == 'dt', 'expected tag dt, got %r' % e.tag
31+
32+
# Wrap the inner html of e in a <span> because the <dt> stretches to
33+
# 100% width which messes up the back decoration.
34+
span = copy.deepcopy(e)
35+
span.tag = 'span'
36+
span.set('data-showdocs', group)
37+
span.classes.add(decoration)
38+
39+
attrs = e.items()
40+
e.clear()
41+
for k, v in attrs:
42+
e.set(k, v)
43+
e.append(span)
44+
45+
def process(self):
46+
self.handled = set()
47+
48+
# Go over top level options.
49+
for e in self.root.cssselect('.sect2 > .dlist > dl > dt.hdlist1'):
50+
if e in self.handled:
51+
continue
52+
53+
name = e.text_content().lower()
54+
55+
# Replace any patterns found in name.
56+
for substring, replacewith in self.patterns.iteritems():
57+
if substring in name:
58+
name = name.replace(substring, replacewith)
59+
break
60+
61+
# Most options take this simple form.
62+
m = re.match(r'(\w+)\.(<\w+>\.)?(\w+)$', name)
63+
if m:
64+
self.handled.add(e)
65+
66+
# Get rid of the subsection and set the group name to be
67+
# section.option-name.
68+
section, subsection, key = m.groups()
69+
self._spanify(e, '%s.%s' % (section, key),
70+
structs.decorate.BACK)
71+
elif name == 'advice.*':
72+
self.handled.add(e)
73+
self._addoptionsforsection(e.getnext(), 'advice')
74+
else:
75+
logger.warn("didn't annotate %r", e.text_content())

showdocs/parsers/gitconfig.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from pyparsing import *
2+
3+
from showdocs.parsers import ast
4+
5+
def _nodeify(name):
6+
def f(s, l, t):
7+
return ast.Node(kind=name, pos=(l, l + len(t[0])), value=t[0])
8+
return f
9+
10+
def _nodeifynamevalue(s, l, t):
11+
t = t.asList()
12+
name = t[0]
13+
value = t[-1]
14+
if len(t) == 1:
15+
value = True
16+
return ast.Node(pos=(l, t[-1].pos[1]),
17+
kind='namevalue',
18+
name=name,
19+
value=value)
20+
21+
def _nodeifysection(s, l, t):
22+
t = t.asList()
23+
name = t[0]
24+
values = t[1]
25+
return ast.Node(pos=(l, values[-1].pos[1]),
26+
kind='section',
27+
name=t[0],
28+
parts=t[1])
29+
30+
def _nodeifyall(s, l, t):
31+
sections = t.asList()
32+
return ast.Node(pos=(l, sections[-1].pos[1]),
33+
kind='config',
34+
parts=sections)
35+
36+
comment = Combine((Literal(';') | '#') + Optional(restOfLine))
37+
name = Word(alphas, alphanums + '-')
38+
name.setParseAction(_nodeify('name'))
39+
value = Word(printables) + restOfLine
40+
value.setParseAction(_nodeify('value'))
41+
namevalue = name + Optional(Literal('=').suppress() + Optional(value))
42+
namevalue.setParseAction(_nodeifynamevalue)
43+
44+
section_header = Suppress('[') + Group(Word(alphanums + '._') + Optional(
45+
dblQuotedString)) + Suppress(']')
46+
section_body = Group(ZeroOrMore(namevalue))
47+
section = section_header + Optional(section_body, [])
48+
section.setParseAction(_nodeifysection)
49+
50+
parser = OneOrMore(section)
51+
parser.ignore(comment)
52+
parser.setParseAction(_nodeifyall)
53+
54+
55+
def loads(s):
56+
return parser.parseString(s).asList()[0]

showdocs/repos/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__all__ = ['nginx', 'sql']
1+
__all__ = ['nginx', 'sql', 'gitconfig']

showdocs/repos/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def httpget(self, url):
119119
'''Calls requests.get on the given URL and returns the response bytes.'''
120120
headers = {'user-agent': 'showthedocs'}
121121

122+
self.log('info', 'http get: url=%s', url)
122123
# Let requests find the encoding and return a Unicode string, then
123124
# encode it as utf8.
124125
return requests.get(url, headers=headers).text.encode('utf8')

showdocs/repos/gitconfig.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import os
2+
3+
from showdocs import filters, repos
4+
5+
import showdocs.repos.common
6+
7+
import showdocs.filters.gitconfig
8+
9+
10+
@repos.common.register
11+
class GitConfigRepository(repos.common.ScrapedRepository):
12+
name = 'gitconfig'
13+
14+
@classmethod
15+
def filters(cls):
16+
mine = [filters.gitconfig.CleanHtmlFilter, filters.common.AbsoluteUrls,
17+
filters.gitconfig.AnnotatingFilter]
18+
return super(GitConfigRepository, cls).filters() + mine
19+
20+
def build(self):
21+
url = 'https://git-scm.com/docs/git-config'
22+
23+
path = os.path.join(self.stagingdir, 'git-config.html')
24+
with open(path, 'wb') as f:
25+
f.write(self.httpget(url))
26+
27+
self.context.path_to_url[path] = url

showdocs/static/examples.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,23 @@ ORDER BY city;`;
2727

2828
examples['postgresql'] = sql;
2929
examples['mysql'] = sql;
30+
31+
examples['gitconfig'] = `; core variables
32+
[core]
33+
; Don't trust file modes
34+
filemode = false
35+
; Our diff algorithm
36+
[diff]
37+
external = /usr/local/bin/diff-wrapper
38+
renames = true
39+
; Proxy settings
40+
[core]
41+
gitproxy=proxy-command for kernel.org
42+
gitproxy=default-proxy ; for all the rest
43+
; HTTP
44+
[http]
45+
sslVerify
46+
[http "https://weak.example.com"]
47+
sslVerify = false
48+
cookieFile = /tmp/cookie.txt
49+
`;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#query {
2+
line-height: 25px;
3+
}
4+
5+
#docs {
6+
}

showdocs/templates/docscss.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@
1010
{% assets "mysql_scss" %}
1111
<link rel="stylesheet" href="{{ ASSET_URL }}">
1212
{% endassets %}
13+
{% elif lang == "gitconfig" %}
14+
{% assets "gitconfig_scss" %}
15+
<link rel="stylesheet" href="{{ ASSET_URL }}">
16+
{% endassets %}
1317
{% endif %}

0 commit comments

Comments
 (0)