├── .nojekyll ├── requirements.txt ├── .update-copyright.conf ├── img └── software-carpentry-banner.png ├── .gitignore ├── css ├── bootstrap │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── bootstrap-theme.css │ └── bootstrap-theme.css.map ├── book.css ├── swc-print.css └── swc.css ├── _includes ├── banner.html ├── footer.html ├── header.html └── javascript.html ├── tools ├── check_knitr_version.R ├── filters │ ├── id4glossary.py │ └── blockquote2div.py ├── chunk-options.R ├── setup-labels ├── validation_helpers.py ├── test_check.py └── check.py ├── .mailmap ├── AUTHORS ├── _layouts └── page.html ├── CONDUCT.md ├── CONTRIBUTING.md ├── Makefile ├── LICENSE.md └── js └── modernizr.custom.js /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | CommonMark 2 | pandocfilters 3 | PyYAML 4 | update-copyright 5 | -------------------------------------------------------------------------------- /.update-copyright.conf: -------------------------------------------------------------------------------- 1 | [project] 2 | vcs: Git 3 | 4 | [files] 5 | authors: yes 6 | files: no 7 | -------------------------------------------------------------------------------- /img/software-carpentry-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swcarpentry/DEPRECATED-lesson-template/HEAD/img/software-carpentry-banner.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README.html 2 | LAYOUT.html 3 | FAQ.html 4 | DESIGN.html 5 | CONTRIBUTING.html 6 | CONDUCT.html 7 | *~ 8 | *.pyc 9 | _site 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /css/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swcarpentry/DEPRECATED-lesson-template/HEAD/css/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /css/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swcarpentry/DEPRECATED-lesson-template/HEAD/css/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /css/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swcarpentry/DEPRECATED-lesson-template/HEAD/css/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /css/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swcarpentry/DEPRECATED-lesson-template/HEAD/css/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /_includes/banner.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tools/check_knitr_version.R: -------------------------------------------------------------------------------- 1 | if (require("knitr")) { 2 | if (packageVersion("knitr") < '1.9.19') { 3 | stop("knitr must be version 1.9.20 or higher") 4 | } 5 | } else stop("knitr 1.9.20 or above is needed to build the lessons.") 6 | -------------------------------------------------------------------------------- /css/book.css: -------------------------------------------------------------------------------- 1 | span.subtitle { 2 | color: #030303; 3 | display: block; 4 | font-family: inherit; 5 | font-size: 31.5px; 6 | font-weight: bold; 7 | line-height: 40px; 8 | margin: 40px 0px 10px 0px; 9 | text-rendering: optimizelegibility; 10 | } 11 | -------------------------------------------------------------------------------- /_includes/footer.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Andy Boughton 2 | Greg Wilson 3 | Abigail Cabunoc 4 | Francois Michonneau 5 | Raniere Silva 6 | Michael Jackson 7 | -------------------------------------------------------------------------------- /_includes/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | lesson-template was written by: 2 | Aaron O'Leary 3 | Abigail Cabunoc 4 | Andy Boughton 5 | Bill Mills 6 | Francois Michonneau 7 | Greg Wilson 8 | James Allen 9 | John Blischak 10 | Jon Pipitone 11 | Michael Hansen 12 | Piotr Banaszkiewicz 13 | Raniere Silva 14 | Rémi Emonet 15 | Timothée Poisot 16 | Trevor Bekolay 17 | W. Trevor King 18 | -------------------------------------------------------------------------------- /_layouts/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Software Carpentry: $pagetitle$ 7 | $header$ 8 | 9 | 10 |
11 | $banner$ 12 |
13 |
14 |
15 | $if(subtitle)$ 16 |

$title$

17 |

$subtitle$

18 | $else$ 19 |

$title$

20 | $endif$ 21 | $body$ 22 |
23 |
24 |
25 | $footer$ 26 |
27 | $javascript$ 28 | 29 | 30 | -------------------------------------------------------------------------------- /_includes/javascript.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /tools/filters/id4glossary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Pandoc filter to convert add ids to glossary entries. 3 | 4 | Usage: 5 | 6 | pandoc source.md --filter=id4glossary.py --output=output.html 7 | """ 8 | import pandocfilters as pf 9 | 10 | def normalize_keyword(keyword): 11 | """Normalize keyword for became id 12 | 13 | - Replace white space with '-' 14 | - Convert to lowercase""" 15 | return keyword.lower().replace(' ', '-') 16 | 17 | def keyword2html(keyword_node): 18 | """Return HTML version of keyword with id.""" 19 | keyword = pf.stringify(keyword_node) 20 | id = normalize_keyword(keyword) 21 | return [{"t": "Span", 22 | "c": [[id, [],[]], 23 | keyword_node]}] 24 | 25 | def id4glossary(key, value, format, meta): 26 | """Add id to keywords at glossary.""" 27 | if key == "DefinitionList": 28 | for definition in value: 29 | definition[0] = keyword2html(definition[0]) 30 | return {"t": key, 31 | "c": value} 32 | 33 | if __name__ == '__main__': 34 | pf.toJSONFilter(id4glossary) 35 | -------------------------------------------------------------------------------- /tools/chunk-options.R: -------------------------------------------------------------------------------- 1 | # These settings control the behavior of all chunks in the novice R materials. 2 | # For example, to generate the lessons with all the output hidden, simply change 3 | # `results` from "markup" to "hide". 4 | # For more information on available chunk options, see 5 | # http://yihui.name/knitr/options#chunk_options 6 | 7 | library("knitr") 8 | opts_chunk$set(tidy = FALSE, results = "markup", comment = NA, 9 | fig.align = "center", fig.path = "fig/") 10 | 11 | # The hooks below add html tags to the code chunks and their output so that they 12 | # are properly formatted when the site is built. 13 | hook_in <- function(x, options) { 14 | stringr::str_c("\n\n~~~{.r}\n", 15 | paste0(x, collapse="\n"), 16 | "\n~~~\n\n") 17 | } 18 | 19 | hook_out <- function(x, options) { 20 | stringr::str_c("\n\n~~~{.output}\n", 21 | paste0(x, collapse="\n"), 22 | "\n~~~\n\n") 23 | } 24 | 25 | hook_error <- function(x, options) { 26 | stringr::str_c("\n\n~~~{.error}\n", 27 | paste0(x, collapse="\n"), 28 | "\n~~~\n\n") 29 | } 30 | 31 | knit_hooks$set(source = hook_in, output = hook_out, warning = hook_error, 32 | error = hook_error, message = hook_out) 33 | -------------------------------------------------------------------------------- /css/swc-print.css: -------------------------------------------------------------------------------- 1 | /* Printing */ 2 | @media print { 3 | h1 { 4 | font-size: 16pt; 5 | line-height: 18pt; 6 | } 7 | 8 | h2,h3,h4,h5,h6 { 9 | font-size: 12pt; 10 | line-height: 13pt; 11 | } 12 | 13 | /* Objectives, Callout Box and Challenges */ 14 | .objectives, .keypoints { 15 | background-color: unset; 16 | border: 5px solid; 17 | } 18 | 19 | .callout { 20 | background-color: unset; 21 | border: 5px solid; 22 | } 23 | 24 | .challenge { 25 | background-color: unset; 26 | border: 5px solid; 27 | } 28 | 29 | p,ul,ol,li,pre,code { 30 | font-size: 8pt; 31 | line-height: 9pt; 32 | } 33 | 34 | code { 35 | padding: 0px; 36 | border: 0px; 37 | background: unset; 38 | } 39 | 40 | pre.sourceCode::before, 41 | pre.input::before. { 42 | content: "Input:"; 43 | } 44 | 45 | pre.output::before { 46 | content: "Output:"; 47 | } 48 | 49 | pre.error::before { 50 | content: "Error:"; 51 | } 52 | 53 | pre.sourceCode code, 54 | pre.input code, 55 | pre.output code, 56 | pre.error code { 57 | display: block; 58 | margin-top: 1em; 59 | margin-left: 2em; 60 | } 61 | 62 | #github-ribbon { 63 | display: none; 64 | } 65 | 66 | .banner { 67 | display: none; 68 | } 69 | 70 | .footer { 71 | display: none; 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/). 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Software Carpentry is an open source project, 2 | and we welcome contributions of all kinds: 3 | new lessons, 4 | fixes to existing material, 5 | bug reports, 6 | and reviews of proposed changes are all equally welcome. 7 | 8 | By contributing, 9 | you are agreeing that Software Carpentry may redistribute your work under 10 | [these licenses][license]. 11 | You also agree to abide by our 12 | [contributor code of conduct][conduct]. 13 | 14 | ## Getting Started 15 | 16 | 1. We use GitHub flow to manage changes, 17 | which is explained in the chapter [Contributing to a Project][pro-git-chapter] 18 | in Scott Chacon's book *Pro Git*. 19 | 20 | 2. For our lessons, 21 | you should branch from and submit pull requests against the `gh-pages` branch. 22 | 23 | 3. When editing lesson pages, you need only commit changes to the Markdown source files. 24 | 25 | 4. If you're looking for things to work on, 26 | please see [the list of issues for this repository][issues], 27 | or for [our other lessons][swc-lessons]. 28 | Comments on issues and reviews of pull requests are equally welcome. 29 | 30 | ## Other Resources 31 | 32 | 1. This lesson is based on the template found at 33 | [https://github.com/swcarpentry/lesson-template](https://github.com/swcarpentry/lesson-template). 34 | That repository has instructions on formatting and previewing lessons. 35 | 36 | 2. For a list of helpful commands run `make` in this directory. 37 | 38 | [conduct]: CONDUCT.md 39 | [issues]: https://github.com/swcarpentry/lesson-template/issues 40 | [license]: LICENSE.md 41 | [pro-git-chapter]: http://git-scm.com/book/en/v2/GitHub-Contributing-to-a-Project 42 | [swc-lessons]: http://software-carpentry.org/lessons.html 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PANDOC ?= pandoc 2 | PANDOC_FLAGS = --smart 3 | 4 | # R Markdown files. 5 | SRC_RMD = $(wildcard ??-*.Rmd) 6 | DST_RMD = $(patsubst %.Rmd,%.md,$(SRC_RMD)) 7 | 8 | # All Markdown files (hand-written and generated). 9 | ALL_MD = $(wildcard *.md) $(DST_RMD) 10 | EXCLUDE_MD = README.md LAYOUT.md FAQ.md DESIGN.md CONTRIBUTING.md CONDUCT.md 11 | SRC_MD = $(filter-out $(EXCLUDE_MD),$(ALL_MD)) 12 | DST_HTML = $(patsubst %.md,%.html,$(SRC_MD)) 13 | 14 | # All outputs. 15 | DST_ALL = $(DST_HTML) 16 | 17 | # Pandoc filters. 18 | FILTERS = $(wildcard tools/filters/*.py) 19 | 20 | # Inclusions. 21 | INCLUDES = \ 22 | -Vheader="$$(cat _includes/header.html)" \ 23 | -Vbanner="$$(cat _includes/banner.html)" \ 24 | -Vfooter="$$(cat _includes/footer.html)" \ 25 | -Vjavascript="$$(cat _includes/javascript.html)" 26 | 27 | # Chunk options for knitr (used in R conversion). 28 | R_CHUNK_OPTS = tools/chunk-options.R 29 | 30 | # Ensure that intermediate (generated) Markdown files from R are kept. 31 | .SECONDARY: $(DST_RMD) 32 | 33 | # Default action is to show what commands are available. 34 | all : commands 35 | 36 | ## check : Validate all lesson content against the template. 37 | check: $(ALL_MD) 38 | python tools/check.py . 39 | 40 | ## clean : Clean up temporary and intermediate files. 41 | clean : 42 | @rm -rf $$(find . -name '*~' -print) 43 | 44 | ## preview : Build website locally for checking. 45 | preview : $(DST_ALL) 46 | 47 | # Pattern to build a generic page. 48 | %.html : %.md _layouts/page.html $(FILTERS) 49 | ${PANDOC} -s -t html \ 50 | ${PANDOC_FLAGS} \ 51 | --mathjax \ 52 | --template=_layouts/page \ 53 | --filter=tools/filters/blockquote2div.py \ 54 | --filter=tools/filters/id4glossary.py \ 55 | $(INCLUDES) \ 56 | -o $@ $< 57 | 58 | # Pattern to convert R Markdown to Markdown. 59 | %.md: %.Rmd $(R_CHUNK_OPTS) tools/check_knitr_version.R 60 | Rscript -e "source('tools/check_knitr_version.R')" 61 | Rscript -e "knitr::knit('$$(basename $<)', output = '$$(basename $@)')" 62 | 63 | ## commands : Display available commands. 64 | commands : Makefile 65 | @sed -n 's/^##//p' $< 66 | 67 | ## settings : Show variables and settings. 68 | settings : 69 | @echo 'PANDOC:' $(PANDOC) 70 | @echo 'SRC_RMD:' $(SRC_RMD) 71 | @echo 'DST_RMD:' $(DST_RMD) 72 | @echo 'SRC_MD:' $(SRC_MD) 73 | @echo 'DST_HTML:' $(DST_HTML) 74 | 75 | ## unittest : Run internal tests to ensure the validator is working correctly (for Python 2 and 3). 76 | unittest: tools/check.py tools/validation_helpers.py tools/test_check.py 77 | cd tools/ && python2 test_check.py 78 | cd tools/ && python3 test_check.py 79 | -------------------------------------------------------------------------------- /tools/setup-labels: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | ## This script sets up labels for issues in your GitHub repository. 4 | ## 5 | ## Syntax: 6 | ## 7 | ## $ tools/setup-label OWNER REPO 8 | ## 9 | ## Parameters: 10 | ## 11 | ## - OWNER: GitHub username of the owner of the repository 12 | ## - REPO: the name of the repository 13 | ## 14 | ## Example: 15 | ## 16 | ## For set up the labels at https://github.com/wking/swc-modular-shell use 17 | ## 18 | ## $ tools/setup-label wking swc-modular-shell 19 | 20 | CURL_FLAGS="--silent --fail" 21 | 22 | if test $# -lt 2 23 | then 24 | echo "Missing parameters." 25 | echo 26 | grep '^##' tools/setup-labels | sed 's/## //' | sed 's/##//' 27 | exit 1 28 | fi 29 | 30 | OWNER=$1 31 | REPO=$2 32 | GITHUB_URL=https://github.com/${OWNER}/${REPO} 33 | LABELS=(bug build defer discussion documentation enhancement newcomer-friendly upstream work-in-progress) 34 | COLORS=(FF0000 551033 66FF00 0000FF D4318C E0115F FCE883 83F52C 545AA7) 35 | LABELS_TO_DELETE=(duplicate filed-by-newcomer getting-started help%20wanted help-wanted invalid left-as-was suitable-for-newcomer question wontfix) 36 | 37 | # Test if repository exists 38 | curl -s --head ${GITHUB_URL} | head -n 1 | grep -q "HTTP/1.[01] [23].." 39 | if test $? -ne 0 40 | then 41 | echo "ERROR: this repository doesn't exist" 42 | exit $? 43 | fi 44 | 45 | echo "Before setup the labels for ${GITHUB_URL}" 46 | echo "you must provide some informations." 47 | echo "Your GitHub username:" 48 | read USERNAME 49 | echo "Your GitHub password:" 50 | read -s PASSWORD 51 | 52 | # Delete labels 53 | for INDEX in $(seq 0 $((${#LABELS_TO_DELETE[*]} - 1))) 54 | do 55 | # Try to delete label 56 | curl ${CURL_FLAGS} -X DELETE \ 57 | -u ${USERNAME}:${PASSWORD} \ 58 | "https://api.github.com/repos/${OWNER}/${REPO}/labels/${LABELS_TO_DELETE[${INDEX}]}" > /dev/null 59 | done 60 | # Create labels 61 | for INDEX in $(seq 0 $((${#LABELS[*]} - 1))) 62 | do 63 | # Try create new label 64 | curl ${CURL_FLAGS} -X POST \ 65 | -u ${USERNAME}:${PASSWORD} \ 66 | -d "{\"name\":\"${LABELS[${INDEX}]}\",\"color\":\"${COLORS[${INDEX}]}\"}" \ 67 | "https://api.github.com/repos/${OWNER}/${REPO}/labels" > /dev/null 68 | if test $? -ne 0 69 | then 70 | # Try to fix label color 71 | curl ${CURL_FLAGS} -X PATCH \ 72 | -u ${USERNAME}:${PASSWORD} \ 73 | -d "{\"name\":\"${LABELS[${INDEX}]}\",\"color\":\"${COLORS[${INDEX}]}\"}" \ 74 | "https://api.github.com/repos/${OWNER}/${REPO}/labels/${LABELS[${INDEX}]}" > /dev/null 75 | if test $? -ne 0 76 | then 77 | echo "Failed when trying to create and update the label ${LABELS[${INDEX}]}." 78 | echo "Please check at ${GITHUB_URL}/labels" 79 | echo "" 80 | echo "If you find a bug report it at" 81 | echo "https://github.com/swcarpentry/lesson-template/." 82 | fi 83 | fi 84 | done 85 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Licenses 4 | --- 5 | ## Instructional Material 6 | 7 | All Software Carpentry instructional material is made available under 8 | the [Creative Commons Attribution license][cc-by-human]. The following 9 | is a human-readable summary of (and not a substitute for) the [full 10 | legal text of the CC BY 4.0 license][cc-by-legal]. 11 | 12 | You are free: 13 | 14 | * to **Share**---copy and redistribute the material in any medium or format 15 | * to **Adapt**---remix, transform, and build upon the material 16 | 17 | for any purpose, even commercially. 18 | 19 | The licensor cannot revoke these freedoms as long as you follow the 20 | license terms. 21 | 22 | Under the following terms: 23 | 24 | * **Attribution**---You must give appropriate credit (mentioning that 25 | your work is derived from work that is Copyright © Software 26 | Carpentry and, where practical, linking to 27 | http://software-carpentry.org/), provide a [link to the 28 | license][cc-by-human], and indicate if changes were made. You may do 29 | so in any reasonable manner, but not in any way that suggests the 30 | licensor endorses you or your use. 31 | 32 | **No additional restrictions**---You may not apply legal terms or 33 | technological measures that legally restrict others from doing 34 | anything the license permits. With the understanding that: 35 | 36 | Notices: 37 | 38 | * You do not have to comply with the license for elements of the 39 | material in the public domain or where your use is permitted by an 40 | applicable exception or limitation. 41 | * No warranties are given. The license may not give you all of the 42 | permissions necessary for your intended use. For example, other 43 | rights such as publicity, privacy, or moral rights may limit how you 44 | use the material. 45 | 46 | ## Software 47 | 48 | Except where otherwise noted, the example programs and other software 49 | provided by Software Carpentry are made available under the 50 | [OSI][osi]-approved 51 | [MIT license][mit-license]. 52 | 53 | Permission is hereby granted, free of charge, to any person obtaining 54 | a copy of this software and associated documentation files (the 55 | "Software"), to deal in the Software without restriction, including 56 | without limitation the rights to use, copy, modify, merge, publish, 57 | distribute, sublicense, and/or sell copies of the Software, and to 58 | permit persons to whom the Software is furnished to do so, subject to 59 | the following conditions: 60 | 61 | The above copyright notice and this permission notice shall be 62 | included in all copies or substantial portions of the Software. 63 | 64 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 65 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 66 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 67 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 68 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 69 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 70 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 71 | 72 | ## Trademark 73 | 74 | "Software Carpentry" and the Software Carpentry logo are registered 75 | trademarks of [NumFOCUS][numfocus]. 76 | 77 | [cc-by-human]: https://creativecommons.org/licenses/by/4.0/ 78 | [cc-by-legal]: https://creativecommons.org/licenses/by/4.0/legalcode 79 | [mit-license]: http://opensource.org/licenses/mit-license.html 80 | [numfocus]: http://numfocus.org/ 81 | [osi]: http://opensource.org 82 | -------------------------------------------------------------------------------- /tools/filters/blockquote2div.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Pandoc filter to convert Blockquotes with attributes into Div 3 | with attributes. 4 | 5 | Usage: 6 | 7 | pandoc source.md --filter=blockquote2div.py --output=output.html 8 | 9 | A blockquote will be converted if 10 | 11 | 1. it begins with a header 12 | 2. that either 13 | 1. matches "Prerequisites", "Objectives", "Callout" or "Challenge" OR 14 | 2. has attributes containing a single class matching 15 | one of ['prereq', 'objectives', 'callout', 'challenge'] 16 | 17 | For example, this is a valid blockquote: 18 | 19 | > ## Callout time! {.callout} 20 | > Let's do something 21 | 22 | and it will be converted into this markdown: 23 | 24 |
25 | ## Callout time! 26 | Let's do something. 27 |
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 | """![this is the image's title](fig/example.svg "this is the image's alt text")""") 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 | """![this is the image's title](fig/exemple.svg "this is the image's alt text")""") 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"]} --------------------------------------------------------------------------------