28 |
29 | This is also a valid blockquote:
30 |
31 | > ## Prerequisites
32 | > Breakfast!
33 |
34 | and it will be converted into this markdown:
35 |
36 |
37 | ## Prerequisites
38 | Breakfast!
39 |
40 |
41 |
42 | For debugging purposes you may find it useful to test the filter
43 | like this:
44 |
45 | pandoc source.md --to json | python blockquote2div.py | pandoc --from json
46 | """
47 | import pandocfilters as pf
48 |
49 | # These are classes that, if set on the title of a blockquote, will
50 | # trigger the blockquote to be converted to a div.
51 | SPECIAL_CLASSES = {
52 | "callout": ("panel-info", "glyphicon-pushpin"),
53 | "challenge": ("panel-success", "glyphicon-pencil"),
54 | "prereq": ("panel-warning", "glyphicon-education"),
55 | "getready": ("panel-warning", "glyphicon-check"),
56 | "objectives": ("panel-warning", "glyphicon-certificate"),
57 | }
58 |
59 |
60 | def find_header(blockquote):
61 | """Find attributes in a blockquote if they are defined on a
62 | header that is the first thing in the block quote.
63 |
64 | Returns the attributes, a list [id, classes, kvs]
65 | where id = str, classes = list, kvs = list of key, value pairs
66 | """
67 | if blockquote[0]['t'] == 'Header':
68 | level, attr, inline = blockquote[0]['c']
69 | return level, attr, inline
70 |
71 |
72 | def blockquote2div(key, value, format, meta):
73 | """Convert a blockquote into a div if it begins with a header
74 | that has attributes containing a single class that is in the
75 | allowed classes.
76 |
77 | This function can be passed directly to toJSONFilter
78 | from pandocfilters.
79 | """
80 | if key == 'BlockQuote':
81 | blockquote = value
82 |
83 | header = find_header(blockquote)
84 | if not header:
85 | return
86 | else:
87 | level, attr, inlines = header
88 |
89 | id, classes, kvs = attr
90 |
91 | if len(classes) == 1 and classes[0] in SPECIAL_CLASSES:
92 | panel_kind, glyphicon_kind = SPECIAL_CLASSES[classes[0]]
93 |
94 | h_level, h_attr, h_inlines = blockquote[0]['c']
95 |
96 | # insert an icon as the first sub-item of the header
97 | span = pf.Span(["", ["glyphicon", glyphicon_kind], []], [])
98 | h_inlines.insert(0, span)
99 |
100 | # only the header goes into panel-heading
101 | header = pf.Header(h_level, [h_attr[0], [], []], h_inlines)
102 | panel_header = pf.Div(("", ["panel-heading"], []), [header])
103 |
104 | # the rest of the blockquote goes into panel-body
105 | panel_body = pf.Div(("", ["panel-body"], []), blockquote[1:])
106 |
107 | # apply Bootstrap panel classes to the div
108 | classes.append("panel")
109 | classes.append(panel_kind)
110 |
111 | # a blockquote is just a list of blocks, so it can be
112 | # passed directly to Div, which expects Div(attr, blocks)
113 | if classes[0] == "callout":
114 | return [{"t": "RawBlock", "c": [ "html", "" ]}]
118 | else:
119 | return [{"t": "RawBlock", "c": [ "html", "".format(' '.join(classes)) ]},
120 | panel_header,
121 | panel_body,
122 | {"t": "RawBlock", "c": [ "html", "" ]}]
123 |
124 |
125 | if __name__ == '__main__':
126 | # pandocfilters.toJSONFilter is a convenience method that
127 | # makes a command line json filter from a given function.
128 | # JSON emitted from pandoc is read from stdin. The JSON tree is
129 | # walked, with the function being applied to each element in the
130 | # tree.
131 | #
132 | # The function passed to to JSONFilter must accept (key, value,
133 | # format, metadata) as arguments:
134 | #
135 | # key - element type (e.g. 'Str', 'Header')
136 | # value - element contents
137 | # format - destination format
138 | # metadata - document metadata
139 | #
140 | # The function return values determine what happens to the
141 | # element:
142 | # returns None: the element is unmodified;
143 | # returns []: delete the element
144 | # otherwise: replace the element with the return value
145 | #
146 | # The JSON is then output to stdout, where it can be consumed by
147 | # pandoc.
148 | pf.toJSONFilter(blockquote2div)
149 |
--------------------------------------------------------------------------------
/tools/validation_helpers.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | import json
4 | import logging
5 | import re
6 | import sys
7 |
8 | try: # Hack to make codebase compatible with python 2 and 3
9 | basestring
10 | except NameError:
11 | basestring = str
12 |
13 |
14 | # Common validation functions
15 | def is_list(text):
16 | """Validate whether the provided string can be converted to python list"""
17 | text = text.strip()
18 | try:
19 | text_as_list = json.loads(text)
20 | except ValueError:
21 | logging.debug("Could not convert string to python object: {0}".format(text))
22 | return False
23 |
24 | return isinstance(text_as_list, list)
25 |
26 |
27 | def is_str(text):
28 | """Validate whether the input is a non-blank python string"""
29 | return isinstance(text, basestring) and len(text) > 0
30 |
31 |
32 | def is_numeric(text):
33 | """Validate whether the string represents a number (including unicode)"""
34 | try:
35 | float(text)
36 | return True
37 | except ValueError:
38 | return False
39 |
40 |
41 | #### Text cleanup functions, pre-validation
42 | def strip_attrs(s):
43 | """Strip attributes of the form {.name} from a markdown title string"""
44 | return re.sub(r"\s\{\..*?\}", "", s)
45 |
46 |
47 | def get_css_class(s):
48 | """Return any and all CSS classes (when a line is suffixed by {.classname})
49 | Returns empty list when """
50 | return re.findall("\{\.(.*?)\}", s)
51 |
52 |
53 | ### Helper objects
54 | class CommonMarkHelper(object):
55 | """Basic helper functions for working with the internal abstract syntax
56 | tree produced by CommonMark parser"""
57 | def __init__(self, ast):
58 | self.data = ast
59 | self.children = self.data.children
60 |
61 | def get_doc_header_title(self):
62 | """Helper method for SWC templates: get the document title from
63 | the YAML headers"""
64 | doc_headers = self.data.children[1] # Throw index error if none found
65 |
66 | for s in doc_headers.strings:
67 | label, contents = s.split(":", 1)
68 | if label.lower() == "title":
69 | return contents.strip()
70 |
71 | # If title not found, return an empty string for display purposes
72 | return ''
73 |
74 | def get_doc_header_subtitle(self):
75 | """Helper method for SWC templates: get the document title from
76 | the YAML headers"""
77 | doc_headers = self.data.children[1] # Throw index error if none found
78 |
79 | for s in doc_headers.strings:
80 | label, contents = s.split(":", 1)
81 | if label.lower() == "subtitle":
82 | return contents.strip()
83 |
84 | # If title not found, return an empty string for display purposes
85 | return ''
86 |
87 | def get_block_titled(self, title, heading_level=2, ast_node=None):
88 | """Examine children. Return all children of the given node that:
89 | a) are blockquoted elements, and
90 | b) contain a heading with the specified text, at the specified level.
91 | For example, this can be used to find the "Prerequisites" section
92 | in index.md
93 |
94 | Returns empty list if no appropriate node is found"""
95 |
96 | # TODO: Deprecate in favor of callout validator
97 | if ast_node is None:
98 | ast_node = self.data
99 | return [n for n in ast_node.children
100 | if self.is_block(n) and
101 | self.has_section_heading(
102 | title,
103 | ast_node=n,
104 | heading_level=heading_level,
105 | show_msg=False)]
106 |
107 | # Helpers to fetch specific document sections
108 | def get_section_headings(self, ast_node=None):
109 | """Returns a list of ast nodes that are headings"""
110 | if ast_node is None:
111 | ast_node = self.data
112 | return [n for n in ast_node.children if self.is_heading(n)]
113 |
114 | def get_callouts(self, ast_node=None):
115 | if ast_node is None:
116 | ast_node = self.data
117 | return [n for n in ast_node.children if self.is_callout(n)]
118 |
119 | def find_external_links(self, ast_node=None, parent_crit=None):
120 | """Recursive function that locates all references to external content
121 | under specified node. (links or images)"""
122 | ast_node = ast_node or self.data
123 | if parent_crit is None:
124 | # User can optionally provide a function to filter link list
125 | # based on where link appears. (eg, only links in headings)
126 | # If no filter is provided, accept all links in that node.
127 | parent_crit = lambda n: True
128 |
129 | # Link can be node itself, or hiding in inline content
130 | links = [n for n in ast_node.inline_content
131 | if self.is_external(n) and parent_crit(ast_node)]
132 |
133 | if self.is_external(ast_node):
134 | links.append(ast_node)
135 |
136 | # Also look for links in sub-nodes
137 | for n in ast_node.children:
138 | links.extend(self.find_external_links(n,
139 | parent_crit=parent_crit))
140 |
141 | return links
142 |
143 | # Helpers to get information from a specific node type
144 | def get_link_info(self, link_node):
145 | """Given a link node, return the link title and destination"""
146 | if not self.is_external(link_node):
147 | raise TypeError("Cannot apply this method to something that is not a link")
148 |
149 | dest = link_node.destination
150 | try:
151 | link_text = link_node.label[0].c
152 | except:
153 | link_text = None
154 |
155 | return dest, link_text
156 |
157 | def get_heading_info(self, heading_node):
158 | """Get heading text and list of all css styles applied"""
159 | heading = heading_node.strings[0]
160 | text = strip_attrs(heading)
161 | css = get_css_class(heading)
162 | return text, css
163 |
164 | # Functions to query type or content of nodes
165 | def has_section_heading(self, section_title, ast_node=None,
166 | heading_level=2, limit=sys.maxsize, show_msg=True):
167 | """Does the section contain (<= x copies of) specified heading text?
168 | Will strip off any CSS attributes when looking for the section title"""
169 | if ast_node is None:
170 | ast_node = self.data
171 |
172 | num_nodes = len([n for n in self.get_section_headings(ast_node)
173 | if (strip_attrs(n.strings[0]) == section_title)
174 | and (n.level == heading_level)])
175 |
176 | # Suppress error msg if used as a helper method
177 | if show_msg and num_nodes == 0:
178 | logging.error("Document does not contain the specified "
179 | "heading: {0}".format(section_title))
180 | elif show_msg and num_nodes > limit:
181 | logging.error("Document must not contain more than {0} copies of"
182 | " the heading {1}".format(limit, section_title or 0))
183 | elif show_msg:
184 | logging.info("Verified that document contains the specified"
185 | " heading: {0}".format(section_title))
186 | return (0 < num_nodes <= limit)
187 |
188 | def has_number_children(self, ast_node,
189 | exact=None, minc=0, maxc=sys.maxsize):
190 | """Does the specified node (such as a bulleted list) have the expected
191 | number of children?"""
192 |
193 | if exact: # If specified, must have exactly this number of children
194 | minc = maxc = exact
195 |
196 | return (minc <= len(ast_node.children) <= maxc)
197 |
198 | # Helpers, in case the evolving CommonMark spec changes the names of nodes
199 | def is_hr(self, ast_node):
200 | """Is the node a horizontal rule (hr)?"""
201 | return ast_node.t == 'HorizontalRule'
202 |
203 | def is_heading(self, ast_node, heading_level=None):
204 | """Is the node a heading/ title?"""
205 | has_tag = ast_node.t == "ATXHeader"
206 |
207 | if heading_level is None:
208 | has_level = True
209 | else:
210 | has_level = (ast_node.level == heading_level)
211 | return has_tag and has_level
212 |
213 | def is_paragraph(self, ast_node):
214 | """Is the node a paragraph?"""
215 | return ast_node.t == "Paragraph"
216 |
217 | def is_list(self, ast_node):
218 | """Is the node a list? (ordered or unordered)"""
219 | return ast_node.t == "List"
220 |
221 | def is_link(self, ast_node):
222 | """Is the node a link?"""
223 | return ast_node.t == "Link"
224 |
225 | def is_external(self, ast_node):
226 | """Does the node reference content outside the file? (image or link)"""
227 | return ast_node.t in ("Link", "Image")
228 |
229 | def is_block(self, ast_node):
230 | """Is the node a BlockQuoted element?"""
231 | return ast_node.t == "BlockQuote"
232 |
233 | def is_callout(self, ast_node):
234 | """Composite element: "callout" elements in SWC templates are
235 | blockquotes whose first child element is a heading"""
236 | if len(ast_node.children) > 0 and \
237 | self.is_heading(ast_node.children[0]):
238 | has_heading = True
239 | else:
240 | has_heading = False
241 |
242 | return self.is_block(ast_node) and has_heading
243 |
--------------------------------------------------------------------------------
/js/modernizr.custom.js:
--------------------------------------------------------------------------------
1 | /* Modernizr 2.0.6 (Custom Build) | MIT & BSD
2 | * Contains: fontface | backgroundsize | borderimage | borderradius | boxshadow | flexbox | hsla | multiplebgs | opacity | rgba | textshadow | cssanimations | csscolumns | generatedcontent | cssgradients | cssreflections | csstransforms | csstransforms3d | csstransitions | applicationcache | canvas | canvastext | draganddrop | hashchange | history | audio | video | indexeddb | input | inputtypes | localstorage | postmessage | sessionstorage | websockets | websqldatabase | webworkers | geolocation | inlinesvg | smil | svg | svgclippaths | touch | webgl | iepp | cssclasses | addtest | teststyles | testprop | testallprops | hasevent | prefixes | domprefixes | load
3 | */
4 | ;window.Modernizr=function(a,b,c){function H(){e.input=function(a){for(var b=0,c=a.length;b",a,""].join(""),k.id=i,k.innerHTML+=f,g.appendChild(k),h=c(k,a),k.parentNode.removeChild(k);return!!h},w=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=C(e[d],"function"),C(e[d],c)||(e[d]=c),e.removeAttribute(d))),e=null;return f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),x,y={}.hasOwnProperty,z;!C(y,c)&&!C(y.call,c)?z=function(a,b){return y.call(a,b)}:z=function(a,b){return b in a&&C(a.constructor.prototype[b],c)};var G=function(c,d){var f=c.join(""),g=d.length;v(f,function(c,d){var f=b.styleSheets[b.styleSheets.length-1],h=f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"",i=c.childNodes,j={};while(g--)j[i[g].id]=i[g];e.touch="ontouchstart"in a||j.touch.offsetTop===9,e.csstransforms3d=j.csstransforms3d.offsetLeft===9,e.generatedcontent=j.generatedcontent.offsetHeight>=1,e.fontface=/src/i.test(h)&&h.indexOf(d.split(" ")[0])===0},g,d)}(['@font-face {font-family:"font";src:url("https://")}',["@media (",o.join("touch-enabled),("),i,")","{#touch{top:9px;position:absolute}}"].join(""),["@media (",o.join("transform-3d),("),i,")","{#csstransforms3d{left:9px;position:absolute}}"].join(""),['#generatedcontent:after{content:"',m,'";visibility:hidden}'].join("")],["fontface","touch","csstransforms3d","generatedcontent"]);r.flexbox=function(){function c(a,b,c,d){a.style.cssText=o.join(b+":"+c+";")+(d||"")}function a(a,b,c,d){b+=":",a.style.cssText=(b+o.join(c+";"+b)).slice(0,-b.length)+(d||"")}var d=b.createElement("div"),e=b.createElement("div");a(d,"display","box","width:42px;padding:0;"),c(e,"box-flex","1","width:10px;"),d.appendChild(e),g.appendChild(d);var f=e.offsetWidth===42;d.removeChild(e),g.removeChild(d);return f},r.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},r.canvastext=function(){return!!e.canvas&&!!C(b.createElement("canvas").getContext("2d").fillText,"function")},r.webgl=function(){return!!a.WebGLRenderingContext},r.touch=function(){return e.touch},r.geolocation=function(){return!!navigator.geolocation},r.postmessage=function(){return!!a.postMessage},r.websqldatabase=function(){var b=!!a.openDatabase;return b},r.indexedDB=function(){for(var b=-1,c=p.length;++b7)},r.history=function(){return!!a.history&&!!history.pushState},r.draganddrop=function(){return w("dragstart")&&w("drop")},r.websockets=function(){for(var b=-1,c=p.length;++b";return(a.firstChild&&a.firstChild.namespaceURI)==q.svg},r.smil=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"animate")))},r.svgclippaths=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"clipPath")))};for(var I in r)z(r,I)&&(x=I.toLowerCase(),e[x]=r[I](),u.push((e[x]?"":"no-")+x));e.input||H(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)z(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return;b=typeof b=="boolean"?b:!!b(),g.className+=" "+(b?"":"no-")+a,e[a]=b}return e},A(""),j=l=null,a.attachEvent&&function(){var a=b.createElement("div");a.innerHTML="";return a.childNodes.length!==1}()&&function(a,b){function s(a){var b=-1;while(++b 1.14. */
60 | h1.panel,
61 | h2.panel,
62 | h3.panel,
63 | h4.panel,
64 | h5.panel,
65 | h6.panel {
66 | margin-top: 0px;
67 | margin-bottom: 0px;
68 | border: 0px;
69 | color: inherit;
70 | background-color: inherit;
71 | background-image: inherit;
72 | box-shadow: none;
73 | }
74 |
75 | /* Comments in code. */
76 | .comment {
77 | color: purple;
78 | }
79 |
80 | /* Error messages. */
81 | .err {
82 | color: darkred;
83 | font-style: italic;
84 | }
85 |
86 | /* Things to fix. */
87 | .fixme {
88 | text-decoration: underline;
89 | color: darkred;
90 | background-color: lightgray;
91 | }
92 |
93 | /* Highlighted changes in code. */
94 | .highlight {
95 | background-color: mistyrose;
96 | }
97 |
98 | /* Manual input. */
99 | .in {
100 | color: darkgreen;
101 | }
102 |
103 | /* Program output. */
104 | .out {
105 | color: darkblue;
106 | font-style: italic;
107 | }
108 |
109 | /* Putting shadows around things. */
110 | .shadow {
111 | -moz-box-shadow: 0 0 30px 5px #999;
112 | -webkit-box-shadow: 0 0 30px 5px #999;
113 | box-shadow: 0 0 30px 5px #999;
114 | }
115 |
116 | /* Things to understand (lead-in to sections in book). */
117 | .understand {
118 | background-color: lightyellow;
119 | }
120 |
121 | /* Block quotations. */
122 | blockquote {
123 | margin: 1em;
124 | padding: 1em 1em .5em 1em;
125 | width: 90%;
126 | font-size: inherit;
127 | }
128 |
129 | /* Citation for testimonial quote. */
130 | blockquote.testimonial cite {
131 | font-style: italic;
132 | font-size: inherit;
133 | }
134 |
135 | /* Images
136 | *
137 | * Rules from http://getbootstrap.com/css/#images-responsive.
138 | *
139 | * This is compatible with Pandoc behavior for HTML and HTML5. */
140 | article img {
141 | max-width: 100%;
142 | height: auto;
143 | display: block;
144 | margin-left: auto;
145 | margin-right: auto;
146 | }
147 |
148 | article div.figure,
149 | article figure {
150 | text-align: center;
151 | }
152 |
153 | article p.caption::before,
154 | article figcaption::before {
155 | content: "Figure: ";
156 | }
157 |
158 | /* Main body of pages. */
159 | body {
160 | font-family: "Open Sans", "Helvetica", "Arial", sans-serif;
161 | color: rgb(03, 03, 03);
162 | }
163 |
164 | body.workshop, body.lesson {
165 | background-color: #BEC3C6;
166 | margin: 20px 0;
167 | }
168 |
169 | /* Styling for editorial stylesheet */
170 | body.stylesheet {
171 | background: #ffffff;
172 | width: 60em;
173 | margin: 20px auto;
174 | }
175 |
176 | /* Code sample */
177 | pre.sourceCode,
178 | pre.input {
179 | color: ForestGreen;
180 | }
181 | pre.output {
182 | color: MediumBlue;
183 | }
184 | pre.error {
185 | color: Red;
186 | }
187 |
188 | @media (max-width: 700px) {
189 | div.banner a img {
190 | padding: 20px 0px;
191 | }
192 | }
193 |
194 | /*----------------------------------------*/
195 | /* Override Bootstrap CSS */
196 | /*----------------------------------------*/
197 |
198 | /* navbar */
199 | .navbar {
200 | min-height: 85px;
201 | margin-bottom: 0;
202 | }
203 |
204 | .navbar-nav {
205 | margin: 15px 10px 0px 0px;
206 | }
207 |
208 | #swc-navbar {
209 | border-top: 5px solid #2b3990;
210 | width: 100%;
211 | background: #d6d6d6;
212 | border-bottom: 1px solid #CCC;
213 | }
214 |
215 | .navbar-brand {
216 | position: absolute;
217 | padding-top: 7px;
218 | }
219 |
220 | .navbar-brand img {
221 | width: 250px;
222 | height: 50px;
223 | }
224 |
225 | #swc-navbar-search {
226 | background-color: #ffffff;
227 | color: #666666;
228 | border-color:#2b3990;
229 | width: 150px;
230 | }
231 |
232 | .navbar-default .navbar-nav > li > a {
233 | color: #20267D;
234 | }
235 |
236 | .navbar-nav li {
237 | margin-right: -7px;
238 | margin-left: -7px;
239 | }
240 |
241 | .navbar-nav .navbar {
242 | diplay: inline-block;
243 | float: none;
244 | }
245 |
246 | .navbar .navbar-collapse {
247 | text-align: center;
248 | }
249 |
250 | .navbar-nav .nav-collapse .nav > li > a,
251 | .navbar-nav .nav-collapse .dropdown-menu a {
252 | color: #2b3990;
253 | text-align: center;
254 | }
255 |
256 | .navbar-nav .nav .active > a,
257 | .navbar-nav .nav .active > a:hover, .navbar-nav .nav .active > a:focus {
258 | color: #ffffff;
259 | background-color: #20267D;
260 | }
261 |
262 | .navbar-nav .nav li.dropdown.open > .dropdown-toggle,
263 | .navbar-nav .nav li.dropdown.active > .dropdown-toggle,
264 | .navbar-nav .nav li.dropdown.open.active > .dropdown-toggle {
265 | color: #ffffff;
266 | background-color: #20267D;
267 | }
268 | .navbar-nav .nav li.dropdown.active > .dropdown-toggle .caret {
269 | border-top-color: #999999;
270 | border-bottom-color: #999999;
271 | }
272 | .navbar-nav .nav li.dropdown.open > .dropdown-toggle .caret {
273 | border-top-color: #ffffff;
274 | border-bottom-color: #ffffff;
275 | }
276 |
277 | blockquote p {
278 | font-size: inherit;
279 | font-weight: inherit;
280 | line-height: inherit;
281 | }
282 |
283 | blockquote h2{
284 | margin-top: 0px;
285 | }
286 |
287 | /* readability: darken the alert colour for contrast with background */
288 |
289 | .alert {
290 | color: rgb(0, 0, 0);
291 | }
292 |
293 | code {
294 | color: #333333;
295 | }
296 |
297 | /* Top banner of every page. */
298 | div.banner {
299 | background-color: #FFFFFF;
300 | width: 100%;
301 | height: 90px;
302 | margin: 0px;
303 | padding: 0;
304 | border-bottom: 1px solid #A6A6A6;
305 | }
306 |
307 | /* Padding around image in top banner. */
308 | div.banner a img {
309 | padding: 20px 25px;
310 | }
311 |
312 | /* Explanatory call-out boxes. */
313 | div.box {
314 | width: 54em;
315 | background-color: mistyrose;
316 | display: block;
317 | margin-left: auto;
318 | margin-right: auto;
319 | padding-top: 1px;
320 | padding-bottom: 1px;
321 | padding-left: 10px;
322 | padding-right: 10px;
323 | outline-color: gray;
324 | outline-width: 1px;
325 | outline-style: solid;
326 | }
327 |
328 | /* Level 2 headings inside pages. */
329 | div.content div h3 {
330 | border-bottom: 1px solid #CCCCCC;
331 | display: block;
332 | font-family: Verdana,"BitStream vera Sans";
333 | margin-top: 10px;
334 | padding: 0 5px 3px;
335 | }
336 |
337 | /* PDF and slide files referenced from lectures. */
338 | div.files {
339 | padding: 10px;
340 | }
341 |
342 | .swc-blue-bg {
343 | /*background-color: #20267D;*/
344 | /* svg colour is slightly different? */
345 | background: #2b3990;
346 | }
347 |
348 | /* Main menu at the top of every page. */
349 | div.mainmenu {
350 | clear: both;
351 | background-color: #F4F4F4;
352 | margin: 0px;
353 | padding: 3px 0px 3px 25px;
354 | border-bottom: 1px solid #A6A6A6;
355 | height: 30px
356 | }
357 |
358 | /* Narration for audio-only lectures. */
359 | div.narration {
360 | text-align: center;
361 | font-size: 2em;
362 | }
363 |
364 | /* Table of contents. */
365 | div.toc {
366 | /* No special styling yet. */
367 | }
368 |
369 | .transcript {
370 | display: table;
371 | }
372 |
373 | .transcript .media img {
374 | border: 1px solid grey;
375 | }
376 |
377 | /* YouTube video embed. */
378 | div.youtube {
379 | text-align: center;
380 | padding: 10px;
381 | }
382 |
383 | /* Glossary description lists. */
384 | dl.gloss {
385 | /* Empty for now. */
386 | }
387 |
388 | /* Displaying YouTube videos. */
389 | iframe.youtube_player {
390 | border: 0;
391 | text-align: center;
392 | width: 640px;
393 | height: 500px;
394 | }
395 |
396 | /* Sections in book chapters. */
397 | section {
398 | clear: both;
399 | }
400 |
401 | /* Person's name in team.html. */
402 | .person {
403 | font-weight: bold;
404 | font-style: italic;
405 | }
406 |
407 | /* Short review of book in bibliography. */
408 | span.review {
409 | font-style: italic;
410 | }
411 |
412 | /* Bibliography book covers. */
413 | img.book-cover {
414 | width: 80px;
415 | }
416 |
417 | /* Blog calendar table in blog/index.html. */
418 | table.blogcalendar th {
419 | text-align : right;
420 | font-weight : bold;
421 | }
422 |
423 | /* See above. */
424 | table.blogcalendar th.left {
425 | text-align : left;
426 | }
427 |
428 | /* See above. */
429 | table.blogcalendar tr td {
430 | text-align : right;
431 | }
432 |
433 | /* Blog index tables in blog/index.html. */
434 | table.blogindex td.date {
435 | text-align: left ;
436 | width:10em;
437 | }
438 |
439 | /* Tables used for displaying choices in challenges. */
440 | table.choices tr td {
441 | vertical-align : top;
442 | }
443 |
444 | /* Database tables do _not_ have double borders */
445 | table.outlined {
446 | border-collapse: collapse;
447 | }
448 |
449 | /* Link items (to workshop pages) in the workshops tables */
450 | table.workshops td.link {
451 | width: 50%;
452 | text-align: left;
453 | }
454 |
455 | /* Spacer items (i.e. ellipsis) on the workshops tables */
456 | table.workshops td.spacer {
457 | max-width: 100%;
458 | text-align: center;
459 | }
460 |
461 | /* Date columns on the workshops tables */
462 | table.workshops td.date {
463 | width: 50%;
464 | text-align: right;
465 | }
466 |
467 | /* Badge modal dialog */
468 | #assertion-modal {
469 | width:700px;
470 | margin-left:-350px;
471 | }
472 | #assertion-modal iframe {
473 | background-color: transparent;
474 | border: 0px none transparent;
475 | padding: 0px;
476 | width: 100%;
477 | height: 20em;
478 | }
479 |
480 | #assertion-model img.badge {
481 | position: absolute;
482 | right: 15px;
483 | bottom: 35px;
484 | opacity: 0.5;
485 | }
486 |
487 | #modal-badge-img {
488 | position:absolute;
489 | right: 25px;
490 | bottom: 25px;
491 | opacity: 0.5;
492 | }
493 | #assertion-modal.in {
494 | color: black;
495 | }
496 |
497 | /* list with checkbox as bullet */
498 | ul.checklist {
499 | list-style-image: url('../img/checkbox.png');
500 | }
501 |
502 | /* FAQ */
503 | dl.faq dt {
504 | font-style: italic;
505 | font-weight: bold;
506 | }
507 |
508 | section.content {
509 | width:100%;
510 | background: white;
511 | }
512 |
513 | dd {
514 | margin-left: 10px;
515 | }
516 |
517 | .header.home {
518 | background: url(../img/header.png) no-repeat center center;
519 | background-attachment: fixed;
520 | -webkit-background-size: cover;
521 | -moz-background-size: cover;
522 | -o-background-size: cover;
523 | background-size: cover;
524 | }
525 |
526 | .header {
527 | background:#2b3990;
528 | }
529 |
530 | .header h1 {
531 | line-height: 1.1;
532 | margin: 60px 0px 80px;
533 | font-size: 40pt;
534 | }
535 |
536 | table {
537 | margin-bottom: 15px;
538 | }
539 |
540 | table th, table td {
541 | padding: 5px 10px;
542 | }
543 |
544 | table > thead > .header {
545 | background: transparent;
546 | }
547 |
548 | table > thead > tr > td, table > thead > tr > th,
549 | table > tbody > tr > td, table > tbody > tr > th,
550 | table > tfoot > tr > td, table > tfoot > tr > th {
551 | border: 1px solid #DDD;
552 | }
553 |
554 | table > thead > tr > th,
555 | table > thead > tr > td {
556 | border-bottom-width: 2px;
557 | }
558 |
559 | table tbody > tr:nth-of-type(2n+1) {
560 | background-color: #F9F9F9;
561 | }
562 |
563 | #header-text {
564 | font-size:20pt;
565 | margin:0;
566 | }
567 |
568 | #home-options {
569 | background:#F6F6F6;
570 | border-top:1px solid #DDDDDD;
571 | border-bottom:1px solid #DDDDDD;
572 | padding:20px 0;
573 | margin-bottom:20px;
574 | }
575 |
576 | #title {
577 | background:#F6F6F6;
578 | border-top:1px solid #DDDDDD;
579 | border-bottom:1px solid #DDDDDD;
580 | padding:0 0 20px;
581 | margin-bottom:20px;
582 | }
583 |
584 | h5 a:link, h5 a:visited,
585 | h4 a:link, h4 a:visited,
586 | h3 a:link, h3 a:visited {
587 | color:#2b3990;
588 | }
589 |
590 | h5 a:hover,
591 | h4 a:hover,
592 | h3 a:hover {
593 | color:#C26D17;
594 | text-decoration: none;
595 | }
596 |
597 | a {
598 | color:#3E51CF;
599 | -webkit-transition: all 0.2s ease;
600 | -moz-transition: all 0.2s ease;
601 | -o-transition: all 0.2s ease;
602 | transition: all 0.2s ease;
603 | }
604 |
605 | a:hover {
606 | color:#965412;
607 | }
608 |
609 | footer {
610 | background: #2b3990;
611 | padding: 20px 0;
612 | font-size: 10pt;
613 | margin-top: 10px;
614 | }
615 |
616 | footer a, footer a:hover{
617 | color:#FFF;
618 | padding-left: 10px;
619 | }
620 |
621 | footer .container .links {
622 | background:url('../img/software-carpentry-banner-white.png') no-repeat;
623 | background-size: 200px;
624 | background-position: 0;
625 | min-height: 40px;
626 | padding: 20px 0px 0px 200px;
627 | text-align: right;
628 | }
629 |
630 |
631 | /* Footer of every page. */
632 | /* TODO -- might clash with site footer */
633 | div.footer {
634 | clear: both;
635 | background: url("/img/main_shadow.png") repeat-x scroll center top #FFFFFF;
636 | padding: 4px 10px 7px 10px;
637 | border-top: 1px solid #A6A6A6;
638 | text-align: right;
639 | }
640 |
641 |
642 | /* doesn't seem to be used on site, workshop or lesson */
643 | /* Chapter titles. */
644 | div.chapter h2 {
645 | text-align: center;
646 | font-style: italic;
647 | }
648 |
649 |
650 | /* For the Request a Workshop form */
651 | #ss-form .ss-q-title {
652 | display: block;
653 | font-weight: bold;
654 | padding-bottom: 0.5em;
655 | }
656 | #ss-form .ss-required-asterisk {
657 | color: #c43b1d;
658 | }
659 | #ss-form label {
660 | display:inline;
661 | cursor: default;
662 | }
663 | #ss-form .ss-secondary-text {
664 | color: #666;
665 | }
666 | #ss-form .ss-form-entry {
667 | margin-bottom: 1.5em;
668 | }
669 | #ss-form ul {
670 | margin:0;
671 | padding:0;
672 | list-style: none;
673 | }
674 | #ss-form ol {
675 | margin:0;
676 | }
677 | #ss-form .ss-choice-item {
678 | line-height: 1.3em;
679 | padding-bottom: .5em;
680 | }
681 | #ss-form .ss-q-long {
682 | resize: vertical;
683 | width: 70%;
684 | }
685 | #ss-form .error-message, .required-message {
686 | display: none;
687 | }
688 | #ss-form .ss-form-entry input {
689 | vertical-align: middle;
690 | margin: 0;
691 | padding:0 4px;
692 | }
693 | #ss-form .ss-choice-item-control {
694 | padding-right: 4px;
695 | }
696 |
697 |
698 | /* GitHub Ribbon */
699 | #github-ribbon a {
700 | background: #000;
701 | color: #fff;
702 | text-decoration: none;
703 | font-family: arial, sans-serif;
704 | text-align: center;
705 | font-weight: bold;
706 | padding: 5px 40%;
707 | font-size: 1.2rem;
708 | line-height: 2rem;
709 | position: relative;
710 | transition: 0.5s;
711 | width: 100%;
712 | margin: 0 auto;
713 | white-space: nowrap;
714 | z-index: 99999;
715 | }
716 | #github-ribbon a:hover {
717 | background: #600;
718 | color: #fff;
719 | }
720 | #github-ribbon a::before, #github-ribbon a::after {
721 | content: "";
722 | width: 100%;
723 | display: block;
724 | position: absolute;
725 | top: 1px;
726 | left: 0;
727 | height: 1px;
728 | background: #fff;
729 | }
730 | #github-ribbon a::after {
731 | bottom: 1px;
732 | top: auto;
733 | }
734 |
735 | /* Collapse navbar */
736 | @media (max-width: 993px) {
737 | .navbar-header {
738 | float: none;
739 | min-height: 80px;
740 | }
741 | .navbar-left,.navbar-right {
742 | float: none !important;
743 | }
744 | .navbar-toggle {
745 | display: block;
746 | background-color: #2b3990;
747 | margin-top: 22px;
748 | margin-right: 100px;
749 | }
750 | .navbar-collapse {
751 | border-top: 1px solid transparent;
752 | box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
753 | }
754 | .navbar-fixed-top {
755 | top: 0;
756 | border-width: 0 0 1px;
757 | }
758 | .navbar-collapse.collapse {
759 | display: none!important;
760 | }
761 | .navbar-nav {
762 | float: none!important;
763 | }
764 | .navbar-nav>li {
765 | float: none;
766 | }
767 | .navbar-nav>li>a {
768 | padding-top: 10px;
769 | padding-bottom: 10px;
770 | }
771 | .collapse.in{
772 | display:block !important;
773 | }
774 | }
775 |
776 | @media (max-width: 600px) {
777 | .navbar-toggle {
778 | margin-right: 20px;
779 | }
780 | .navbar-brand img {
781 | width: 180px;
782 | height: 36px;
783 | margin-top: 10px
784 | }
785 | footer .container .links {
786 | background:url('../img/software-carpentry-banner-white.png') no-repeat;
787 | background-size: 180px;
788 | background-position: 0;
789 | }
790 | }
791 |
792 | /* GitHub ribbon breaking point */
793 | @media screen and (min-width: 600px) {
794 | #github-ribbon {
795 | position: absolute;
796 | display: block;
797 | top: 0;
798 | right: 0;
799 | width: 150px;
800 | overflow: hidden;
801 | height: 150px;
802 | }
803 | #github-ribbon a {
804 | width: 200px;
805 | position: absolute;
806 | padding: 5px 40px;
807 | top: 40px;
808 | right: -40px;
809 | transform: rotate(45deg);
810 | -webkit-transform: rotate(45deg);
811 | box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.8);
812 | }
813 | }
814 | @media (max-width: 599px) {
815 | .header h1 {
816 | font-size: 20pt;
817 | }
818 | #header-text {
819 | font-size: 16pt;
820 | }
821 | #github-ribbon {
822 | width: 100%;
823 | }
824 | #github-ribbon a {
825 | display: block;
826 | padding: 0px 0px;
827 | margin: 0px 0px;
828 | }
829 | }
830 |
--------------------------------------------------------------------------------
/tools/test_check.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | """
4 | Unit and functional tests for markdown lesson template validator.
5 |
6 | Some of these tests require looking for example files, which exist only on
7 | the gh-pages branch. Some tests may therefore fail on branch "core".
8 | """
9 |
10 | import logging
11 | import os
12 | import unittest
13 |
14 | import check
15 |
16 | # Make log messages visible to help audit test failures
17 | check.start_logging(level=logging.DEBUG)
18 |
19 | MARKDOWN_DIR = os.path.abspath(
20 | os.path.join(os.path.dirname(__file__), os.pardir))
21 |
22 |
23 | class BaseTemplateTest(unittest.TestCase):
24 | """Common methods for testing template validators"""
25 | SAMPLE_FILE = "" # Path to a file that should pass all tests
26 | VALIDATOR = check.MarkdownValidator
27 |
28 | def _create_validator(self, markdown):
29 | """Create validator object from markdown string; useful for failures"""
30 | return self.VALIDATOR(markdown=markdown)
31 |
32 |
33 | class TestAstHelpers(BaseTemplateTest):
34 | SAMPLE_FILE = os.path.join(MARKDOWN_DIR, 'index.md')
35 | VALIDATOR = check.MarkdownValidator
36 |
37 | def test_link_text_extracted(self):
38 | """Verify that link text and destination are extracted correctly"""
39 | validator = self._create_validator("""[This is a link](discussion.html)""")
40 | links = validator.ast.find_external_links(validator.ast.children[0])
41 |
42 | dest, link_text = validator.ast.get_link_info(links[0])
43 | self.assertEqual(dest, "discussion.html")
44 | self.assertEqual(link_text, "This is a link")
45 |
46 |
47 | class TestIndexPage(BaseTemplateTest):
48 | """Test the ability to correctly identify and validate specific sections
49 | of a markdown file"""
50 | SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "index.md")
51 | VALIDATOR = check.IndexPageValidator
52 |
53 | def test_sample_file_passes_validation(self):
54 | sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
55 | res = sample_validator.validate()
56 | self.assertTrue(res)
57 |
58 | def test_headers_missing_hrs(self):
59 | validator = self._create_validator("""Blank row
60 |
61 | layout: lesson
62 | title: Lesson Title
63 |
64 | Another section that isn't an HR
65 | """)
66 |
67 | self.assertFalse(validator._validate_doc_headers())
68 |
69 | def test_headers_missing_a_line(self):
70 | """One of the required headers is missing"""
71 | validator = self._create_validator("""---
72 | layout: lesson
73 | ---""")
74 | self.assertFalse(validator._validate_doc_headers())
75 |
76 | # TESTS INVOLVING DOCUMENT HEADER SECTION
77 | def test_headers_fail_with_other_content(self):
78 | validator = self._create_validator("""---
79 | layout: lesson
80 | title: Lesson Title
81 | otherline: Nothing
82 | ---""")
83 | self.assertFalse(validator._validate_doc_headers())
84 |
85 | def test_fail_when_headers_not_yaml_dict(self):
86 | """Fail when the headers can't be parsed to a dict of YAML data"""
87 | validator = self._create_validator("""---
88 | This will parse as a string, not a dictionary
89 | ---""")
90 | self.assertFalse(validator._validate_doc_headers())
91 |
92 | # TESTS INVOLVING SECTION TITLES/HEADINGS
93 | def test_index_has_valid_section_headings(self):
94 | """The provided index page"""
95 | validator = self._create_validator("""## Topics
96 |
97 | 1. [Topic Title One](01-one.html)
98 | 2. [Topic Title Two](02-two.html)
99 |
100 | ## Other Resources
101 |
102 | * [Reference Guide](reference.html)
103 | * [Next Steps](discussion.html)
104 | * [Instructor's Guide](instructors.html)""")
105 | res = validator._validate_section_heading_order()
106 | self.assertTrue(res)
107 |
108 | def test_index_fail_when_section_heading_absent(self):
109 | validator = self._create_validator("""## Topics
110 |
111 | 1. [Topic Title One](01-one.html)
112 | 2. [Topic Title Two](02-two.html)
113 |
114 | ## Other Resources
115 |
116 | * [Reference Guide](reference.html)
117 | * [Next Steps](discussion.html)
118 | * [Instructor's Guide](instructors.html)""")
119 | res = validator.ast.has_section_heading("Fake heading")
120 | self.assertFalse(res)
121 |
122 | def test_fail_when_section_heading_is_wrong_level(self):
123 | """All headings must be exactly level 2"""
124 | validator = self._create_validator("""---
125 | layout: page
126 | title: Lesson Title
127 | ---
128 | Paragraph of introductory material.
129 |
130 | > ## Prerequisites
131 | >
132 | > A short paragraph describing what learners need to know
133 | > before tackling this lesson.
134 |
135 | ### Topics
136 |
137 | 1. [Topic Title 1](01-one.html)
138 | 2. [Topic Title 2](02-two.html)
139 |
140 | ## Other Resources
141 |
142 | * [Reference Guide](reference.html)
143 | * [Next Steps](discussion.html)
144 | * [Instructor's Guide](instructors.html)""")
145 | self.assertFalse(validator._validate_section_heading_order())
146 |
147 | def test_fail_when_section_headings_in_wrong_order(self):
148 | validator = self._create_validator("""---
149 | layout: lesson
150 | title: Lesson Title
151 | ---
152 | Paragraph of introductory material.
153 |
154 | > ## Prerequisites
155 | >
156 | > A short paragraph describing what learners need to know
157 | > before tackling this lesson.
158 |
159 | ## Other Resources
160 |
161 | * [Reference Guide](reference.html)
162 | * [Instructor's Guide](instructors.html)
163 |
164 |
165 | ## Topics
166 |
167 | * [Topic Title 1](01-one.html)
168 | * [Topic Title 2](02-two.html)""")
169 |
170 | self.assertFalse(validator._validate_section_heading_order())
171 |
172 | def test_pass_when_prereq_section_has_correct_heading_level(self):
173 | validator = self._create_validator("""---
174 | layout: lesson
175 | title: Lesson Title
176 | ---
177 | Paragraph of introductory material.
178 |
179 | > ## Prerequisites
180 | >
181 | > A short paragraph describing what learners need to know
182 | > before tackling this lesson.
183 | """)
184 | self.assertTrue(validator._validate_intro_section())
185 |
186 | def test_fail_when_prereq_section_has_incorrect_heading_level(self):
187 | validator = self._create_validator("""
188 | > # Prerequisites {.prereq}
189 | >
190 | > A short paragraph describing what learners need to know
191 | > before tackling this lesson.
192 | """)
193 | self.assertFalse(validator._validate_callouts())
194 |
195 | # TESTS INVOLVING LINKS TO OTHER CONTENT
196 | def test_should_check_text_of_all_links_in_index(self):
197 | """Text of every local-html link in index.md should
198 | match dest page title"""
199 | validator = self._create_validator("""
200 | ## [This link is in a heading](reference.html)
201 | [Topic Title One](01-one.html#anchor)""")
202 | links = validator.ast.find_external_links()
203 | check_text, dont_check_text = validator._partition_links()
204 |
205 | self.assertEqual(len(dont_check_text), 0)
206 | self.assertEqual(len(check_text), 2)
207 |
208 | def test_file_links_validate(self):
209 | """Verify that all links in a sample file validate.
210 | Involves checking for example files; may fail on "core" branch"""
211 | sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
212 | res = sample_validator._validate_links()
213 | self.assertTrue(res)
214 |
215 | def test_html_link_to_extant_md_file_passes(self):
216 | """Verify that an HTML link with corresponding MD file will pass
217 | Involves checking for example files; may fail on "core" branch"""
218 | validator = self._create_validator("""[Topic Title One](01-one.html)""")
219 | self.assertTrue(validator._validate_links())
220 |
221 | def test_html_link_with_anchor_to_extant_md_passes(self):
222 | """Verify that link is identified correctly even if to page anchor
223 |
224 | For now this just tests that the regex handles #anchors.
225 | It doesn't validate that the named anchor exists in the md file
226 |
227 | Involves checking for example files; may fail on "core" branch
228 | """
229 | validator = self._create_validator("""[Topic Title One](01-one.html#anchor)""")
230 | self.assertTrue(validator._validate_links())
231 |
232 | def test_inpage_anchor_passes_validation(self):
233 | """Links that reference anchors within the page should be ignored"""
234 | # TODO: Revisit once anchor rules are available
235 | validator = self._create_validator("""Most databases also support Booleans and date/time values;
236 | SQLite uses the integers 0 and 1 for the former, and represents the latter as discussed [earlier](#a:dates).""")
237 | self.assertTrue(validator._validate_links())
238 |
239 | def test_missing_markdown_file_fails_validation(self):
240 | """Fail validation when an html file is linked without corresponding
241 | markdown file"""
242 | validator = self._create_validator("""[Broken link](nonexistent.html)""")
243 | self.assertFalse(validator._validate_links())
244 |
245 | def test_website_link_ignored_by_validator(self):
246 | """Don't look for markdown if the file linked isn't local-
247 | remote website links are ignored"""
248 | validator = self._create_validator("""[Broken link](http://website.com/filename.html)""")
249 | self.assertTrue(validator._validate_links())
250 |
251 | def test_malformed_website_link_fails_validator(self):
252 | """If the link isn't prefixed by http(s):// or ftp://, fail.
253 | This is because there are a lot of edge cases in distinguishing
254 | between filenames and URLs: err on the side of certainty."""
255 | validator = self._create_validator("""[Broken link](www.website.com/filename.html)""")
256 | self.assertFalse(validator._validate_links())
257 |
258 | def test_finds_image_asset(self):
259 | """Image asset is found in the expected file location
260 | Involves checking for example files; may fail on "core" branch"""
261 | validator = self._create_validator(
262 | """""")
263 | self.assertTrue(validator._validate_links())
264 |
265 | def test_image_asset_not_found(self):
266 | """Image asset can't be found if path is invalid"""
267 | validator = self._create_validator(
268 | """""")
269 | self.assertFalse(validator._validate_links())
270 |
271 | def test_non_html_link_finds_csv(self):
272 | """Look for CSV file in appropriate folder
273 | Involves checking for example files; may fail on "core" branch
274 | """
275 | validator = self._create_validator(
276 | """Use [this CSV](data/data.csv) for the exercise.""")
277 | self.assertTrue(validator._validate_links())
278 |
279 | def test_non_html_links_are_path_sensitive(self):
280 | """Fails to find CSV file with wrong path."""
281 | validator = self._create_validator(
282 | """Use [this CSV](data.csv) for the exercise.""")
283 | self.assertFalse(validator._validate_links())
284 |
285 | ### Tests involving callout/blockquote sections
286 | def test_one_prereq_callout_passes(self):
287 | """index.md should have one, and only one, prerequisites box"""
288 | validator = self._create_validator("""> ## Prerequisites {.prereq}
289 | >
290 | > What learners need to know before tackling this lesson.
291 | """)
292 | self.assertTrue(validator._validate_callouts())
293 |
294 | def test_two_prereq_callouts_fail(self):
295 | """More than one prereq callout box is not allowed"""
296 | validator = self._create_validator("""> ## Prerequisites {.prereq}
297 | >
298 | > What learners need to know before tackling this lesson.
299 |
300 | A spacer paragraph
301 |
302 | > ## Prerequisites {.prereq}
303 | >
304 | > A second prerequisites box should cause an error
305 | """)
306 | self.assertFalse(validator._validate_callouts())
307 |
308 | def test_callout_without_style_fails(self):
309 | """A callout box will fail if it is missing the required style"""
310 | validator = self._create_validator("""> ## Prerequisites
311 | >
312 | > What learners need to know before tackling this lesson.
313 | """)
314 | self.assertFalse(validator._validate_callouts())
315 |
316 | def test_callout_with_wrong_title_fails(self):
317 | """A callout box will fail if it has the wrong title"""
318 | validator = self._create_validator("""> ## Wrong title {.prereq}
319 | >
320 | > What learners need to know before tackling this lesson.
321 | """)
322 | self.assertFalse(validator._validate_callouts())
323 |
324 | def test_unknown_callout_style_fails(self):
325 | """A callout whose style is unrecognized by template is invalid"""
326 | validator = self._create_validator("""> ## Any title {.callout}
327 | >
328 | > What learners need to know before tackling this lesson.
329 | """)
330 | callout_node = validator.ast.get_callouts()[0]
331 | self.assertFalse(validator._validate_one_callout(callout_node))
332 |
333 | def test_block_ignored_sans_heading(self):
334 | """
335 | Blockquotes only count as callouts if they have a heading
336 | """
337 | validator = self._create_validator("""> Prerequisites {.prereq}
338 | >
339 | > What learners need to know before tackling this lesson.
340 | """)
341 | callout_nodes = validator.ast.get_callouts()
342 | self.assertEqual(len(callout_nodes), 0)
343 |
344 | def test_callout_heading_must_be_l2(self):
345 | """Callouts will fail validation if the heading is not level 2"""
346 | validator = self._create_validator("""> ### Prerequisites {.prereq}
347 | >
348 | > What learners need to know before tackling this lesson.
349 | """)
350 | self.assertFalse(validator._validate_callouts())
351 |
352 | def test_fail_if_fixme_present_all_caps(self):
353 | """Validation should fail if a line contains the word FIXME (exact)"""
354 | validator = self._create_validator("""Incomplete sentence (FIXME).""")
355 | self.assertFalse(validator._validate_no_fixme())
356 |
357 | def test_fail_if_fixme_present_mixed_case(self):
358 | """Validation should fail if a line contains the word FIXME
359 | (in any capitalization)"""
360 | validator = self._create_validator("""Incomplete sentence (FiXmE).""")
361 | self.assertFalse(validator._validate_no_fixme())
362 |
363 |
364 | class TestTopicPage(BaseTemplateTest):
365 | """Verifies that the topic page validator works as expected"""
366 | SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "01-one.md")
367 | VALIDATOR = check.TopicPageValidator
368 |
369 | def test_headers_fail_because_invalid_content(self):
370 | """The value provided as YAML does not match the expected datatype"""
371 | validator = self._create_validator("""---
372 | layout: lesson
373 | title: Lesson Title
374 | subtitle: A page
375 | minutes: not a number
376 | ---""")
377 | self.assertFalse(validator._validate_doc_headers())
378 |
379 | def test_topic_page_should_have_no_headings(self):
380 | """Requirement according to spec; may be relaxed in future"""
381 | validator = self._create_validator("""
382 | ## Heading that should not be present
383 |
384 | Some text""")
385 | self.assertFalse(validator._validate_has_no_headings())
386 |
387 | def test_should_not_check_text_of_links_in_topic(self):
388 | """Never check that text of local-html links in topic
389 | matches dest title """
390 | validator = self._create_validator("""
391 | ## [This link is in a heading](reference.html)
392 | [Topic Title One](01-one.html#anchor)""")
393 | links = validator.ast.find_external_links()
394 | check_text, dont_check_text = validator._partition_links()
395 |
396 | self.assertEqual(len(dont_check_text), 2)
397 | self.assertEqual(len(check_text), 0)
398 |
399 | def test_pass_when_optional_callouts_absent(self):
400 | """Optional block titles should be optional"""
401 | validator = self._create_validator("""> ## Learning Objectives {.objectives}
402 | >
403 | > * All topic pages must have this callout""")
404 | self.assertTrue(validator._validate_callouts())
405 |
406 |
407 | def test_callout_style_passes_regardless_of_title(self):
408 | """Verify that certain kinds of callout box can be recognized solely
409 | by style, regardless of the heading title"""
410 | validator = self._create_validator("""> ## Learning Objectives {.objectives}
411 | >
412 | > * All topic pages must have this callout
413 |
414 | > ## Some random title {.callout}
415 | >
416 | > Some informative text""")
417 |
418 | self.assertTrue(validator._validate_callouts())
419 |
420 | def test_callout_style_allows_duplicates(self):
421 | """Multiple blockquoted sections with style 'callout' are allowed"""
422 | validator = self._create_validator("""> ## Learning Objectives {.objectives}
423 | >
424 | > * All topic pages must have this callout
425 |
426 | > ## Callout box one {.callout}
427 | >
428 | > Some informative text
429 |
430 | Spacer paragraph
431 |
432 | > ## Callout box two {.callout}
433 | >
434 | > Further exposition""")
435 | self.assertTrue(validator._validate_callouts())
436 |
437 | def test_sample_file_passes_validation(self):
438 | sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
439 | res = sample_validator.validate()
440 | self.assertTrue(res)
441 |
442 |
443 | class TestReferencePage(BaseTemplateTest):
444 | """Verifies that the reference page validator works as expected"""
445 | SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "reference.md")
446 | VALIDATOR = check.ReferencePageValidator
447 |
448 | def test_missing_glossary_definition(self):
449 | validator = self._create_validator("")
450 | self.assertFalse(validator._validate_glossary_entry(
451 | ["Key word"]))
452 |
453 | def test_missing_colon_at_glossary_definition(self):
454 | validator = self._create_validator("")
455 | self.assertFalse(validator._validate_glossary_entry(
456 | ["Key word", "Definition of term"]))
457 |
458 | def test_wrong_indentation_at_glossary_definition(self):
459 | validator = self._create_validator("")
460 | self.assertFalse(validator._validate_glossary_entry(
461 | ["Key word", ": Definition of term"]))
462 |
463 | def test_wrong_continuation_at_glossary_definition(self):
464 | validator = self._create_validator("")
465 | self.assertFalse(validator._validate_glossary_entry(
466 | ["Key word", ": Definition of term", "continuation"]))
467 |
468 | def test_valid_glossary_definition(self):
469 | validator = self._create_validator("")
470 | self.assertTrue(validator._validate_glossary_entry(
471 | ["Key word", ": Definition of term", " continuation"]))
472 |
473 | def test_only_definitions_can_appear_after_glossary_heading(self):
474 | validator = self._create_validator("""## Glossary
475 |
476 | Key Word 1
477 | : Definition of first term
478 |
479 | Paragraph
480 |
481 | Key Word 2
482 | : Definition of second term
483 | """)
484 | self.assertFalse(validator._validate_glossary())
485 |
486 | def test_glossary(self):
487 | validator = self._create_validator("""## Glossary
488 |
489 | Key Word 1
490 | : Definition of first term
491 |
492 | Key Word 2
493 | : Definition of second term
494 | """)
495 | self.assertTrue(validator._validate_glossary())
496 |
497 | def test_callout_fails_when_none_specified(self):
498 | """The presence of a callout box should cause validation to fail
499 | when the template doesn't define any recognized callouts
500 |
501 | (No "unknown" blockquote sections are allowed)
502 | """
503 | validator = self._create_validator("""> ## Learning Objectives {.objectives}
504 | >
505 | > * Learning objective 1
506 | > * Learning objective 2""")
507 |
508 | self.assertFalse(validator._validate_callouts())
509 |
510 | def test_sample_file_passes_validation(self):
511 | sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
512 | res = sample_validator.validate()
513 | self.assertTrue(res)
514 |
515 |
516 | class TestInstructorPage(BaseTemplateTest):
517 | """Verifies that the instructors page validator works as expected"""
518 | SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "instructors.md")
519 | VALIDATOR = check.InstructorPageValidator
520 |
521 | def test_should_selectively_check_text_of_links_in_topic(self):
522 | """Only verify that text of local-html links in topic
523 | matches dest title if the link is in a heading"""
524 | validator = self._create_validator("""
525 | ## [Reference](reference.html)
526 |
527 | [Topic Title One](01-one.html#anchor)""")
528 | check_text, dont_check_text = validator._partition_links()
529 |
530 | self.assertEqual(len(dont_check_text), 1)
531 | self.assertEqual(len(check_text), 1)
532 |
533 | def test_link_dest_bad_while_text_ignored(self):
534 | validator = self._create_validator("""
535 | [ignored text](nonexistent.html)""")
536 | self.assertFalse(validator._validate_links())
537 |
538 | def test_link_dest_good_while_text_ignored(self):
539 | validator = self._create_validator("""
540 | [ignored text](01-one.html)""")
541 | self.assertTrue(validator._validate_links())
542 |
543 | def test_sample_file_passes_validation(self):
544 | sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
545 | res = sample_validator.validate()
546 | self.assertTrue(res)
547 |
548 |
549 | class TestLicensePage(BaseTemplateTest):
550 | SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "LICENSE.md")
551 | VALIDATOR = check.LicensePageValidator
552 |
553 | def test_sample_file_passes_validation(self):
554 | sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
555 | res = sample_validator.validate()
556 | self.assertTrue(res)
557 |
558 | def test_modified_file_fails_validation(self):
559 | with open(self.SAMPLE_FILE, 'rU') as f:
560 | orig_text = f.read()
561 | mod_text = orig_text.replace("The", "the")
562 | validator = self._create_validator(mod_text)
563 | self.assertFalse(validator.validate())
564 |
565 |
566 | class TestDiscussionPage(BaseTemplateTest):
567 | SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "discussion.md")
568 | VALIDATOR = check.DiscussionPageValidator
569 |
570 | def test_sample_file_passes_validation(self):
571 | sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
572 | res = sample_validator.validate()
573 | self.assertTrue(res)
574 |
575 |
576 | if __name__ == "__main__":
577 | unittest.main()
578 |
--------------------------------------------------------------------------------
/css/bootstrap/bootstrap-theme.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.3.4 (http://getbootstrap.com)
3 | * Copyright 2011-2015 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 |
7 | .btn-default,
8 | .btn-primary,
9 | .btn-success,
10 | .btn-info,
11 | .btn-warning,
12 | .btn-danger {
13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
16 | }
17 | .btn-default:active,
18 | .btn-primary:active,
19 | .btn-success:active,
20 | .btn-info:active,
21 | .btn-warning:active,
22 | .btn-danger:active,
23 | .btn-default.active,
24 | .btn-primary.active,
25 | .btn-success.active,
26 | .btn-info.active,
27 | .btn-warning.active,
28 | .btn-danger.active {
29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
31 | }
32 | .btn-default .badge,
33 | .btn-primary .badge,
34 | .btn-success .badge,
35 | .btn-info .badge,
36 | .btn-warning .badge,
37 | .btn-danger .badge {
38 | text-shadow: none;
39 | }
40 | .btn:active,
41 | .btn.active {
42 | background-image: none;
43 | }
44 | .btn-default {
45 | text-shadow: 0 1px 0 #fff;
46 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
47 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
48 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
49 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
50 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
51 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
52 | background-repeat: repeat-x;
53 | border-color: #dbdbdb;
54 | border-color: #ccc;
55 | }
56 | .btn-default:hover,
57 | .btn-default:focus {
58 | background-color: #e0e0e0;
59 | background-position: 0 -15px;
60 | }
61 | .btn-default:active,
62 | .btn-default.active {
63 | background-color: #e0e0e0;
64 | border-color: #dbdbdb;
65 | }
66 | .btn-default.disabled,
67 | .btn-default:disabled,
68 | .btn-default[disabled] {
69 | background-color: #e0e0e0;
70 | background-image: none;
71 | }
72 | .btn-primary {
73 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
74 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
75 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
76 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
79 | background-repeat: repeat-x;
80 | border-color: #245580;
81 | }
82 | .btn-primary:hover,
83 | .btn-primary:focus {
84 | background-color: #265a88;
85 | background-position: 0 -15px;
86 | }
87 | .btn-primary:active,
88 | .btn-primary.active {
89 | background-color: #265a88;
90 | border-color: #245580;
91 | }
92 | .btn-primary.disabled,
93 | .btn-primary:disabled,
94 | .btn-primary[disabled] {
95 | background-color: #265a88;
96 | background-image: none;
97 | }
98 | .btn-success {
99 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
100 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
101 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
102 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
103 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
104 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
105 | background-repeat: repeat-x;
106 | border-color: #3e8f3e;
107 | }
108 | .btn-success:hover,
109 | .btn-success:focus {
110 | background-color: #419641;
111 | background-position: 0 -15px;
112 | }
113 | .btn-success:active,
114 | .btn-success.active {
115 | background-color: #419641;
116 | border-color: #3e8f3e;
117 | }
118 | .btn-success.disabled,
119 | .btn-success:disabled,
120 | .btn-success[disabled] {
121 | background-color: #419641;
122 | background-image: none;
123 | }
124 | .btn-info {
125 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
126 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
127 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
128 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
129 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
130 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
131 | background-repeat: repeat-x;
132 | border-color: #28a4c9;
133 | }
134 | .btn-info:hover,
135 | .btn-info:focus {
136 | background-color: #2aabd2;
137 | background-position: 0 -15px;
138 | }
139 | .btn-info:active,
140 | .btn-info.active {
141 | background-color: #2aabd2;
142 | border-color: #28a4c9;
143 | }
144 | .btn-info.disabled,
145 | .btn-info:disabled,
146 | .btn-info[disabled] {
147 | background-color: #2aabd2;
148 | background-image: none;
149 | }
150 | .btn-warning {
151 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
152 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
153 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
154 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
155 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
156 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
157 | background-repeat: repeat-x;
158 | border-color: #e38d13;
159 | }
160 | .btn-warning:hover,
161 | .btn-warning:focus {
162 | background-color: #eb9316;
163 | background-position: 0 -15px;
164 | }
165 | .btn-warning:active,
166 | .btn-warning.active {
167 | background-color: #eb9316;
168 | border-color: #e38d13;
169 | }
170 | .btn-warning.disabled,
171 | .btn-warning:disabled,
172 | .btn-warning[disabled] {
173 | background-color: #eb9316;
174 | background-image: none;
175 | }
176 | .btn-danger {
177 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
178 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
179 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
180 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
182 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
183 | background-repeat: repeat-x;
184 | border-color: #b92c28;
185 | }
186 | .btn-danger:hover,
187 | .btn-danger:focus {
188 | background-color: #c12e2a;
189 | background-position: 0 -15px;
190 | }
191 | .btn-danger:active,
192 | .btn-danger.active {
193 | background-color: #c12e2a;
194 | border-color: #b92c28;
195 | }
196 | .btn-danger.disabled,
197 | .btn-danger:disabled,
198 | .btn-danger[disabled] {
199 | background-color: #c12e2a;
200 | background-image: none;
201 | }
202 | .thumbnail,
203 | .img-thumbnail {
204 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
205 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
206 | }
207 | .dropdown-menu > li > a:hover,
208 | .dropdown-menu > li > a:focus {
209 | background-color: #e8e8e8;
210 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
211 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
212 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
213 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
214 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
215 | background-repeat: repeat-x;
216 | }
217 | .dropdown-menu > .active > a,
218 | .dropdown-menu > .active > a:hover,
219 | .dropdown-menu > .active > a:focus {
220 | background-color: #2e6da4;
221 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
222 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
223 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
224 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
225 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
226 | background-repeat: repeat-x;
227 | }
228 | .navbar-default {
229 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
230 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
231 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
232 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
233 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
234 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
235 | background-repeat: repeat-x;
236 | border-radius: 4px;
237 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
238 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
239 | }
240 | .navbar-default .navbar-nav > .open > a,
241 | .navbar-default .navbar-nav > .active > a {
242 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
243 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
244 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
245 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
246 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
247 | background-repeat: repeat-x;
248 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
249 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
250 | }
251 | .navbar-brand,
252 | .navbar-nav > li > a {
253 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
254 | }
255 | .navbar-inverse {
256 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
257 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
258 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
259 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
260 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
261 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
262 | background-repeat: repeat-x;
263 | }
264 | .navbar-inverse .navbar-nav > .open > a,
265 | .navbar-inverse .navbar-nav > .active > a {
266 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
267 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
268 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
269 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
270 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
271 | background-repeat: repeat-x;
272 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
273 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
274 | }
275 | .navbar-inverse .navbar-brand,
276 | .navbar-inverse .navbar-nav > li > a {
277 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
278 | }
279 | .navbar-static-top,
280 | .navbar-fixed-top,
281 | .navbar-fixed-bottom {
282 | border-radius: 0;
283 | }
284 | @media (max-width: 767px) {
285 | .navbar .navbar-nav .open .dropdown-menu > .active > a,
286 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
287 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
288 | color: #fff;
289 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
290 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
291 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
292 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
293 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
294 | background-repeat: repeat-x;
295 | }
296 | }
297 | .alert {
298 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
299 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
300 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
301 | }
302 | .alert-success {
303 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
304 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
305 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
306 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
307 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
308 | background-repeat: repeat-x;
309 | border-color: #b2dba1;
310 | }
311 | .alert-info {
312 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
313 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
314 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
315 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
316 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
317 | background-repeat: repeat-x;
318 | border-color: #9acfea;
319 | }
320 | .alert-warning {
321 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
322 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
323 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
324 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
325 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
326 | background-repeat: repeat-x;
327 | border-color: #f5e79e;
328 | }
329 | .alert-danger {
330 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
331 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
332 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
333 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
334 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
335 | background-repeat: repeat-x;
336 | border-color: #dca7a7;
337 | }
338 | .progress {
339 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
340 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
342 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
344 | background-repeat: repeat-x;
345 | }
346 | .progress-bar {
347 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
348 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);
349 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
350 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);
351 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
352 | background-repeat: repeat-x;
353 | }
354 | .progress-bar-success {
355 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
356 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
357 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
358 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
359 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
360 | background-repeat: repeat-x;
361 | }
362 | .progress-bar-info {
363 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
364 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
365 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
366 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
367 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
368 | background-repeat: repeat-x;
369 | }
370 | .progress-bar-warning {
371 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
372 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
373 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
374 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
375 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
376 | background-repeat: repeat-x;
377 | }
378 | .progress-bar-danger {
379 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
380 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
381 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
382 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
383 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
384 | background-repeat: repeat-x;
385 | }
386 | .progress-bar-striped {
387 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
388 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
389 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
390 | }
391 | .list-group {
392 | border-radius: 4px;
393 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
394 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
395 | }
396 | .list-group-item.active,
397 | .list-group-item.active:hover,
398 | .list-group-item.active:focus {
399 | text-shadow: 0 -1px 0 #286090;
400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
405 | background-repeat: repeat-x;
406 | border-color: #2b669a;
407 | }
408 | .list-group-item.active .badge,
409 | .list-group-item.active:hover .badge,
410 | .list-group-item.active:focus .badge {
411 | text-shadow: none;
412 | }
413 | .panel {
414 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
415 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
416 | }
417 | .panel-default > .panel-heading {
418 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
419 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
420 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
421 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
422 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
423 | background-repeat: repeat-x;
424 | }
425 | .panel-primary > .panel-heading {
426 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
427 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
428 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
429 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
430 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
431 | background-repeat: repeat-x;
432 | }
433 | .panel-success > .panel-heading {
434 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
435 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
436 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
437 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
438 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
439 | background-repeat: repeat-x;
440 | }
441 | .panel-info > .panel-heading {
442 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
443 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
444 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
445 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
446 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
447 | background-repeat: repeat-x;
448 | }
449 | .panel-warning > .panel-heading {
450 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
451 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
453 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
455 | background-repeat: repeat-x;
456 | }
457 | .panel-danger > .panel-heading {
458 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
459 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
461 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
463 | background-repeat: repeat-x;
464 | }
465 | .well {
466 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
467 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
469 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
471 | background-repeat: repeat-x;
472 | border-color: #dcdcdc;
473 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
474 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
475 | }
476 | /*# sourceMappingURL=bootstrap-theme.css.map */
477 |
--------------------------------------------------------------------------------
/tools/check.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | """
4 | Validate Software Carpentry lessons
5 | according to the Markdown template specification described here:
6 | http://software-carpentry.org/blog/2014/10/new-lesson-template-v2.html
7 |
8 | Validates the presence of headings, as well as specific sub-nodes.
9 | Contains validators for several kinds of template.
10 |
11 | Call at command line with flag -h to see options and usage instructions.
12 | """
13 |
14 | import argparse
15 | import collections
16 | import functools
17 | import glob
18 | import hashlib
19 | import logging
20 | import os
21 | import re
22 | import sys
23 |
24 | import CommonMark
25 | import yaml
26 |
27 | import validation_helpers as vh
28 |
29 | NUMBER_OF_ERRORS = 0
30 |
31 | def incr_error(func):
32 | """Wrapper to count the number of errors"""
33 | @functools.wraps(func)
34 | def wrapper(*args, **kwargs):
35 | global NUMBER_OF_ERRORS
36 | NUMBER_OF_ERRORS += 1
37 | return func(*args, **kwargs)
38 | return wrapper
39 |
40 | logging.error = incr_error(logging.error)
41 |
42 |
43 | class MarkdownValidator(object):
44 | """Base class for Markdown validation
45 |
46 | Contains basic validation skeleton to be extended for specific page types
47 | """
48 | HEADINGS = [] # List of strings containing expected heading text
49 |
50 | # Callout boxes (blockquote items) have special rules.
51 | # Dict of tuples for each callout type: {style: (title, min, max)}
52 | CALLOUTS = {}
53 |
54 | WARN_ON_EXTRA_HEADINGS = False # Warn when other headings are present?
55 |
56 | # Validate YAML doc headers: dict of {header text: validation_func}
57 | DOC_HEADERS = {}
58 |
59 | def __init__(self, filename=None, markdown=None):
60 | """Perform validation on a Markdown document.
61 |
62 | Validator accepts either the path to a file containing Markdown,
63 | OR a valid Markdown string. The latter is useful for unit testing."""
64 | self.filename = filename
65 |
66 | if filename:
67 | # Expect Markdown files to be in same directory as the input file
68 | self.markdown_dir = os.path.dirname(filename)
69 | self.lesson_dir = self.markdown_dir
70 | with open(filename, 'rU') as f:
71 | self.markdown = f.read()
72 | else:
73 | # Look for linked content in ../pages (relative to this file)
74 | self.lesson_dir = os.path.abspath(
75 | os.path.join(os.path.dirname(__file__), os.pardir))
76 |
77 | self.markdown_dir = self.lesson_dir
78 | self.markdown = markdown
79 |
80 | ast = self._parse_markdown(self.markdown)
81 | self.ast = vh.CommonMarkHelper(ast)
82 |
83 | # Keep track of how many times callout box styles are used
84 | self._callout_counts = collections.Counter()
85 |
86 | def _parse_markdown(self, markdown):
87 | parser = CommonMark.DocParser()
88 | ast = parser.parse(markdown)
89 | return ast
90 |
91 | def _validate_no_fixme(self):
92 | """Validate that the file contains no lines marked 'FIXME'
93 | This will be based on the raw markdown, not the ast"""
94 | valid = True
95 | for i, line in enumerate(self.markdown.splitlines()):
96 | if re.search("FIXME", line, re.IGNORECASE):
97 | logging.error(
98 | "In {0}: "
99 | "Line {1} contains FIXME, indicating "
100 | "work in progress".format(self.filename, i+1))
101 | valid = False
102 | return valid
103 |
104 | def _validate_hrs(self):
105 | """Validate header
106 |
107 | Verify that the header section at top of document
108 | is bracketed by two horizontal rules"""
109 | valid = True
110 | try:
111 | hr_nodes = [self.ast.children[0], self.ast.children[2]]
112 | except IndexError:
113 | logging.error(
114 | "In {0}: "
115 | "Document must include header sections".format(self.filename))
116 | return False
117 |
118 | for hr in hr_nodes:
119 | if not self.ast.is_hr(hr):
120 | logging.error(
121 | "In {0}: "
122 | "Expected --- at line: {1}".format(
123 | self.filename, hr.start_line))
124 | valid = False
125 | return valid
126 |
127 | def _validate_one_doc_header_row(self, label, content):
128 | """Validate a single row of the document header section"""
129 | if label not in self.DOC_HEADERS:
130 | logging.warning(
131 | "In {0}: "
132 | "Unrecognized label in header section: {1}".format(
133 | self.filename, label))
134 | return False
135 |
136 | validation_function = self.DOC_HEADERS[label]
137 | validate_header = validation_function(content)
138 | if not validate_header:
139 | logging.error(
140 | "In {0}: "
141 | "Contents of document header field for label {1} "
142 | "do not follow expected format".format(self.filename, label))
143 | return validate_header
144 |
145 | # Methods related to specific validation. Can override specific tests.
146 | def _validate_doc_headers(self):
147 | """Validate the document header section.
148 |
149 | Pass only if the header of the document contains the specified
150 | sections with the expected contents"""
151 |
152 | # Test: Header section should be wrapped in hrs
153 | has_hrs = self._validate_hrs()
154 |
155 | header_node = self.ast.children[1]
156 | header_text = '\n'.join(header_node.strings)
157 |
158 | # Parse headers as YAML. Don't check if parser returns None or str.
159 | header_yaml = yaml.load(header_text)
160 | if not isinstance(header_yaml, dict):
161 | logging.error("In {0}: "
162 | "Expected YAML markup with labels "
163 | "{1}".format(self.filename, self.DOC_HEADERS.keys()))
164 | return False
165 |
166 | # Test: Labeled YAML should match expected format
167 | test_headers = [self._validate_one_doc_header_row(k, v)
168 | for k, v in header_yaml.items()]
169 |
170 | # Test: Must have all expected header lines, and no others.
171 | only_headers = (len(header_yaml) == len(self.DOC_HEADERS))
172 |
173 | # If expected headings are missing, print an informative message
174 | missing_headings = [h for h in self.DOC_HEADERS
175 | if h not in header_yaml]
176 |
177 | for h in missing_headings:
178 | logging.error("In {0}: "
179 | "Header section is missing expected "
180 | "row '{1}'".format(self.filename, h))
181 |
182 | return has_hrs and all(test_headers) and only_headers
183 |
184 | def _validate_section_heading_order(self, ast_node=None, headings=None):
185 | """Verify that section headings appear, and in the order expected"""
186 | if ast_node is None:
187 | ast_node = self.ast.data
188 | headings = self.HEADINGS
189 |
190 | heading_nodes = self.ast.get_section_headings(ast_node)
191 | # All headings should be exactly level 2
192 | correct_level = True
193 | for n in heading_nodes:
194 | if n.level != 2:
195 | logging.error(
196 | "In {0}: "
197 | "Heading at line {1} should be level 2".format(
198 | self.filename, n.start_line))
199 | correct_level = False
200 |
201 | heading_labels = [vh.strip_attrs(n.strings[0]) for n in heading_nodes]
202 |
203 | # Check for missing and extra headings
204 | missing_headings = [expected_heading for expected_heading in headings
205 | if expected_heading not in heading_labels]
206 |
207 | extra_headings = [found_heading for found_heading in heading_labels
208 | if found_heading not in headings]
209 |
210 | for h in missing_headings:
211 | logging.error(
212 | "In {0}: "
213 | "Document is missing expected heading: {1}".format(
214 | self.filename, h))
215 |
216 | if self.WARN_ON_EXTRA_HEADINGS is True:
217 | for h in extra_headings:
218 | logging.error(
219 | "In {0}: "
220 | "Document contains heading "
221 | "not specified in the template: {1}".format(
222 | self.filename, h))
223 | no_extra = (len(extra_headings) == 0)
224 | else:
225 | no_extra = True
226 |
227 | # Check that the subset of headings
228 | # in the template spec matches order in the document
229 | valid_order = True
230 | headings_overlap = [h for h in heading_labels if h in headings]
231 | if len(missing_headings) == 0 and headings_overlap != headings:
232 | valid_order = False
233 | logging.error(
234 | "In {0}: "
235 | "Document headings do not match "
236 | "the order specified by the template".format(self.filename))
237 |
238 | return (len(missing_headings) == 0) and \
239 | valid_order and no_extra and correct_level
240 |
241 | def _validate_one_callout(self, callout_node):
242 | """
243 | Logic to validate a single callout box (defined as a blockquoted
244 | section that starts with a heading). Checks that:
245 |
246 | * First child of callout box should be a lvl 2 header with
247 | known title & CSS style
248 | * Callout box must have at least one child after the heading
249 |
250 | An additional test is done in another function:
251 | * Checks # times callout style appears in document, minc <= n <= maxc
252 | """
253 | heading_node = callout_node.children[0]
254 | valid_head_lvl = self.ast.is_heading(heading_node, heading_level=2)
255 | title, styles = self.ast.get_heading_info(heading_node)
256 |
257 | if not valid_head_lvl:
258 | logging.error("In {0}: "
259 | "Callout box titled '{1}' must start with a "
260 | "level 2 heading".format(self.filename, title))
261 |
262 | try:
263 | style = styles[0]
264 | except IndexError:
265 | logging.error(
266 | "In {0}: "
267 | "Callout section titled '{1}' must specify "
268 | "a CSS style".format(self.filename, title))
269 | return False
270 |
271 | # Track # times this style is used in any callout
272 | self._callout_counts[style] += 1
273 |
274 | # Verify style actually in callout spec
275 | if style not in self.CALLOUTS:
276 | spec_title = None
277 | valid_style = False
278 | else:
279 | spec_title, _, _ = self.CALLOUTS[style]
280 | valid_style = True
281 |
282 | has_children = self.ast.has_number_children(callout_node, minc=2)
283 | if spec_title is not None and title != spec_title:
284 | # Callout box must have specified heading title
285 | logging.error(
286 | "In {0}: "
287 | "Callout section with style '{1}' should have "
288 | "title '{2}'".format(self.filename, style, spec_title))
289 | valid_title = False
290 | else:
291 | valid_title = True
292 |
293 | res = (valid_style and valid_title and has_children and valid_head_lvl)
294 | return res
295 |
296 | def _validate_callouts(self):
297 | """
298 | Validate all sections that appear as callouts
299 |
300 | The style is a better determinant of callout than the title
301 | """
302 | callout_nodes = self.ast.get_callouts()
303 | callouts_valid = True
304 |
305 | # Validate all the callout nodes present
306 | for n in callout_nodes:
307 | res = self._validate_one_callout(n)
308 | callouts_valid = callouts_valid and res
309 |
310 | found_styles = self._callout_counts
311 |
312 | # Issue error if style is not present correct # times
313 | missing_styles = [style
314 | for style, (title, minc, maxc) in self.CALLOUTS.items()
315 | if not ((minc or 0) <= found_styles[style]
316 | <= (maxc or sys.maxsize))]
317 | unknown_styles = [k for k in found_styles if k not in self.CALLOUTS]
318 |
319 | for style in unknown_styles:
320 | logging.error("In {0}: "
321 | "Found callout box with unrecognized "
322 | "style '{1}'".format(self.filename, style))
323 |
324 | for style in missing_styles:
325 | minc = self.CALLOUTS[style][1]
326 | maxc = self.CALLOUTS[style][2]
327 | logging.error("In {0}: "
328 | "Expected between min {1} and max {2} callout boxes "
329 | "with style '{3}'".format(
330 | self.filename, minc, maxc, style))
331 |
332 | return (callouts_valid and
333 | len(missing_styles) == 0 and len(unknown_styles) == 0)
334 |
335 | # Link validation methods
336 | def _validate_one_html_link(self, link_node, check_text=False):
337 | """
338 | Any local html file being linked was generated as part of the lesson.
339 | Therefore, file links (.html) must have a Markdown file
340 | in the expected folder.
341 |
342 | The title of the linked Markdown document should match the link text.
343 | """
344 | dest, link_text = self.ast.get_link_info(link_node)
345 |
346 | # HTML files in same folder are made from Markdown; special tests
347 | fn = dest.split("#")[0] # Split anchor name from filename
348 | expected_md_fn = os.path.splitext(fn)[0] + os.extsep + "md"
349 | expected_md_path = os.path.join(self.markdown_dir,
350 | expected_md_fn)
351 | if not os.path.isfile(expected_md_path):
352 | logging.error(
353 | "In {0}: "
354 | "The document links to {1}, but could not find "
355 | "the expected markdown file {2}".format(
356 | self.filename, fn, expected_md_path))
357 | return False
358 |
359 | if check_text is True:
360 | # If file exists, parse and validate link text = node title
361 | with open(expected_md_path, 'rU') as link_dest_file:
362 | dest_contents = link_dest_file.read()
363 |
364 | dest_ast = self._parse_markdown(dest_contents)
365 | dest_ast = vh.CommonMarkHelper(dest_ast)
366 | dest_page_title = dest_ast.get_doc_header_subtitle()
367 |
368 | if dest_page_title != link_text:
369 | logging.error(
370 | "In {0}: "
371 | "The linked page {1} exists, but "
372 | "the link text '{2}' does not match the "
373 | "(sub)title of that page, '{3}'.".format(
374 | self.filename, dest,
375 | link_text, dest_page_title))
376 | return False
377 | return True
378 |
379 | def _validate_one_link(self, link_node, check_text=False):
380 | """Logic to validate a single link to a file asset
381 |
382 | Performs special checks for links to a local markdown file.
383 |
384 | For links or images, just verify that a file exists.
385 | """
386 | dest, link_text = self.ast.get_link_info(link_node)
387 |
388 | if re.match(r"^[\w,\s-]+\.(html?)", dest, re.IGNORECASE):
389 | # Validate local html links have matching md file
390 | return self._validate_one_html_link(link_node,
391 | check_text=check_text)
392 | elif not re.match(r"^((https?|ftp)://.+)|mailto:",
393 | dest, re.IGNORECASE)\
394 | and not re.match(r"^#.*", dest):
395 | # If not web or email URL, and not anchor on same page, then
396 | # verify that local file exists
397 | dest_path = os.path.join(self.lesson_dir, dest)
398 | dest_path = dest_path.split("#")[0] # Split anchor from filename
399 | if not os.path.isfile(dest_path):
400 | fn = dest.split("#")[0] # Split anchor name from filename
401 | logging.error(
402 | "In {0}: "
403 | "Could not find the linked asset file "
404 | "{1} in {2}. If this is a URL, it must be "
405 | "prefixed with http(s):// or ftp://.".format(
406 | self.filename, fn, dest_path))
407 | return False
408 | else:
409 | logging.debug(
410 | "In {0}: "
411 | "Skipped validation of link {1}".format(self.filename, dest))
412 | return True
413 |
414 | def _partition_links(self):
415 | """Fetch links in document. If this template has special requirements
416 | for link text (eg only some links' text should match dest page title),
417 | filter the list accordingly.
418 |
419 | Default behavior: don't check the text of any links"""
420 | check_text = []
421 | no_check_text = self.ast.find_external_links()
422 |
423 | return check_text, no_check_text
424 |
425 | def _validate_links(self):
426 | """Validate all references to external content
427 |
428 | This includes links AND images: these are the two types of node that
429 | CommonMark assigns a .destination property"""
430 | check_text, no_check_text = self._partition_links()
431 |
432 | valid = True
433 | for link_node in check_text:
434 | res = self._validate_one_link(link_node, check_text=True)
435 | valid = valid and res
436 |
437 | for link_node in no_check_text:
438 | res = self._validate_one_link(link_node, check_text=False)
439 | valid = valid and res
440 | return valid
441 |
442 | def _run_tests(self):
443 | """
444 | Let user override the list of tests to be performed.
445 |
446 | Error trapping is handled by the validate() wrapper method.
447 | """
448 | tests = [self._validate_no_fixme(),
449 | self._validate_doc_headers(),
450 | self._validate_section_heading_order(),
451 | self._validate_callouts(),
452 | self._validate_links()]
453 |
454 | return all(tests)
455 |
456 | def validate(self):
457 | """Perform all required validations. Wrap in exception handler"""
458 | try:
459 | return self._run_tests()
460 | except IndexError:
461 | logging.error("Document is missing critical sections")
462 | return False
463 |
464 |
465 | class IndexPageValidator(MarkdownValidator):
466 | """Validate the contents of the homepage (index.md)"""
467 | HEADINGS = ['Topics',
468 | 'Other Resources']
469 |
470 | DOC_HEADERS = {'layout': vh.is_str,
471 | 'title': vh.is_str}
472 |
473 | CALLOUTS = {'prereq': ("Prerequisites", 1, 1),
474 | 'getready': ("Getting ready", 1, 1)}
475 |
476 | def _partition_links(self):
477 | """Check the text of every link in index.md"""
478 | external_links = self.ast.find_external_links()
479 |
480 | check_text = []
481 | no_check_text = []
482 |
483 | for link in external_links:
484 | if '#' in link.destination:
485 | no_check_text.append(link)
486 | else:
487 | check_text.append(link)
488 |
489 | return check_text, no_check_text
490 |
491 | def _validate_intro_section(self):
492 | """Validate the intro section
493 |
494 | It must be a paragraph, followed by blockquoted list of prereqs"""
495 | intro_block = self.ast.children[3]
496 | intro_section = self.ast.is_paragraph(intro_block)
497 | if not intro_section:
498 | logging.error(
499 | "In {0}: "
500 | "Expected paragraph of introductory text at {1}".format(
501 | self.filename, intro_block.start_line))
502 |
503 | return intro_section
504 |
505 | def _run_tests(self):
506 | parent_tests = super(IndexPageValidator, self)._run_tests()
507 | tests = [self._validate_intro_section()]
508 | return all(tests) and parent_tests
509 |
510 |
511 | class TopicPageValidator(MarkdownValidator):
512 | """Validate the Markdown contents of a topic page, eg 01-topicname.md"""
513 | DOC_HEADERS = {"layout": vh.is_str,
514 | "title": vh.is_str,
515 | "subtitle": vh.is_str,
516 | "minutes": vh.is_numeric}
517 |
518 | CALLOUTS = {"objectives": ("Learning Objectives", 1, 1),
519 | "callout": (None, 0, None),
520 | "challenge": (None, 0, None)}
521 |
522 | def _validate_has_no_headings(self):
523 | """Check headings
524 |
525 | The top-level document has no headings indicating subtopics.
526 | The only valid subheadings are nested in blockquote elements"""
527 | heading_nodes = self.ast.get_section_headings()
528 | if len(heading_nodes) != 0:
529 | # Individual heading msgs are logged by validate_section_heading_order
530 | logging.warning(
531 | "In {0}: "
532 | "Sub-headings are often a sign "
533 | "a lesson needs to be split into multiple topics. "
534 | "Please make sure this subsection doesn't belong "
535 | "in a separate lesson.".format(self.filename))
536 |
537 | return True
538 |
539 | def _run_tests(self):
540 | parent_tests = super(TopicPageValidator, self)._run_tests()
541 | tests = [self._validate_has_no_headings()]
542 | return all(tests) and parent_tests
543 |
544 |
545 | class ReferencePageValidator(MarkdownValidator):
546 | """Validate reference.md"""
547 | HEADINGS = ["Glossary"]
548 | WARN_ON_EXTRA_HEADINGS = False
549 |
550 | DOC_HEADERS = {"layout": vh.is_str,
551 | "title": vh.is_str,
552 | "subtitle": vh.is_str}
553 |
554 | def _partition_links(self):
555 | """For reference.md, only check that text of link matches
556 | dest page subtitle if the link is in a heading"""
557 | all_links = self.ast.find_external_links()
558 | check_text = self.ast.find_external_links(
559 | parent_crit=self.ast.is_heading)
560 | dont_check_text = [n for n in all_links if n not in check_text]
561 | return check_text, dont_check_text
562 |
563 | def _validate_glossary_entry(self, glossary_entry):
564 | """Validate glossary entry
565 |
566 | Glossary entry must be formatted in conformance with Pandoc's
567 | ```definition_lists``` extension.
568 |
569 | That syntax isn't supported by the CommonMark parser, so we identify
570 | terms manually."""
571 | glossary_keyword = glossary_entry[0]
572 | if len(glossary_entry) < 2:
573 | logging.error(
574 | "In {0}: "
575 | "Glossary entry '{1}' must have at least two lines- "
576 | "a term and a definition.".format(
577 | self.filename, glossary_keyword))
578 | return False
579 |
580 | entry_is_valid = True
581 | for line_index, line in enumerate(glossary_entry):
582 | if line_index == 1 and not re.match("^: ", line):
583 | logging.error(
584 | "In {0}: "
585 | "At glossary entry '{1}' "
586 | "First line of definition must "
587 | "start with ': '.".format(
588 | self.filename, glossary_keyword))
589 | entry_is_valid = False
590 | return entry_is_valid
591 |
592 | def _validate_glossary(self):
593 | """Validate the glossary section.
594 |
595 | Assumes that the glossary is at the end of the file:
596 | everything after the header. (and there must be a glossary section)
597 |
598 | Verifies that the only things in the glossary are definition items.
599 | """
600 | is_glossary_valid = True
601 | in_glossary = False
602 | for node in self.ast.children:
603 | if in_glossary:
604 | is_glossary_valid = is_glossary_valid and \
605 | self._validate_glossary_entry(node.strings)
606 | elif self.ast.is_heading(node) and "Glossary" in node.strings:
607 | in_glossary = True
608 |
609 | return is_glossary_valid
610 |
611 | def _run_tests(self):
612 | tests = [self._validate_glossary()]
613 | parent_tests = super(ReferencePageValidator, self)._run_tests()
614 | return all(tests) and parent_tests
615 |
616 |
617 | class InstructorPageValidator(MarkdownValidator):
618 | """Simple validator for Instructor's Guide- instructors.md"""
619 | HEADINGS = ["Overall"]
620 | WARN_ON_EXTRA_HEADINGS = False
621 |
622 | DOC_HEADERS = {"layout": vh.is_str,
623 | "title": vh.is_str,
624 | "subtitle": vh.is_str}
625 |
626 | def _partition_links(self):
627 | """For instructors.md, only check that text of link matches
628 | dest page subtitle if the link is in a heading"""
629 | all_links = self.ast.find_external_links()
630 | check_text = self.ast.find_external_links(
631 | parent_crit=self.ast.is_heading)
632 | dont_check_text = [n for n in all_links if n not in check_text]
633 | return check_text, dont_check_text
634 |
635 |
636 | class LicensePageValidator(MarkdownValidator):
637 | """Validate LICENSE.md: user should not edit this file"""
638 | def _run_tests(self):
639 | """Skip the base tests; just check md5 hash"""
640 | # TODO: This hash is specific to the license for english-language repo
641 | expected_hash = '051a04b8ffe580ba6b7018fb4fd72a50'
642 | m = hashlib.md5()
643 | try:
644 | m.update(self.markdown)
645 | except TypeError:
646 | # Workaround for hashing in python3
647 | m.update(self.markdown.encode('utf-8'))
648 |
649 | if m.hexdigest() == expected_hash:
650 | return True
651 | else:
652 | logging.error("The provided license file should not be modified.")
653 | return False
654 |
655 |
656 | class DiscussionPageValidator(MarkdownValidator):
657 | """
658 | Validate the discussion page (discussion.md).
659 | Most of the content is free-form.
660 | """
661 | WARN_ON_EXTRA_HEADINGS = False
662 | DOC_HEADERS = {"layout": vh.is_str,
663 | "title": vh.is_str,
664 | "subtitle": vh.is_str}
665 |
666 |
667 | # Associate lesson template names with validators. This list used by CLI.
668 | # Dict of {name: (Validator, filename_pattern)}
669 | LESSON_TEMPLATES = {"index": (IndexPageValidator, "^index"),
670 | "topic": (TopicPageValidator, "^[0-9]{2}-.*"),
671 | "reference": (ReferencePageValidator, "^reference"),
672 | "instructor": (InstructorPageValidator, "^instructors"),
673 | "license": (LicensePageValidator, "^LICENSE"),
674 | "discussion": (DiscussionPageValidator, "^discussion")}
675 |
676 | # List of files in the lesson directory that should not be validated at all
677 | SKIP_FILES = ("CONDUCT.md", "CONTRIBUTING.md",
678 | "DESIGN.md", "FAQ.md", "LAYOUT.md", "README.md")
679 |
680 |
681 | def identify_template(filepath):
682 | """Identify template
683 |
684 | Given the path to a single file,
685 | identify the appropriate template to use"""
686 | for template_name, (validator, pattern) in LESSON_TEMPLATES.items():
687 | if re.search(pattern, os.path.basename(filepath)):
688 | return template_name
689 |
690 | return None
691 |
692 |
693 | def validate_single(filepath, template=None):
694 | """Validate a single Markdown file based on a specified template"""
695 | if os.path.basename(filepath) in SKIP_FILES:
696 | # Silently pass certain non-lesson files without validating them
697 | return True
698 |
699 | template = template or identify_template(filepath)
700 | if template is None:
701 | logging.error(
702 | "Validation failed for {0}: "
703 | "Could not automatically identify correct template.".format(
704 | filepath))
705 | return False
706 |
707 | logging.debug(
708 | "Beginning validation of {0} using template {1}".format(
709 | filepath, template))
710 | validator = LESSON_TEMPLATES[template][0]
711 | validate_file = validator(filepath)
712 |
713 | res = validate_file.validate()
714 | if res is True:
715 | logging.debug("File {0} successfully passed validation".format(
716 | filepath))
717 | else:
718 | logging.debug("File {0} failed validation: "
719 | "see error log for details".format(filepath))
720 |
721 | return res
722 |
723 |
724 | def validate_folder(path, template=None):
725 | """Validate an entire folder of files"""
726 | search_str = os.path.join(path, "*.md") # Find files based on extension
727 | filename_list = glob.glob(search_str)
728 |
729 | if not filename_list:
730 | logging.error(
731 | "No Markdown files were found "
732 | "in specified directory {0}".format(path))
733 | return False
734 |
735 | all_valid = True
736 | for fn in filename_list:
737 | res = validate_single(fn, template=template)
738 | all_valid = all_valid and res
739 | return all_valid
740 |
741 |
742 | def start_logging(level=logging.INFO):
743 | """Initialize logging and print messages to console."""
744 | logging.basicConfig(stream=sys.stdout,
745 | level=level,
746 | format="%(levelname)s: %(message)s")
747 |
748 |
749 | def command_line():
750 | """Handle arguments passed in via the command line"""
751 | parser = argparse.ArgumentParser()
752 | parser.add_argument("file_or_path",
753 | nargs="*",
754 | default=[os.getcwd()],
755 | help="The individual pathname")
756 |
757 | parser.add_argument('-t', '--template',
758 | choices=LESSON_TEMPLATES.keys(),
759 | help="The type of template to apply to all file(s). "
760 | "If not specified, will auto-identify template.")
761 |
762 | parser.add_argument('-d', '--debug',
763 | action='store_true',
764 | help="Enable debug information.")
765 |
766 | return parser.parse_args()
767 |
768 |
769 | def check_required_files(dir_to_validate):
770 | """Check if required files exists."""
771 | REQUIRED_FILES = ["01-*.md",
772 | "CONDUCT.md",
773 | "CONTRIBUTING.md",
774 | "discussion.md",
775 | "index.md",
776 | "instructors.md",
777 | "LICENSE.md",
778 | "README.md",
779 | "reference.md"]
780 | valid = True
781 |
782 | for required in REQUIRED_FILES:
783 | req_fn = os.path.join(dir_to_validate, required)
784 | if not glob.glob(req_fn):
785 | logging.error(
786 | "Missing file {0}.".format(required))
787 | valid = False
788 |
789 | return valid
790 |
791 |
792 | def get_files_to_validate(file_or_path):
793 | """Generate list of files to validate."""
794 | files_to_validate = []
795 | dirs_to_validate = []
796 |
797 | for fn in file_or_path:
798 | if os.path.isdir(fn):
799 | search_str = os.path.join(fn, "*.md")
800 | files_to_validate.extend(glob.glob(search_str))
801 | dirs_to_validate.append(fn)
802 | elif os.path.isfile(fn):
803 | files_to_validate.append(fn)
804 | else:
805 | logging.error(
806 | "The specified file or folder {0} does not exist; "
807 | "could not perform validation".format(fn))
808 |
809 | return files_to_validate, dirs_to_validate
810 |
811 |
812 | def main(parsed_args_obj):
813 | if parsed_args_obj.debug:
814 | log_level = "DEBUG"
815 | else:
816 | log_level = "WARNING"
817 | start_logging(log_level)
818 |
819 | template = parsed_args_obj.template
820 |
821 | all_valid = True
822 |
823 | files_to_validate, dirs_to_validate = get_files_to_validate(
824 | parsed_args_obj.file_or_path)
825 |
826 | # If user ask to validate only one file don't check for required files.
827 | for d in dirs_to_validate:
828 | all_valid = all_valid and check_required_files(d)
829 |
830 | for fn in files_to_validate:
831 | res = validate_single(fn, template=template)
832 |
833 | all_valid = all_valid and res
834 |
835 | if all_valid is True:
836 | logging.debug("All Markdown files successfully passed validation.")
837 | else:
838 | logging.warning(
839 | "{0} errors were encountered during validation. "
840 | "See log for details.".format(NUMBER_OF_ERRORS))
841 | sys.exit(NUMBER_OF_ERRORS)
842 |
843 |
844 | if __name__ == "__main__":
845 | parsed_args = command_line()
846 | main(parsed_args)
847 |
--------------------------------------------------------------------------------
/css/bootstrap/bootstrap-theme.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","bootstrap-theme.css","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAcA;;;;;;EAME,0CAAA;ECgDA,6FAAA;EACQ,qFAAA;EC5DT;AFgBC;;;;;;;;;;;;EC2CA,0DAAA;EACQ,kDAAA;EC7CT;AFVD;;;;;;EAiBI,mBAAA;EECH;AFiCC;;EAEE,wBAAA;EE/BH;AFoCD;EGnDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJiCA,6BAAA;EACA,uBAAA;EAgC2C,2BAAA;EAA2B,oBAAA;EEzBvE;AFLC;;EAEE,2BAAA;EACA,8BAAA;EEOH;AFJC;;EAEE,2BAAA;EACA,uBAAA;EEMH;AFHC;;;EAGE,2BAAA;EACA,wBAAA;EEKH;AFUD;EGpDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJiCA,6BAAA;EACA,uBAAA;EEgCD;AF9BC;;EAEE,2BAAA;EACA,8BAAA;EEgCH;AF7BC;;EAEE,2BAAA;EACA,uBAAA;EE+BH;AF5BC;;;EAGE,2BAAA;EACA,wBAAA;EE8BH;AFdD;EGrDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJiCA,6BAAA;EACA,uBAAA;EEyDD;AFvDC;;EAEE,2BAAA;EACA,8BAAA;EEyDH;AFtDC;;EAEE,2BAAA;EACA,uBAAA;EEwDH;AFrDC;;;EAGE,2BAAA;EACA,wBAAA;EEuDH;AFtCD;EGtDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJiCA,6BAAA;EACA,uBAAA;EEkFD;AFhFC;;EAEE,2BAAA;EACA,8BAAA;EEkFH;AF/EC;;EAEE,2BAAA;EACA,uBAAA;EEiFH;AF9EC;;;EAGE,2BAAA;EACA,wBAAA;EEgFH;AF9DD;EGvDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJiCA,6BAAA;EACA,uBAAA;EE2GD;AFzGC;;EAEE,2BAAA;EACA,8BAAA;EE2GH;AFxGC;;EAEE,2BAAA;EACA,uBAAA;EE0GH;AFvGC;;;EAGE,2BAAA;EACA,wBAAA;EEyGH;AFtFD;EGxDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJiCA,6BAAA;EACA,uBAAA;EEoID;AFlIC;;EAEE,2BAAA;EACA,8BAAA;EEoIH;AFjIC;;EAEE,2BAAA;EACA,uBAAA;EEmIH;AFhIC;;;EAGE,2BAAA;EACA,wBAAA;EEkIH;AFxGD;;EChBE,oDAAA;EACQ,4CAAA;EC4HT;AFnGD;;EGzEI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EHwEF,2BAAA;EEyGD;AFvGD;;;EG9EI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8EF,2BAAA;EE6GD;AFpGD;EG3FI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ECnBF,qEAAA;EJ6GA,oBAAA;EC/CA,6FAAA;EACQ,qFAAA;EC0JT;AF/GD;;EG3FI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EF2CF,0DAAA;EACQ,kDAAA;ECoKT;AF5GD;;EAEE,gDAAA;EE8GD;AF1GD;EG9GI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ECnBF,qEAAA;EF+OD;AFlHD;;EG9GI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EF2CF,yDAAA;EACQ,iDAAA;EC0LT;AF5HD;;EAYI,2CAAA;EEoHH;AF/GD;;;EAGE,kBAAA;EEiHD;AF5FD;EAfI;;;IAGE,aAAA;IG3IF,0EAAA;IACA,qEAAA;IACA,+FAAA;IAAA,wEAAA;IACA,6BAAA;IACA,wHAAA;ID0PD;EACF;AFxGD;EACE,+CAAA;ECzGA,4FAAA;EACQ,oFAAA;ECoNT;AFhGD;EGpKI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH4JF,uBAAA;EE4GD;AFvGD;EGrKI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH4JF,uBAAA;EEoHD;AF9GD;EGtKI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH4JF,uBAAA;EE4HD;AFrHD;EGvKI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH4JF,uBAAA;EEoID;AFrHD;EG/KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDuSH;AFlHD;EGzLI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED8SH;AFxHD;EG1LI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDqTH;AF9HD;EG3LI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED4TH;AFpID;EG5LI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDmUH;AF1ID;EG7LI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED0UH;AF7ID;EGhKI,+MAAA;EACA,0MAAA;EACA,uMAAA;EDgTH;AFzID;EACE,oBAAA;EC5JA,oDAAA;EACQ,4CAAA;ECwST;AF1ID;;;EAGE,+BAAA;EGjNE,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH+MF,uBAAA;EEgJD;AFrJD;;;EAQI,mBAAA;EEkJH;AFxID;ECjLE,mDAAA;EACQ,2CAAA;EC4TT;AFlID;EG1OI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED+WH;AFxID;EG3OI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDsXH;AF9ID;EG5OI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED6XH;AFpJD;EG7OI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDoYH;AF1JD;EG9OI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED2YH;AFhKD;EG/OI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDkZH;AFhKD;EGtPI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EHoPF,uBAAA;ECzMA,2FAAA;EACQ,mFAAA;ECgXT","file":"bootstrap-theme.css","sourcesContent":["\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &:disabled,\n &[disabled] {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n",".btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default:disabled,\n.btn-default[disabled] {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary:disabled,\n.btn-primary[disabled] {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success:disabled,\n.btn-success[disabled] {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info:disabled,\n.btn-info[disabled] {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning:disabled,\n.btn-warning[disabled] {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger:disabled,\n.btn-danger[disabled] {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222222 100%);\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n}\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]}
--------------------------------------------------------------------------------