1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2025-01-18 08:54:46 +01:00

Merge branch 'master' into refactor/search-results

This commit is contained in:
squidfunk 2017-03-11 19:52:46 +01:00
commit f72a88721e
427 changed files with 11337 additions and 925 deletions

View File

@ -2,9 +2,6 @@
"presets": ["es2015"],
"plugins": [
"add-module-exports",
"babel-root-import",
["transform-react-jsx", {
"pragma": "JSX.createElement"
}]
"babel-root-import"
]
}

View File

@ -18,7 +18,15 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Build files
# Files generated by build
/build
/material
/site
# Files used and generated by flow
/lib/declarations
/tmp
# Files generated by visual tests
/gemini-report
/tests/visual/data

View File

@ -169,7 +169,7 @@
"space-unary-ops": 2,
"spaced-comment": [2, "always", {
"line": {
"markers": ["/"],
"markers": ["/", ":"],
"exceptions": ["-", "+"]
},
"block": {

8
.flowconfig Normal file
View File

@ -0,0 +1,8 @@
[ignore]
.*/node_modules/.*
[libs]
lib/declarations/
[options]
strip_root=true

View File

@ -25,6 +25,6 @@ CHANGED="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"
# Perform install and prune of NPM dependencies if package.json changed
if $(echo "$CHANGED" | grep --quiet package.json); then
echo "Hook[post-merge]: Updating dependencies..."
npm install && npm prune
echo -e "\x1B[33m!\x1B[0m Updating dependencies"
yarn install
fi

View File

@ -22,12 +22,14 @@
# Determine current branch
BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Hook[pre-commit]: Checking branch..."
MESSAGE="Commits on master are only allowed via Pull Requests. Aborting."
# If we're on master, abort commit
if [[ "$BRANCH" == "master" ]]; then
echo "Commits on master are only allowed via Pull Requests. Aborting."
echo -e "\x1B[31m✗\x1B[0m Branch: $BRANCH - \x1B[31m$MESSAGE\x1B[0m"
exit 1
else
echo -e "\x1B[32m✓\x1B[0m Branch: $BRANCH"
fi
# We're good

View File

@ -22,6 +22,7 @@
# Patch file to store unindexed changes
PATCH_FILE=".working-tree.patch"
MESSAGE="Terminated with errors"
# Revert changes that have been registered in the patch file
function cleanup {
@ -44,8 +45,26 @@ git checkout -- .
FILES=$(git diff --cached --name-only --diff-filter=ACMR | \
grep "\.\(js\|jsx\|scss\)$")
# Run the check and print indicator
# Run check and print indicator
if [ "$FILES" ]; then
echo "Hook[pre-commit]: Running linter..."
npm run lint --silent || exit 1
# If linter terminated with errors, abort commit
if [ $? -gt 0 ]; then
echo -e "\x1B[31m✗\x1B[0m Linter - \x1B[31m$MESSAGE\x1B[0m"
exit 1
else
echo -e "\x1B[32m✓\x1B[0m Linter"
fi
# If flow terminated with errors, abort commit
yarn run flow --silent
if [ $? -gt 0 ]; then
echo -e "\x1B[31m✗\x1B[0m Flow - \x1B[31m$MESSAGE\x1B[0m"
exit 1
else
echo -e "\x1B[32m✓\x1B[0m Flow"
fi
fi
# We're good
exit 0

13
.gitignore vendored
View File

@ -23,14 +23,23 @@
# NPM-related
/node_modules
/npm-debug.log
/npm-debug.log*
/yarn-error.log
# Build files
# Files generated by build
/build
/manifest.json
/MANIFEST
/site
# Files generated by flow typechecker
/tmp
# Files generated by visual tests
/gemini-report
/tests/visual/baseline/local
/tests/visual/data
# Distribution files
/dist
/mkdocs_material.egg-info

View File

@ -180,7 +180,6 @@
"z-index"
],
"property-no-vendor-prefix": true,
"root-no-standard-properties": true,
"selector-class-pattern": "^[a-z0-9]+(-[a-z0-9]+)*(__[a-z]+)?(--[a-z]+)?$",
"selector-descendant-combinator-no-non-space": null,
"string-quotes": "double",

View File

@ -21,21 +21,94 @@
language: node_js
sudo: false
# -----------------------------------------------------------------------------
# Regular builds
# -----------------------------------------------------------------------------
# Node.js versions
node_js:
- 4
- 5
- 6
- 7
# Limit clone depth to 5, to speed up build
git:
depth: 5
# Cache dependencies
cache:
pip: true
yarn: true
directories:
- node_modules
# Install yarn as Travis doesn't support it out of the box
before_install:
- npm install -g yarn
# Install dependencies
before_script:
install:
- yarn install --ignore-optional
- pip install --user -r requirements.txt
# Perform build and tests
script: npm run build
script:
- yarn run build
# -----------------------------------------------------------------------------
# Additional builds
# -----------------------------------------------------------------------------
# Matrix for additional builds
matrix:
include:
# Build release and docker image and send to PyPI and Docker Hub.
- node_js: 5
services:
- docker
env:
- __TASK=RELEASE
# If we're not on a release branch, exit early and indicate success
before_install:
- echo "$TRAVIS_BRANCH" | grep -qvE "^[0-9.]+$" && exit 0; :;
# Install wheel for build
install:
- pip install wheel
# Perform build
script:
- python setup.py build sdist bdist_wheel --universal
- docker build -t $TRAVIS_REPO_SLUG .
# If build was successful, publish
after_success:
# Install twine and push release to PyPI
- pip install twine
- twine upload -u $PYPI_USERNAME -p $PYPI_PASSWORD dist/*
# Push to Docker Hub
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- docker tag $TRAVIS_REPO_SLUG $TRAVIS_REPO_SLUG:$TRAVIS_BRANCH
- docker tag $TRAVIS_REPO_SLUG $TRAVIS_REPO_SLUG:latest
- docker push $TRAVIS_REPO_SLUG
# # Build visual tests separately - temporary disabled until tests stable
# - node_js: 5
# addons:
# artifacts:
# paths:
# - gemini-report
# apt:
# sources:
# - ubuntu-toolchain-r-test
# packages:
# - gcc-4.8
# - g++-4.8
# env:
# - CXX=g++-4.8
# install: yarn install
# script: yarn run test:visual:run

View File

@ -1,3 +1,56 @@
mkdocs-material-1.3.0 (2017-11-03)
* Added support for page-specific title and description using metadata
* Added support for linking source files to documentation
* Fixed jitter and offset of sidebar when zooming browser
* Fixed incorrectly initialized tablet sidebar height
* Fixed regression for #1: GitHub stars break if the repo_url ends with a '/'
* Fixed undesired white line below copyright footer due to base font scaling
* Fixed issue with whitespace in path for scripts
* Fixed #205: support non-fixed (static) header
* Refactored footnote references for better visibility
* Reduced repaints to a minimum for non-tabs configuration
* Reduced contrast of edit button (slightly)
mkdocs-material-1.2.0 (2017-03-03)
* Added quote (synonym: cite) style for Admonition
* Added help message to build pipeline
* Fixed wrong navigation link colors when applying palette
* Fixed #197: Link missing in tabs navigation on deeply nested items
* Removed unnecessary dev dependencies
mkdocs-material-1.1.1 (2017-02-26)
* Fixed incorrectly displayed nested lists when using tabs
mkdocs-material-1.1.0 (2017-02-26)
* Added tabs navigation feature (optional)
* Added Disqus integration (optional)
* Added a high resolution Favicon with the new logo
* Added static type checking using Facebook's Flow
* Fixed #173: Dictionary elements have no bottom spacing
* Fixed #175: Tables cannot be set to 100% width
* Fixed race conditions in build related to asset revisioning
* Fixed accidentally re-introduced Permalink on top-level headline
* Fixed alignment of logo in drawer on IE11
* Refactored styles related to tables
* Refactored and automated Docker build and PyPI release
* Refactored build scripts
mkdocs-material-1.0.5 (2017-02-18)
* Fixed #153: Sidebar flows out of constrained area in Chrome 56
* Fixed #159: Footer jitter due to JavaScript if content is short
mkdocs-material-1.0.4 (2017-02-16)
* Fixed #142: Documentation build errors if h1 is defined as raw HTML
* Fixed #164: PyPI release does not build and install
* Fixed offsets of targeted headlines
* Increased sidebar font size by 0.12rem
mkdocs-material-1.0.3 (2017-01-22)
* Fixed #117: Table of contents items don't blur on fast scrolling

View File

@ -21,17 +21,30 @@
FROM jfloff/alpine-python:2.7-slim
MAINTAINER Martin Donath <martin.donath@squidfunk.com>
# Set working directory
WORKDIR /docs
# Set build directory
WORKDIR /tmp
# Install packages
# Install dependencies
COPY requirements.txt .
RUN \
pip install -r requirements.txt && \
pip install mkdocs-material && \
rm requirements.txt
# Expose MkDocs default port
# Copy files necessary for build
COPY material material
COPY MANIFEST.in MANIFEST.in
COPY package.json package.json
COPY setup.py setup.py
# Perform build and cleanup artifacts
RUN \
python setup.py install && \
rm -rf /tmp/*
# Set working directory
WORKDIR /docs
# Expose MkDocs development server port
EXPOSE 8000
# Start development server by default

View File

@ -20,6 +20,7 @@
* IN THE SOFTWARE.
*/
import chalk from "chalk"
import gulp from "gulp"
import notifier from "node-notifier"
import plumber from "gulp-plumber"
@ -30,28 +31,130 @@ import yargs from "yargs"
* Configuration and arguments
* ------------------------------------------------------------------------- */
/* General configuration */
const config = {
assets: {
src: "src/assets", /* Source directory for assets */
build: "material/assets" /* Target directory for assets */
},
lib: "lib", /* Libraries */
lib: "lib", /* Libraries and tasks */
tests: {
visual: "tests/visual" /* Base directory for visual tests */
},
views: {
src: "src", /* Source directory for views */
build: "material" /* Target directory for views */
}
}
const args = yargs
.default("clean", false) /* Clean before build */
.default("karma", true) /* Karma watchdog */
.default("lint", true) /* Lint sources */
.default("mkdocs", true) /* MkDocs watchdog */
.default("optimize", false) /* Optimize sources */
.default("revision", false) /* Revision assets */
.default("sourcemaps", false) /* Create sourcemaps */
/* Commandline arguments */
let args = yargs
.locale("en")
.usage(`\n${chalk.yellow("Usage:")} yarn run <command> -- [options]`)
.wrap(84)
.updateStrings({
"Commands:": chalk.yellow("Commands:"),
"Examples:": chalk.yellow("Examples:")
})
/* Commands */
.command("build", chalk.grey("build assets and views"))
.command("clean", chalk.grey("clean build artifacts"))
.command("flow", chalk.grey("type check with flow"))
.command("help", chalk.grey("display this message"))
.command("lint", chalk.grey("lint sources"))
.command("start", chalk.grey("start development server"))
.command("test:visual:run", chalk.grey("run visual tests"))
.command("test:visual:session", chalk.grey("start test server"))
.command("test:visual:update", chalk.grey("update reference images"))
/* Options */
.group([
"help", "clean"
], chalk.yellow("Options:"))
.help("help", chalk.grey("display this message"))
.option("clean", {
describe: chalk.grey("clean artifacts before command"),
default: false,
global: true
})
/* Build options */
.group([
"lint", "optimize", "revision", "sourcemaps", "mkdocs"
], chalk.yellow("Build Options:"))
.option("lint", {
describe: chalk.grey("lint sources before build"),
default: true,
global: true
})
.option("optimize", {
describe: chalk.grey("optimize and minify assets"),
default: true,
global: true
})
.option("revision", {
describe: chalk.grey("revision assets for cache busting"),
default: false,
global: true
})
.option("sourcemaps", {
describe: chalk.grey("generate sourcemaps for assets"),
default: false,
global: true
})
.option("mkdocs", {
describe: chalk.grey("build documentation or start watchdog"),
default: true,
global: true
})
/* Test options */
.group([
"grep", "browser"
], chalk.yellow("Test Options:"))
.option("grep", {
describe: chalk.grey("only execute tests matching a regex"),
global: true
})
.option("browser", {
describe: chalk.grey("only execute tests for the given browser"),
global: true
})
/* Example commands */
.example("yarn run build")
.example("yarn run build -- --no-optimize")
.example("yarn run clean")
.example("yarn run flow")
.example("yarn run lint")
.example("yarn run start")
.example("yarn run test:visual:run")
.example("yarn run test:visual:run -- --no-clean")
.example("yarn run test:visual:run -- --grep nav")
.example("yarn run test:visual:run -- --browser ie11")
.example("yarn run test:visual:session")
.example("yarn run test:visual:update")
/* Document Environment variables */
.epilogue(
`${chalk.yellow("Environment:")}\n` +
` SAUCE=${chalk.grey("<true|false)>")}\n` +
` SAUCE_USERNAME=${chalk.grey("<username>")}\n` +
` SAUCE_ACCESS_KEY=${chalk.grey("<key>")}`
)
/* Apply to process.argv */
.argv
/* Only use the last seen value if boolean, so overrides are possible */
args = Object.keys(args).reduce((result, arg) => {
result[arg] = Array.isArray(args[arg]) && typeof args[arg][0] === "boolean"
? [].concat(args[arg]).pop()
: args[arg]
return result
}, {})
/* ----------------------------------------------------------------------------
* Overrides and helpers
* ------------------------------------------------------------------------- */
@ -90,9 +193,16 @@ gulp.src = (...glob) => {
/*
* Helper function to load a task
*
* This function returns a callback that will require the task with the given
* name and execute the function that is returned by this task. It omits the
* need to load all tasks upfront, speeding up the build a gazillion times.
*/
const load = task => {
return require(`./${config.lib}/tasks/${task}`)(gulp, config, args)
return done => {
return require(`./${config.lib}/tasks/${task}`)
.call(gulp, gulp, config, args)(done)
}
}
/* ----------------------------------------------------------------------------
@ -102,26 +212,26 @@ const load = task => {
/*
* Copy favicon
*/
gulp.task("assets:images:build:ico",
gulp.task("assets:images:build:ico", [
args.clean ? "assets:images:clean" : false
].filter(t => t),
load("assets/images/build/ico"))
/*
* Copy and minify vector graphics
*/
gulp.task("assets:images:build:svg",
gulp.task("assets:images:build:svg", [
args.clean ? "assets:images:clean" : false
].filter(t => t),
load("assets/images/build/svg"))
/*
* Copy images
*/
gulp.task("assets:images:build", args.clean ? [
"assets:images:clean"
] : [], () => {
return gulp.start([
gulp.task("assets:images:build", [
"assets:images:build:ico",
"assets:images:build:svg"
])
})
])
/*
* Clean images generated by build
@ -135,30 +245,39 @@ gulp.task("assets:images:clean",
/*
* Build application logic
*
* When revisioning assets, the build must be serialized due to possible race
* conditions when two tasks try to write manifest.json simultaneously
*/
gulp.task("assets:javascripts:build:application",
gulp.task("assets:javascripts:build:application", [
args.clean ? "assets:javascripts:clean" : false,
args.lint ? "assets:javascripts:lint" : false,
args.revision ? "assets:stylesheets:build" : false
].filter(t => t),
load("assets/javascripts/build/application"))
/*
* Build custom modernizr
*
* When revisioning assets, the build must be serialized due to possible race
* conditions when two tasks try to write manifest.json simultaneously
*/
gulp.task("assets:javascripts:build:modernizr", [
"assets:stylesheets:build"
], load("assets/javascripts/build/modernizr"))
"assets:stylesheets:build",
args.clean ? "assets:javascripts:clean" : false,
args.lint ? "assets:javascripts:lint" : false,
args.revision ? "assets:javascripts:build:application" : false
].filter(t => t),
load("assets/javascripts/build/modernizr"))
/*
* Build application logic and modernizr
* Build application logic and Modernizr
*/
gulp.task("assets:javascripts:build", (args.clean ? [
"assets:javascripts:clean"
] : []).concat(args.lint ? [
"assets:javascripts:lint"
] : []), () => {
return gulp.start([
gulp.task("assets:javascripts:build", [
"assets:javascripts:build:application",
"assets:javascripts:build:modernizr"
])
})
])
/*
* Clean JavaScript generated by build
@ -166,6 +285,12 @@ gulp.task("assets:javascripts:build", (args.clean ? [
gulp.task("assets:javascripts:clean",
load("assets/javascripts/clean"))
/*
* Annotate JavaScript
*/
gulp.task("assets:javascripts:annotate",
load("assets/javascripts/annotate"))
/*
* Lint JavaScript
*/
@ -179,11 +304,10 @@ gulp.task("assets:javascripts:lint",
/*
* Build stylesheets from SASS source
*/
gulp.task("assets:stylesheets:build", (args.clean ? [
"assets:stylesheets:clean"
] : []).concat(args.lint ? [
"assets:stylesheets:lint"
] : []),
gulp.task("assets:stylesheets:build", [
args.clean ? "assets:stylesheets:clean" : false,
args.lint ? "assets:stylesheets:lint" : false
].filter(t => t),
load("assets/stylesheets/build"))
/*
@ -227,13 +351,14 @@ gulp.task("assets:clean", [
/*
* Minify views
*/
gulp.task("views:build", (args.revision ? [
"assets:images:build",
"assets:stylesheets:build",
"assets:javascripts:build"
] : []).concat(args.clean ? [
"views:clean"
] : []), load("views/build"))
gulp.task("views:build", [
args.clean ? "views:clean" : false,
args.revision ? "assets:images:build" : false,
args.revision ? "assets:stylesheets:build" : false,
args.revision ? "assets:javascripts:build" : false
].filter(t => t),
load("views/build"))
/*
* Clean views
@ -267,14 +392,44 @@ gulp.task("mkdocs:serve",
load("mkdocs/serve"))
/* ----------------------------------------------------------------------------
* Tests
* Visual tests
* ------------------------------------------------------------------------- */
/*
* Start karma test runner
* Generate visual tests
*/
gulp.task("tests:unit:watch",
load("tests/unit/watch"))
gulp.task("tests:visual:generate", [
args.clean ? "tests:visual:clean" : false,
args.clean ? "assets:build" : false,
args.clean ? "views:build" : false
].filter(t => t),
load("tests/visual/generate"))
/*
* Run visual tests
*/
gulp.task("tests:visual:run", [
"tests:visual:generate"
], load("tests/visual/run"))
/*
* Update reference images for visual tests
*/
gulp.task("tests:visual:update",
load("tests/visual/update"))
/*
* Clean files generated by visual tests
*/
gulp.task("tests:visual:clean",
load("tests/visual/clean"))
/*
* Open a SauceConnect session for manual testing
*/
gulp.task("tests:visual:session", [
"tests:visual:generate"
], load("tests/visual/session"))
/* ----------------------------------------------------------------------------
* Interface
@ -285,10 +440,9 @@ gulp.task("tests:unit:watch",
*/
gulp.task("build", [
"assets:build",
"views:build"
].concat(args.mkdocs
? "mkdocs:build"
: []))
"views:build",
args.mkdocs ? "mkdocs:build" : false
].filter(f => f))
/*
* Clean assets and documentation
@ -312,10 +466,6 @@ gulp.task("watch", [
if (args.mkdocs)
gulp.start("mkdocs:serve")
/* Start karma test runner */
// if (args.karma)
// gulp.start("tests:unit:watch")
/* Rebuild stylesheets */
gulp.watch([
`${config.assets.src}/stylesheets/**/*.scss`
@ -337,6 +487,11 @@ gulp.task("watch", [
], ["views:build"])
})
/*
* Print help message
*/
gulp.task("help")
/*
* Build assets by default
*/

View File

@ -3,3 +3,4 @@ recursive-exclude site *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
include LICENSE
include package.json

View File

@ -1,21 +1,17 @@
[![Travis][travis-image]][travis-link]
[![Dependencies][deps-image]][deps-link]
[![Codacy][codacy-image]][codacy-link]
[![Docker][docker-image]][docker-link]
[![PyPI][pypi-image]][pypi-link]
[travis-image]: https://travis-ci.org/squidfunk/mkdocs-material.svg
[travis-image]: https://travis-ci.org/squidfunk/mkdocs-material.svg?branch=master
[travis-link]: https://travis-ci.org/squidfunk/mkdocs-material
[deps-image]: https://david-dm.org/squidfunk/mkdocs-material/dev-status.svg
[deps-link]: https://david-dm.org/squidfunk/mkdocs-material?type=dev
[codacy-image]: https://api.codacy.com/project/badge/Grade/fe07aa1fa91d453cb69711d3885c5d7e
[codacy-link]: https://www.codacy.com/app/squidfunk/mkdocs-material?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=squidfunk/mkdocs-material&amp;utm_campaign=Badge_Grade
[docker-image]: https://img.shields.io/docker/pulls/squidfunk/mkdocs-material.svg
[docker-image]: https://img.shields.io/docker/automated/squidfunk/mkdocs-material.svg
[docker-link]: https://hub.docker.com/r/squidfunk/mkdocs-material/
[pypi-image]: https://img.shields.io/pypi/v/mkdocs-material.svg
[pypi-link]: https://pypi.python.org/pypi/mkdocs-material
# Material for MkDocs
A Material Design theme for [MkDocs](http://www.mkdocs.org).

View File

@ -4,8 +4,8 @@
Project documentation is as diverse as the projects themselves and the Material
theme is a good starting point for making it look great. However, as you write
your documentation, you may reach some point where some small adjustments are
necessary to preserve the style.
your documentation, you may reach a point where some small adjustments are
necessary to preserve the desired style.
## Adding assets
@ -98,6 +98,7 @@ The directory layout of the Material theme is as follows:
│ ├─ javascripts/ # JavaScript
│ └─ stylesheets/ # Stylesheets
├─ partials/
│ ├─ disqus.html # Disqus integration
│ ├─ footer.html # Footer bar
│ ├─ header.html # Header bar
│ ├─ language.html # Localized labels
@ -106,6 +107,8 @@ The directory layout of the Material theme is as follows:
│ ├─ search.html # Search box
│ ├─ social.html # Social links
│ ├─ source.html # Repository information
│ ├─ tabs-item.html # Tabs navigation item
│ ├─ tabs.html # Tabs navigation
│ ├─ toc-item.html # Table of contents item
│ └─ toc.html # Table of contents
├─ 404.html # 404 error page
@ -141,6 +144,7 @@ The Material theme provides the following template blocks:
| ------------ | ----------------------------------------------- |
| `analytics` | Wraps the Google Analytics integration |
| `content` | Wraps the main content |
| `disqus` | Wraps the disqus integration |
| `extrahead` | Empty block to define additional meta tags |
| `fonts` | Wraps the webfont definitions |
| `footer` | Wraps the footer with navigation and copyright |
@ -149,6 +153,7 @@ The Material theme provides the following template blocks:
| `libs` | Wraps the JavaScript libraries, e.g. Modernizr |
| `repo` | Wraps the repository link in the header bar |
| `scripts` | Wraps the JavaScript application logic |
| `source` | Wraps the linked source files |
| `search_box` | Wraps the search form in the header bar |
| `site_meta` | Wraps the meta tags in the document head |
| `site_name` | Wraps the site name in the header bar |
@ -174,7 +179,8 @@ theme and recompile it. This is fairly easy.
### Environment setup
In order to start development on the Material theme, a [Node.js][8] version of
at least 4 is required. Clone the repository from GitHub:
at least 5 is required, as well as the package manager [yarn][9] which is a
better version of `npm`. First, clone the repository:
``` sh
git clone https://github.com/squidfunk/mkdocs-material
@ -185,23 +191,24 @@ Next, all dependencies need to be installed, which is done with:
``` sh
cd mkdocs-material
pip install -r requirements.txt
npm install
yarn install
```
[8]: https://nodejs.org
[9]: https://yarnpkg.com/
### Development mode
The Material theme uses a sophisticated asset pipeline using [Gulp][9] and
The Material theme uses a sophisticated asset pipeline using [Gulp][10] and
Webpack which can be started with the following command:
``` sh
npm start
yarn start
```
This will also start the MkDocs development server which will monitor changes
on assets, templates and documentation. Point your browser to
[localhost:8000][10] and you should see this documentation in front of you.
[localhost:8000][11] and you should see this documentation in front of you.
For example, changing the color palette is as simple as changing the
`$md-color-primary` and `$md-color-accent` variables in
@ -218,21 +225,21 @@ $md-color-accent: $clr-teal-a700;
directory are automatically generated from the `src` directory and will be
overriden when the theme is built.
[9]: http://gulpjs.com
[10]: http://localhost:8000
[10]: http://gulpjs.com
[11]: http://localhost:8000
### Build process
When you finished making your changes, you can build the theme by invoking:
When you've finished making your changes, you can build the theme by invoking:
``` sh
npm run build
yarn run build
```
This triggers the production-level compilation and minification of all
stylesheets and JavaScript sources. When the command is ready, the final
theme is located in the `material` directory. Add the `theme_dir` variable
pointing to the aforementioned directory in your original `mkdocs.yml`.
stylesheets and JavaScript sources. When the command exits, the final theme is
located in the `material` directory. Add the `theme_dir` variable pointing to
the aforementioned directory in your original `mkdocs.yml`.
Now you can run `mkdocs build` and you should see your documentation with your
changes to the original Material theme.

View File

@ -331,3 +331,27 @@ Result:
Qualifiers:
* `bug`
### Quote
Example:
``` markdown
!!! quote
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
massa, nec semper lorem quam in massa.
```
Result:
!!! quote
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
massa, nec semper lorem quam in massa.
Qualifiers:
* `quote`
* `cite`

View File

@ -4,13 +4,21 @@
and is included in the standard Markdown library. The highlighting process is
executed during compilation of the Markdown file.
!!! failure "Syntax highlighting not working?"
Please ensure that [Pygments][2] is installed. See the next section for
further directions on how to set up Pygments or use the official
[Docker image][3] with all dependencies pre-installed.
[1]: https://pythonhosted.org/Markdown/extensions/code_hilite.html
[2]: http://pygments.org
[3]: https://hub.docker.com/r/squidfunk/mkdocs-material/
## Installation
CodeHilite parses code blocks and wraps them in `<pre>` tags. If [Pygments][2]
CodeHilite parses code blocks and wraps them in `pre` tags. If [Pygments][2]
is installed, which is a generic syntax highlighter with support for over
[300 languages][3], CodeHilite will also highlight the code block. Pygments can
[300 languages][4], CodeHilite will also highlight the code block. Pygments can
be installed with the following command:
``` sh
@ -32,8 +40,7 @@ markdown_extensions:
that and defines styles for the default `.codehilite` class, so the part
`css_class=code` needs to be removed.
[2]: http://pygments.org
[3]: http://pygments.org/languages
[4]: http://pygments.org/languages
## Usage

View File

@ -0,0 +1,99 @@
path: tree/master/docs/extensions
source: metadata.md
# Metadata
The [Metadata][1] extension makes it possible to add metadata to a document
which gives more control over the theme in a page-specific context.
[1]: https://pythonhosted.org/Markdown/extensions/meta_data.html
## Installation
Add the following lines to your `mkdocs.yml`:
``` yaml
markdown_extensions:
- meta
```
## Usage
Metadata is written as a series of key-value pairs at the beginning of the
Markdown document, delimited by a blank line which ends the metadata context.
Naturally, the metadata is stripped from the document before rendering the
actual page content and made available to the theme.
Example:
``` markdown
title: Lorem ipsum dolor sit amet
description: Nullam urna elit, malesuada eget finibus ut, ac tortor.
path: path/to/file
source: file.js
# Headline
...
```
See the next section which covers the metadata that is supported by Material.
### Overriding the title
The page title can be overridden on a per-document level:
``` markdown
title: Lorem ipsum dolor sit amet
```
This will set the `title` tag inside the document `head` for the current page
to the provided value. It will also override the default behavior of Material
for MkDocs which appends the site title using a dash as a separator to the page
title.
### Overriding the description
The page description can also be overridden on a per-document level:
``` yaml
description: Nullam urna elit, malesuada eget finibus ut, ac tortor.
```
This will set the `meta` tag containing the site description inside the
document `head` for the current page to the provided value.
### Linking sources
When a document is related to a specific set of source files and the `repo_url`
is defined inside the project's `mkdocs.yml`, the files can be linked using the
`source` key:
``` markdown
source: file.js
```
A new entry at the bottom of the table of contents is generated that is linking
to the section listing the linked source files. Multiple files can be linked by
adding filenames on separate lines:
``` markdown
source: file.js
file.css
```
The filenames are appended to the `repo_url` set in your `mkdocs.yml`, but can
be prefixed with a `path` to ensure correct path resolving:
Example:
``` markdown
path: tree/master/docs/extensions
source: metadata.md
```
Result:
See the [source][2] section for the resulting output.
[2]: #__source

View File

@ -6,7 +6,9 @@
The official [Docker image][1] for Material comes with all dependencies
pre-installed and ready-to-use with the latest version published on PyPI,
packaged in a very small image (27MB compressed).
packaged in a very small image (28MB compressed).
[1]: https://hub.docker.com/r/squidfunk/mkdocs-material/
### Installing MkDocs
@ -40,7 +42,6 @@ pip install pygments
pip install pymdown-extensions
```
[1]: https://hub.docker.com/r/squidfunk/mkdocs-material/
[2]: http://www.mkdocs.org
[3]: http://pygments.org
[4]: http://facelessuser.github.io/pymdown-extensions/
@ -55,20 +56,6 @@ Material can be installed with `pip`:
pip install mkdocs-material
```
!!! warning "Installation on macOS"
When you're running the pre-installed version of Python on macOS, `pip`
tries to install packages in a folder for which your user might not have
the adequate permissions. There are two possible solutions to this:
1. **Installing in user space** (recommended): Provide the `--user` flag
to the install command and `pip` will install the package in a user-site
location. This is the recommended way.
2. **Switching to a homebrewed Python**: Upgrade your Python installation
to a self-contained solution by installing Python with Homebrew. This
should eliminate a lot of problems you may be having with `pip`.
#### using choco
If you're on Windows you can use [Chocolatey][5] to install [Material][6]:
@ -98,6 +85,29 @@ This is especially useful if you want to extend the theme and override some
parts of the theme. The theme will reside in the folder
`mkdocs-material/material`.
### Troubleshooting
!!! warning "Installation on macOS"
When you're running the pre-installed version of Python on macOS, `pip`
tries to install packages in a folder for which your user might not have
the adequate permissions. There are two possible solutions for this:
1. **Installing in user space** (recommended): Provide the `--user` flag
to the install command and `pip` will install the package in a user-site
location. This is the recommended way.
2. **Switching to a homebrewed Python**: Upgrade your Python installation
to a self-contained solution by installing Python with Homebrew. This
should eliminate a lot of problems you may be having with `pip`.
!!! failure "Error: unrecognized theme 'material'"
If you run into this error, the most common reason is that you installed
MkDocs through some package manager (e.g. Homebrew or `apt-get`) and the
Material theme through `pip`, so both packages end up in different
locations. MkDocs only checks it's install location for themes.
## Usage
In order to enable the Material theme just add one of the following lines to
@ -113,7 +123,7 @@ If you cloned Material from GitHub:
theme_dir: 'mkdocs-material/material'
```
MkDocs includes a development server, so you can view your changes as you go.
MkDocs includes a development server, so you can review your changes as you go.
The development server can be started with the following command:
``` sh
@ -129,7 +139,7 @@ read on and customize the theme through some options.
## Options
The Material theme adds some extra variables for configuration via your
project's `mkdocs.yml`. See the following section for all available options.
project's `mkdocs.yml`. See the following sections for all available options.
### Changing the color palette
@ -238,17 +248,44 @@ extra:
The text font will be loaded in font-weights 400 and **700**, the `monospaced`
font in regular weight. If you want to load fonts from other destinations or
don't want to use the Google Fonts loading magic, just set `font` to `'none'`:
don't want to use the Google Fonts loading magic, just set `font` to `false`:
``` yaml
extra:
font: 'none'
font: false
```
[12]: https://fonts.google.com/specimen/Roboto
[13]: https://fonts.google.com/
[13]: https://fonts.google.com
[14]: https://fonts.google.com/specimen/Ubuntu
### Adding a source repository
To include a link to the repository of your project within your documentation,
set the following variables via your project's `mkdocs.yml`:
``` yaml
repo_name: 'my-github-handle/my-project'
repo_url: 'https://github.com/my-github-handle/my-project'
```
Material will render the name of the repository next to the search bar on
big screens and as part of the main navigation drawer on smaller screen sizes.
Furthermore, if `repo_url` points to a GitHub, BitBucket or GitLab repository,
the respective service logo will be shown next to the name of the repository.
Additionally, for GitHub, the number of stars and forks is shown.
!!! warning "Why is there an edit button at the top of every article?"
If the `repo_url` is set to a GitHub or BitBucket repository, and the
`repo_name` is set to *GitHub* or *BitBucket* (implied by default), an
edit button will appear at the top of every article. This is the automatic
behavior that MkDocs implements. See the [MkDocs documentation][15] on more
guidance regarding the `edit_uri` attribute, which defines whether the edit
button is show or not.
[15]: http://www.mkdocs.org/user-guide/configuration/#edit_uri
### Adding a logo
Material makes it easy to add your logo. Your logo should have rectangular
@ -266,7 +303,7 @@ extra:
If you want to link your social accounts, the Material theme provides an easy
way for doing this in the footer of the documentation using the automatically
included [FontAwesome][15] webfont. The syntax is simple the `type` must
included [FontAwesome][16] webfont. The syntax is simple the `type` must
denote the name of the social service, e.g. `github`, `twitter` or `linkedin`
and the `link` must contain the URL you want to link to:
@ -285,7 +322,7 @@ The links are generated in order and the `type` of the links must match the
name of the FontAwesome glyph. The `fa` is automatically added, so `github`
will result in `fa fa-github`.
[15]: http://fontawesome.io/icons/
[16]: http://fontawesome.io/icons/
### Google Analytics integration
@ -300,17 +337,36 @@ google_analytics:
- 'auto'
```
### Localization <small>L10N</small>
### Disqus integation
In order to localize the labels (e.g. *Previous* and *Next* in the footer),
you can override the file `partials/language.html` to provide your own
translations inside the macro `t`:
Material for MkDocs is integrated with [Disqus][17], so if you want to add a
comments section to your documentation set the shortname of your Disqus project
in your `mkdocs.yml`:
``` yaml
extra:
disqus: 'your-disqus-shortname'
```
A new entry at the bottom of the table of contents is generated that is linking
to the comments section. The necessary JavaScript is automatically included.
[17]: https://disqus.com
### Localization
Material for MkDocs supports internationalization (i18n). In order to translate
the labels (e.g. *Previous* and *Next* in the footer), you can override the
file `partials/language.html` and provide your own translations inside the
macro `t`:
``` jinja
{% macro t(key) %}{{ {
"edit.link.title": "Edit this page",
"footer.previous": "Previous",
"footer.next": "Next",
"meta.comments": "Comments",
"meta.source": "Source",
"search.placeholder": "Search",
"source.link.title": "Go to repository",
"toc.title": "Table of contents"
@ -318,26 +374,35 @@ translations inside the macro `t`:
```
Just copy the file from the original theme and make your adjustments. See the
section on [overriding partials][16] in the customization guide.
section on [overriding partials][18] and the general guide on
[theme extension][19] in the customization guide.
!!! warning "Migrating from Material 0.2.x"
[18]: customization.md#overriding-partials
[19]: customization.md#extending-the-theme
In 0.2.x localization was done within the `extra` configuration of your
`mkdocs.yml`. With 1.0.0 this is no longer possible as the configuration
will be ignored.
### Tabs
[16]: customization.md#overriding-partials
From version 1.1.0 on, Material supports another layer on top of the main
navigation for larger screens in the form of tabs. This is especially useful
for larger documentation projects with a few top-level sections. Tabs can be
enabled by setting the respective feature flag to true:
``` yaml
extra:
feature:
tabs: true
```
### More advanced customization
If you want to change the general appearance of the Material theme, see
[this article][17] for more information on advanced customization.
[this article][20] for more information on advanced customization.
[17]: customization.md
[20]: customization.md
## Extensions
MkDocs supports several [Markdown extensions][18]. The following extensions
MkDocs supports several [Markdown extensions][21]. The following extensions
are not enabled by default (see the link for which are enabled by default)
but highly recommended, so they should be switched on at all times:
@ -351,18 +416,20 @@ markdown_extensions:
For more information, see the following list of extensions supported by the
Material theme including more information regarding installation and usage:
* [Admonition][19]
* [Codehilite][20]
* [Permalinks][21]
* [Footnotes][22]
* [PyMdown Extensions][23]
* [Admonition][22]
* [Codehilite][23]
* [Footnotes][24]
* [Metadata][25]
* [Permalinks][26]
* [PyMdown Extensions][27]
[18]: http://www.mkdocs.org/user-guide/writing-your-docs/#markdown-extensions
[19]: extensions/admonition.md
[20]: extensions/codehilite.md
[21]: extensions/permalinks.md
[22]: extensions/footnotes.md
[23]: extensions/pymdown.md
[21]: http://www.mkdocs.org/user-guide/writing-your-docs/#markdown-extensions
[22]: extensions/admonition.md
[23]: extensions/codehilite.md
[24]: extensions/footnotes.md
[25]: extensions/metadata.md
[26]: extensions/permalinks.md
[27]: extensions/pymdown.md
## Full example
@ -376,11 +443,11 @@ site_author: 'John Doe'
site_url: 'https://my-github-handle.github.io/my-project'
# Repository
repo_name: 'GitHub'
repo_name: 'my-github-handle/my-project'
repo_url: 'https://github.com/my-github-handle/my-project'
# Copyright
copyright: 'Copyright &copy; 2016 John Doe'
copyright: 'Copyright &copy; 2016 - 2017 John Doe'
# Documentation and theme
theme: 'material'
@ -396,11 +463,11 @@ extra:
code: 'Roboto Mono'
social:
- type: 'github'
link: 'https://github.com/squidfunk'
link: 'https://github.com/john-doe'
- type: 'twitter'
link: 'https://twitter.com/squidfunk'
link: 'https://twitter.com/jonh-doe'
- type: 'linkedin'
link: 'https://de.linkedin.com/in/martin-donath-20a95039'
link: 'https://de.linkedin.com/in/john-doe'
# Google Analytics
google_analytics:
@ -411,7 +478,5 @@ google_analytics:
markdown_extensions:
- admonition
- codehilite(guess_lang=false)
- footnotes
- meta
- toc(permalink=true)
```

View File

@ -2,7 +2,7 @@
**MIT License**
Copyright &copy; 2016 Martin Donath
Copyright &copy; 2016 - 2017 Martin Donath
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to

View File

@ -12,11 +12,77 @@ To determine the currently installed version, use the following command:
``` sh
pip show mkdocs-material | grep -E ^Version
# Version 1.0.3
# Version 1.3.0
```
## Changelog
### 1.3.0 <small> _ March 11, 2017</small>
* Added support for page-specific title and description using metadata
* Added support for linking source files to documentation
* Fixed jitter and offset of sidebar when zooming browser
* Fixed incorrectly initialized tablet sidebar height
* Fixed regression for [#1][1]: GitHub stars break if repo_url ends with a `/`
* Fixed undesired white line below copyright footer due to base font scaling
* Fixed issue with whitespace in path for scripts
* Fixed [#205][205]: support non-fixed (static) header
* Refactored footnote references for better visibility
* Reduced repaints to a minimum for non-tabs configuration
* Reduced contrast of edit button (slightly)
[205]: https://github.com/squidfunk/mkdocs-material/issues/197
### 1.2.0 <small> _ March 3, 2017</small>
* Added `quote` (synonym: `cite`) style for Admonition
* Added help message to build pipeline
* Fixed wrong navigation link colors when applying palette
* Fixed [#197][197]: Link missing in tabs navigation on deeply nested items
* Removed unnecessary dev dependencies
[197]: https://github.com/squidfunk/mkdocs-material/issues/197
### 1.1.1 <small> _ February 26, 2017</small>
* Fixed incorrectly displayed nested lists when using tabs
### 1.1.0 <small> _ February 26, 2017</small>
* Added tabs navigation feature (optional)
* Added Disqus integration (optional)
* Added a high resolution Favicon with the new logo
* Added static type checking using Facebook's Flow
* Fixed [#173][173]: Dictionary elements have no bottom spacing
* Fixed [#175][175]: Tables cannot be set to 100% width
* Fixed race conditions in build related to asset revisioning
* Fixed accidentally re-introduced Permalink on top-level headline
* Fixed alignment of logo in drawer on IE11
* Refactored styles related to tables
* Refactored and automated Docker build and PyPI release
* Refactored build scripts
[173]: https://github.com/squidfunk/mkdocs-material/issues/173
[175]: https://github.com/squidfunk/mkdocs-material/issues/175
### 1.0.5 <small> _ February 18, 2017</small>
* Fixed [#153][153]: Sidebar flows out of constrained area in Chrome 56
* Fixed [#159][159]: Footer jitter due to JavaScript if content is short
[153]: https://github.com/squidfunk/mkdocs-material/issues/153
[159]: https://github.com/squidfunk/mkdocs-material/issues/159
### 1.0.4 <small> _ February 16, 2017</small>
* Fixed [#142][142]: Documentation build errors if `h1` is defined as raw HTML
* Fixed [#164][164]: PyPI release does not build and install
* Fixed offsets of targeted headlines
* Increased sidebar font size by `0.12rem`
[142]: https://github.com/squidfunk/mkdocs-material/issues/142
[164]: https://github.com/squidfunk/mkdocs-material/issues/164
### 1.0.3 <small> _ January 22, 2017</small>
* Fixed [#117][117]: Table of contents items don't blur on fast scrolling

View File

@ -137,6 +137,24 @@ tincidunt. Aenean ullamcorper sit amet nulla at interdum.
sagittis. Aliquam purus tellus, faucibus eget urna at, iaculis venenatis
nulla. Vivamus a pharetra leo.
### Definition lists
Lorem ipsum dolor sit amet
: Sed sagittis eleifend rutrum. Donec vitae suscipit est. Nullam tempus
tellus non sem sollicitudin, quis rutrum leo facilisis. Nulla tempor
lobortis orci, at elementum urna sodales vitae. In in vehicula nulla.
Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis.
Nam vulputate tincidunt fringilla.
Nullam dignissim ultrices urna non auctor.
Cras arcu libero
: Aliquam metus eros, pretium sed nulla venenatis, faucibus auctor ex. Proin
ut eros sed sapien ullamcorper consequat. Nunc ligula ante, fringilla at
aliquam ac, aliquet sed mauris.
## Code blocks
### Inline

6
lib/.eslintrc Normal file
View File

@ -0,0 +1,6 @@
{
"rules": {
"no-invalid-this": 0,
"max-params": 0
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
declare module "fastclick" {
/* Type: FastClick */
declare type FastClick = {
attach(name: HTMLElement): void
}
/* Exports */
declare export default FastClick
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
declare module "js-cookie" {
/* Type: Options for setting cookie values */
declare type Options = {
path?: string,
expires?: number | string
}
/* Type: Cookie */
declare type Cookie = {
getJSON(json: string): Object,
set(key: string, value: string, options?: Options): string
}
/* Exports */
declare export default Cookie
}

36
lib/declarations/jsx.js Normal file
View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
declare class Jsx {
static createElement(tag: string, properties?: Object,
...children?: Array<
string | number | { __html: string } | Array<HTMLElement>
>
): HTMLElement
}
/* Exports */
declare export default Jsx

34
lib/declarations/lunr.js Normal file
View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
/*
* Currently, it's not possible to export a function that returns a class type,
* as the imports just don't correctly work with flow. As a workaround we
* export an object until this error is fixed.
*/
declare module "lunr" {
declare function exports(name: () => void): Object
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Declarations
* ------------------------------------------------------------------------- */
declare class Modernizr {
static addTest(name: string, test: () => boolean): void
}
/* Exports */
declare export default Modernizr

View File

@ -21,18 +21,17 @@
*/
/* ----------------------------------------------------------------------------
* Definition
* Module
* ------------------------------------------------------------------------- */
/* eslint-disable no-underscore-dangle */
export default /* JSX */ {
/**
* Create a native DOM node from JSX's intermediate representation
*
* @param {string} tag - Tag name
* @param {object} properties - Properties
* @param {?Object} properties - Properties
* @param {...(string|number|Array)} children - Child nodes
* @return {HTMLElement} Native DOM node
*/

65
lib/servers/ecstatic.js Normal file
View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import ecstatic from "ecstatic"
import * as http from "http"
/* ----------------------------------------------------------------------------
* Locals
* ------------------------------------------------------------------------- */
/* Static file server */
let server = null
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Start static file server
*
* @param {string} directory - Directory to serve
* @param {number} port - Port to listen on
* @param {Function} done - Resolve callback
*/
export const start = (directory, port, done) => {
server = http.createServer(ecstatic({
root: directory
}))
/* Listen and register signal handlers */
server.listen(port, "127.0.0.1", done)
for (const signal of ["SIGTERM", "SIGINT", "exit"])
process.on(signal, stop)
}
/**
* Stop static file server
*
* @param {Function} done - Resolve callback
*/
export const stop = done => {
if (server) {
server.close(done)
server = null
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import launcher from "sauce-connect-launcher"
/* ----------------------------------------------------------------------------
* Locals
* ------------------------------------------------------------------------- */
/* SauceConnect process */
let server = null
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Open SauceConnect tunnel
*
* @param {string} id - Unique identifier
* @param {string} username - SauceConnect username
* @param {string} accesskey - SauceConnect accesskey
* @param {Function} done - Resolve callback
*/
export const start = (id, username, accesskey, done) => {
launcher({
username,
accessKey: accesskey,
tunnelIdentifier: id
}, (err, proc) => {
if (err)
throw new Error(err)
server = proc
done()
})
/* Register signal handlers */
for (const signal of ["SIGTERM", "SIGINT", "exit"])
process.on(signal, stop)
}
/**
* Close SauceConnect tunnel
*
* @param {Function} done - Resolve callback
*/
export const stop = done => {
if (server) {
server.close(done)
server = null
}
}

View File

@ -33,17 +33,31 @@ let server = null
* Definition
* ------------------------------------------------------------------------- */
/**
* Start Selenium
*
* @param {Function} done - Resolve callback
*/
export const start = done => {
selenium.start({}, (err, proc) => {
/* Register signal handlers */
for (const signal of ["SIGTERM", "SIGINT", "exit"])
process.on(signal, stop)
if (err) {
/* Install selenium, if not present */
if (/^Missing(.*)chromedriver$/.test(err.message)) {
selenium.install(done)
new Promise(resolve => {
selenium.install({}, resolve)
})
/* Start selenium again */
.then(() => {
selenium.start({}, (err_, proc_) => {
server = proc_
done()
})
})
/* Otherwise, throw error */
@ -53,20 +67,21 @@ export const start = done => {
}
/* Remember process handle */
server = server || proc
server = proc
done()
})
}
export const stop = () => {
if (server)
/**
* Stop Selenium
*
* @param {Function} done - Resolve callback
*/
export const stop = done => {
if (server) {
if (typeof done === "function")
server.on("exit", done)
server.kill()
server = null
}
}
/* ----------------------------------------------------------------------------
* Signal handler
* ------------------------------------------------------------------------- */
/* Register signal handler for all relevant events */
for (const signal of ["SIGTERM", "SIGINT", "exit"])
process.on(signal, stop)

View File

@ -1,5 +0,0 @@
{
"rules": {
"no-invalid-this": 0
}
}

View File

@ -28,7 +28,7 @@ import changed from "gulp-changed"
export default (gulp, config) => {
return () => {
return gulp.src(`${config.assets.src}/images/**/*.ico`)
return gulp.src(`${config.assets.src}/images/**/favicon.*`)
.pipe(changed(`${config.assets.build}/images`))
.pipe(gulp.dest(`${config.assets.build}/images`))
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { transform } from "babel-core"
import jsdoc2flow from "flow-jsdoc"
import through from "through2"
/* ----------------------------------------------------------------------------
* Task: annotate JavaScript
* ------------------------------------------------------------------------- */
export default (gulp, config) => {
return () => {
return gulp.src(`${config.assets.src}/javascripts/**/*.{js,jsx}`)
/* Linting */
.pipe(
through.obj(function(file, enc, done) {
if (file.isNull() || file.isStream())
return done()
/* Perform Babel transformation to resolve JSX calls */
const transformed = transform(file.contents.toString(), {
plugins: [
["transform-react-jsx", {
"pragma": "Jsx.createElement"
}]
]
})
/* Annotate contents */
file.contents = new Buffer(jsdoc2flow(
`/* @flow */\n\n${transformed.code}`
).toString())
/* Push file to next stage */
this.push(file)
done()
}))
/* Print errors */
.pipe(gulp.dest("tmp/assets/javascripts"))
}
}

View File

@ -50,12 +50,13 @@ export default (gulp, config, args) => {
],
output: {
filename: "application.js",
library: "Application"
library: "app",
libraryTarget: "window"
},
module: {
/* Transpile ES6 to ES5 with Babel */
loaders: [
rules: [
{
loader: "babel-loader",
test: /\.jsx?$/
@ -65,11 +66,11 @@ export default (gulp, config, args) => {
plugins: [
/* Don't emit assets that include errors */
new webpack.NoErrorsPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
/* Provide JSX helper */
new webpack.ProvidePlugin({
JSX: path.join(process.cwd(), `${config.lib}/providers/jsx.js`)
Jsx: path.join(process.cwd(), `${config.lib}/providers/jsx.js`)
})
].concat(
@ -77,19 +78,30 @@ export default (gulp, config, args) => {
args.optimize ? [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
warnings: false,
screw_ie8: true, // eslint-disable-line camelcase
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true, // eslint-disable-line camelcase
evaluate: true,
if_return: true, // eslint-disable-line camelcase
join_vars: true // eslint-disable-line camelcase
},
output: {
comments: false
}
})
] : []),
/* Module resolver */
resolve: {
modulesDirectories: [
modules: [
"src/assets/javascripts",
"node_modules"
],
extensions: [
"",
".js",
".jsx"
]
@ -101,8 +113,8 @@ export default (gulp, config, args) => {
},
/* Sourcemap support */
devtool: args.sourcemaps ? "source-map" : ""
}))
devtool: args.sourcemaps ? "inline-source-map" : ""
}, webpack))
/* Revisioning */
.pipe(gulpif(args.revision, rev()))

View File

@ -23,6 +23,7 @@
import path from "path"
import through from "through2"
import util from "gulp-util"
import { CLIEngine } from "eslint"
/* ----------------------------------------------------------------------------
@ -38,7 +39,7 @@ const format = eslint.getFormatter()
export default (gulp, config) => {
return () => {
return gulp.src(`${config.assets.src}/javascripts/**/*.js`)
return gulp.src(`${config.assets.src}/javascripts/**/*.{js,jsx}`)
/* Linting */
.pipe(

View File

@ -25,6 +25,7 @@ import gulpif from "gulp-if"
import mincss from "gulp-cssnano"
import mqpacker from "css-mqpacker"
import postcss from "gulp-postcss"
import pseudoclasses from "postcss-pseudo-classes"
import rev from "gulp-rev"
import sass from "gulp-sass"
import sourcemaps from "gulp-sourcemaps"
@ -54,7 +55,11 @@ export default (gulp, config, args) => {
postcss([
autoprefixer(),
mqpacker
]))
].concat(!args.optimize ? [
pseudoclasses({
"restrictTo": ["hover", "focus"]
})
] : [])))
/* Minify sources */
.pipe(gulpif(args.optimize, mincss()))
@ -63,7 +68,7 @@ export default (gulp, config, args) => {
.pipe(gulpif(args.revision, rev()))
.pipe(gulpif(args.revision,
version({ manifest: gulp.src("manifest.json") })))
.pipe(gulpif(args.sourcemaps, sourcemaps.write(".")))
.pipe(gulpif(args.sourcemaps, sourcemaps.write()))
.pipe(gulp.dest(`${config.assets.build}/stylesheets`))
.pipe(gulpif(args.revision,
rev.manifest("manifest.json", {

View File

@ -39,7 +39,7 @@ export default () => {
server.kill()
/* Spawn MkDocs server */
server = child.spawn("mkdocs", ["serve", "-a", "0.0.0.0:8000"], {
server = child.spawn("mkdocs", ["serve", "--dev-addr", "0.0.0.0:8000"], {
stdio: "inherit"
})
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import clean from "del"
import vinyl from "vinyl-paths"
/* ----------------------------------------------------------------------------
* Task: clean files generated by visual tests
* ------------------------------------------------------------------------- */
export default (gulp, config) => {
return () => {
return gulp.src([
`${config.tests.visual}/data`,
"./gemini-report"
])
.pipe(vinyl(clean))
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import child from "child_process"
import path from "path"
import through from "through2"
import util from "gulp-util"
/* ----------------------------------------------------------------------------
* Task: generate visual tests
* ------------------------------------------------------------------------- */
export default (gulp, config) => {
const theme = path.resolve(process.cwd(), config.views.build)
return () => {
return gulp.src(`${config.tests.visual}/suites/**/mkdocs.yml`)
.pipe(
through.obj(function(file, enc, done) {
if (file.isNull() || file.isStream())
return done()
/* Resolve test name and destination */
const name = path.relative(`${config.tests.visual}/suites`,
path.dirname(file.path))
const site = path.resolve(process.cwd(),
`${config.tests.visual}/data`, name, "_")
/* Generate test fixtures with freshly built theme */
const proc = child.spawnSync("mkdocs", [
"build", "--site-dir", site, "--theme-dir", theme
], {
cwd: path.dirname(file.path)
})
/* Emit error, if any */
if (proc.status)
this.emit("error", new util.PluginError("mkdocs",
`Terminated with errors: ${proc.stderr.toString()}`))
/* Terminate */
done()
}))
}
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import moniker from "moniker"
import path from "path"
import * as ecstatic from "~/lib/servers/ecstatic"
import * as sauce from "~/lib/servers/sauce-connect"
import * as selenium from "~/lib/servers/selenium"
import Gemini from "gemini"
import SauceLabs from "saucelabs"
/* ----------------------------------------------------------------------------
* Locals
* ------------------------------------------------------------------------- */
/* SauceLabs job name */
const id = process.env.TRAVIS
? `Travis #${process.env.TRAVIS_BUILD_NUMBER}`
: `Local #${moniker.choose()}`
/* SauceLabs test results */
const passed = {}
/* ----------------------------------------------------------------------------
* Task: run visual tests
* ------------------------------------------------------------------------- */
export default (gulp, config, args) => {
return done => {
/* Start static file server */
let error = false
new Promise(resolve => {
ecstatic.start(`${config.tests.visual}/data`, 8000, resolve)
/* Create and start test runner */
}).then(() => {
return new Promise((resolve, reject) => {
/* Start SauceConnect tunnel */
if (process.env.CI || process.env.SAUCE) {
if (!process.env.SAUCE_USERNAME ||
!process.env.SAUCE_ACCESS_KEY)
throw new Error(
"SauceConnect: please provide SAUCE_USERNAME " +
"and SAUCE_ACCESS_KEY")
/* Start tunnel, if credentials are given */
sauce.start(
id,
process.env.SAUCE_USERNAME,
process.env.SAUCE_ACCESS_KEY,
err => {
return err ? reject(err) : resolve(sauce)
})
/* Start Selenium */
} else {
selenium.start(() => resolve(selenium))
}
})
/* Setup and run Gemini */
.then(runner => {
const setup = require(
path.join(process.cwd(), `${config.tests.visual}/config`,
process.env.CI || process.env.SAUCE
? "gemini.sauce-connect.json"
: "gemini.selenium.json"))
/* Add dynamic configuration to capabilities */
for (const key of Object.keys(setup.browsers)) {
const caps = setup.browsers[key].desiredCapabilities
caps.tunnelIdentifier = id
caps.public = "private"
caps.name = id
/* Adjust configuration for Travis CI */
if (process.env.CI && process.env.TRAVIS)
caps.public = "public"
}
/* Setup Gemini and test listeners */
const gemini = new Gemini(setup)
if (process.env.CI || process.env.SAUCE) {
/* Initialize test run */
gemini.on(gemini.events.START_BROWSER, job => {
passed[job.sessionId] = true
})
/* Update state of test run */
gemini.on(gemini.events.TEST_RESULT, job => {
passed[job.sessionId] = passed[job.sessionId] && job.equal
})
}
/* Run tests */
return gemini.test(`${config.tests.visual}/suites`, {
reporters: ["flat", "html"],
browsers: args.browser ? [].concat(args.browser) : null
})
/* Return runner for graceful stop */
.then(status => {
error = status.failed + status.errored > 0
return runner
})
})
/* Stop test runner */
.then(runner => {
return new Promise(resolve => {
runner.stop(resolve)
})
})
/* Update SauceLabs jobs with test results */
.then(() => {
const saucelabs = new SauceLabs({
username: process.env.SAUCE_USERNAME,
password: process.env.SAUCE_ACCESS_KEY
})
const updates = Object.keys(passed).map(sessionId => {
return new Promise(resolve => {
saucelabs.updateJob(sessionId, {
passed: passed[sessionId]
}, resolve)
})
})
return Promise.all(updates)
})
/* Stop static file server */
})
.then(() => {
ecstatic.stop(() => {
return error
? done(new Error("Gemini terminated with errors"))
: done()
})
}, err => {
return done(new Error(err))
})
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import moniker from "moniker"
import * as ecstatic from "~/lib/servers/ecstatic"
import * as sauce from "~/lib/servers/sauce-connect"
/* ----------------------------------------------------------------------------
* Task: run visual tests
* ------------------------------------------------------------------------- */
export default (gulp, config) => {
return done => {
/* Start static file server */
new Promise(resolve => {
ecstatic.start(`${config.tests.visual}/data`, 8000, resolve)
/* Open SauceConnect tunnel */
}).then(() => {
return new Promise((resolve, reject) => {
/* Start SauceConnect tunnel */
if (process.env.CI || process.env.SAUCE) {
if (!process.env.SAUCE_USERNAME ||
!process.env.SAUCE_ACCESS_KEY)
throw new Error(
"SauceConnect: please provide SAUCE_USERNAME " +
"and SAUCE_ACCESS_KEY")
/* Open tunnel */
sauce.start(
`Local #${moniker.choose()}`,
process.env.SAUCE_USERNAME,
process.env.SAUCE_ACCESS_KEY,
err => {
return err ? reject(err) : resolve(sauce)
})
} else {
resolve()
}
})
/* Close tunnel on CTRL-C */
.then(runner => {
return new Promise(resolve => {
process.on("SIGINT", () => {
return runner
? runner.stop(resolve)
: resolve()
})
})
})
/* Stop static file server */
})
.then(() => {
ecstatic.stop(done)
}, err => {
return done(err)
})
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import fs from "fs"
import path from "path"
import through from "through2"
/* ----------------------------------------------------------------------------
* Task: update reference images for visual tests
* ------------------------------------------------------------------------- */
export default (gulp, config) => {
return () => {
const base = path.join(
process.cwd(), `${config.tests.visual}/config`)
/* Read Gemini configs and map browsers to screenshot directories */
const mapping = fs.readdirSync(base)
.reduce((result, filename) => {
return Object.assign(result, (gemini => {
return Object.keys(gemini.browsers)
.reduce((browsers, name) => {
browsers[name] = gemini.screenshotsDir
return browsers
}, {})
})(require(path.join(base, filename))))
}, {})
/* Prepare filenames */
const dest = path.join(process.cwd(), `${config.tests.visual}/baseline`)
return gulp.src("gemini-report/images/**/*~current.png")
.pipe(
through.obj(function(file, enc, done) {
if (file.isNull() || file.isStream())
return done()
/* Remove the state from the filename */
file.path = file.path.replace("~current", "")
/* Retrieve the folder for the environment of the baseline */
const folder = path.relative(dest,
mapping[path.basename(file.path, ".png")])
file.path = file.path.replace("images", `images/${folder}`)
/* Push file to next stage */
this.push(file)
done()
}))
/* Update reference images */
.pipe(gulp.dest(dest))
}
}

View File

@ -48,8 +48,8 @@ export default (gulp, config, args) => {
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true
}))
.pipe(replace("$theme-name$", metadata.name))
.pipe(replace("$theme-version$", metadata.version))
.pipe(replace("$md-name$", metadata.name))
.pipe(replace("$md-version$", metadata.version))
.pipe(compact())
.pipe(gulpif(args.revision,
version({ manifest: gulp.src("manifest.json") })))

View File

@ -29,7 +29,7 @@ import vinyl from "vinyl-paths"
export default (gulp, config) => {
return () => {
return gulp.src(`${config.views.build}/**/*.html`)
return gulp.src(`${config.views.build}/**/*.{html,py}`)
.pipe(vinyl(clean))
}
}

View File

@ -1,4 +1,4 @@
{% extends "main.html" %}
{% extends "base.html" %}
{% block content %}
<h1>404 - Not found</h1>
{% endblock %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,36 +5,46 @@
{% block site_meta %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
{% if config.site_description %}
{% if page and page.meta.description %}
<meta name="description" content="{{ page.meta.description | first }}">
{% elif config.site_description %}
<meta name="description" content="{{ config.site_description }}">
{% endif %}
{% if page.canonical_url %}
<link rel="canonical" href="{{ page.canonical_url }}">
{% endif %}
{% if config.site_author %}
{% if page and page.meta.author %}
<meta name="author" content="{{ page.meta.author | first }}">
{% elif config.site_author %}
<meta name="author" content="{{ config.site_author }}">
{% endif %}
{% if config.site_favicon %}
<link rel="shortcut icon" href="{{ base_url }}/{{ config.site_favicon }}">
{% else %}
<link rel="shortcut icon" href="{{ base_url }}/assets/images/favicon.ico">
<link rel="shortcut icon" href="{{ base_url }}/assets/images/favicon.png">
{% endif %}
<meta name="generator" content="mkdocs+mkdocs-material#1.0.3">
<meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-1.3.0">
{% endblock %}
{% block htmltitle %}
{% if page.title %}
{% if page and page.meta.title %}
<title>{{ page.meta.title | first }}</title>
{% elif page and page.title and not page.is_homepage %}
<title>{{ page.title }} - {{ config.site_name }}</title>
{% elif config.site_description %}
<title>{{ config.site_name }} - {{ config.site_description }}</title>
{% else %}
<title>{{ config.site_name }}</title>
{% endif %}
{% endblock %}
{% block libs %}
<script src="{{ base_url }}/assets/javascripts/modernizr-facb31f4a3.js"></script>
<script src="{{ base_url }}/assets/javascripts/modernizr-56ade86843.js"></script>
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-1d1da4857d.css">
{% if config.extra.palette %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-66fa0d9bba.palette.css">
{% endif %}
{% endblock %}
{% block fonts %}
{% if config.extra.font != "none" %}
{% if config.extra.font != false and config.extra.font != "none" %}
{% set text = config.extra.get("font", {}).text | default("Roboto") %}
{% set code = config.extra.get("font", {}).code
| default("Roboto Mono") %}
@ -44,15 +54,9 @@
{% endif %}
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-f3ab63f78a.css">
{% if config.extra.palette %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-02ce7adcc2.palette.css">
{% endif %}
{% for path in extra_css %}
<link rel="stylesheet" href="{{ path }}">
{% endfor %}
{% endblock %}
{% block extrahead %}{% endblock %}
</head>
{% set palette = config.extra.get("palette", {}) %}
@ -82,6 +86,10 @@
{% include "partials/header.html" %}
{% endblock %}
<div class="md-container">
{% set feature = config.extra.get("feature", {}) %}
{% if feature.tabs %}
{% include "partials/tabs.html" %}
{% endif %}
<main class="md-main">
<div class="md-main__inner md-grid" data-md-component="container">
{% block site_nav %}
@ -106,14 +114,33 @@
{% endblock %}
<div class="md-content">
<article class="md-content__inner md-typeset">
{% if config.edit_uri %}
<a href="{{ page.edit_url }}" title="{{ lang.t('edit.link.title') }}" class="md-icon md-content__edit">edit</a>
{% endif %}
{% block content %}
{% if not "\x3ch1 id=" in page.content %}
{% if config.edit_uri %}
<a href="{{ page.edit_url }}" title="{{ lang.t('edit.link.title') }}" class="md-icon md-content__icon">edit</a>
{% endif %}
{% if not "\x3ch1" in page.content %}
<h1>{{ page.title | default(config.site_name, true)}}</h1>
{% endif %}
{{ page.content }}
{% block source %}
{% if page.meta.source %}
<h2 id="__source">{{ lang.t('meta.source') }}</h2>
{% set path = (page.meta.path | default([""]) | first) %}
{% for file in page.meta.source %}
<a href="{{
[repo_url, path, file] | join('/') | replace('//', '/')
}}" title="{{ file }}" class="md-source-file">
{{ file }}
</a>
{% endfor %}
{% endif %}
{% endblock %}
{% endblock %}
{% block disqus %}
{% if config.extra.disqus and not page.is_homepage %}
<h2 id="__comments">{{ lang.t('meta.comments') }}</h2>
{% include "partials/disqus.html" %}
{% endif %}
{% endblock %}
</article>
</div>
@ -124,8 +151,8 @@
{% endblock %}
</div>
{% block scripts %}
<script src="{{ base_url }}/assets/javascripts/application-9f4632d68f.js"></script>
<script>var config={url:{base:"{{ base_url }}"}},app=new Application(config);app.initialize()</script>
<script src="{{ base_url }}/assets/javascripts/application-42beea1040.js"></script>
<script>app.initialize({url:{base:"{{ base_url }}"}})</script>
{% for path in extra_javascript %}
<script src="{{ path }}"></script>
{% endfor %}

View File

@ -0,0 +1,14 @@
<div id="disqus_thread"></div>
<script>
var disqus_config = function () {
this.page.url = "{{ page.canonical_url }}";
this.page.identifier =
"{{ page.canonical_url | replace(config.site_url, "") }}";
};
(function() {
var d = document, s = d.createElement("script");
s.src = "//{{ config.extra.disqus }}.disqus.com/embed.js";
s.setAttribute("data-timestamp", +new Date());
(d.head || d.body).appendChild(s);
})();
</script>

View File

@ -1,12 +1,15 @@
<header class="md-header">
<header class="md-header" data-md-component="header">
<nav class="md-header-nav md-grid">
<div class="md-flex">
<div class="md-flex__cell md-flex__cell--shrink">
<a href="{{ nav.homepage.url }}" title="{{ config.site_name }}" class="{% if config.extra.logo %} md-logo {% else %} md-icon md-icon--home {% endif %} md-header-nav__button">
{% if config.extra.logo %}
<a href="{{ nav.homepage.url }}" title="{{ config.site_name }}" class="md-logo md-header-nav__button">
<img src="{{ base_url }}/{{ config.extra.logo }}" width="24" height="24">
{% endif %}
</a>
{% else %}
<a href="{{ nav.homepage.url }}" title="{{ config.site_name }}" class="md-icon md-icon--home md-header-nav__button">
</a>
{% endif %}
</div>
<div class="md-flex__cell md-flex__cell--shrink">
<label class="md-icon md-icon--menu md-header-nav__button" for="drawer"></label>

View File

@ -2,6 +2,8 @@
"edit.link.title": "Edit this page",
"footer.previous": "Previous",
"footer.next": "Next",
"meta.comments": "Comments",
"meta.source": "Source",
"search.placeholder": "Search",
"source.link.title": "Go to repository",
"toc.title": "Table of contents"

View File

@ -1,5 +1,9 @@
{% set class = "md-nav__item" %}
{% if nav_item.active %}
{% set class = "md-nav__item md-nav__item--active" %}
{% endif %}
{% if nav_item.children %}
<li class="md-nav__item md-nav__item--nested">
<li class="{{ class }} md-nav__item--nested">
{% if nav_item.active %}
<input class="md-toggle md-nav__toggle" data-md-toggle="{{ path }}" type="checkbox" id="{{ path }}" checked>
{% else %}
@ -8,27 +12,28 @@
<label class="md-nav__link" for="{{ path }}">
{{ nav_item.title }}
</label>
<nav class="md-nav" data-md-component="collapsible">
<nav class="md-nav" data-md-component="collapsible" data-md-level="{{ level }}">
<label class="md-nav__title" for="{{ path }}">
{{ nav_item.title}}
{{ nav_item.title }}
</label>
<ul class="md-nav__list" data-md-scrollfix>
{% set base = path %}
{% for nav_item in nav_item.children %}
{% set path = base + "-" + loop.index | string %}
{% set level = level + 1 %}
{% include "partials/nav-item.html" %}
{% endfor %}
</ul>
</nav>
</li>
{% elif nav_item == page %}
<li class="md-nav__item">
<li class="{{ class }}">
{% set toc_ = page.toc %}
<input class="md-toggle md-nav__toggle" data-md-toggle="toc" type="checkbox" id="toc">
{% if "\x3ch1 id=" in page.content %}
{% if toc_ | first is defined %}
{% set toc_ = (toc_ | first).children %}
{% endif %}
{% if toc_ and (toc_ | first) %}
{% if toc_ | first is defined %}
<label class="md-nav__link md-nav__link--active" for="toc">
{{ nav_item.title }}
</label>
@ -36,20 +41,14 @@
<a href="{{ nav_item.url }}" title="{{ nav_item.title }}" class="md-nav__link md-nav__link--active">
{{ nav_item.title }}
</a>
{% if page.toc %}
{% if toc_ | first is defined %}
{% include "partials/toc.html" %}
{% endif %}
</li>
{% else %}
<li class="md-nav__item">
{% if nav_item.active %}
<a href="{{ nav_item.url }}" title="{{ nav_item.title }}" class="md-nav__link md-nav__link--active">
{{ nav_item.title }}
</a>
{% else %}
<li class="{{ class }}">
<a href="{{ nav_item.url }}" title="{{ nav_item.title }}" class="md-nav__link">
{{ nav_item.title }}
</a>
{% endif %}
</li>
{% endif %}

View File

@ -1,10 +1,12 @@
<nav class="md-nav md-nav--primary">
<nav class="md-nav md-nav--primary" data-md-level="0">
<label class="md-nav__title md-nav__title--site" for="drawer">
<i class="{% if config.extra.logo %} md-logo {% else %} md-icon md-icon--home {% endif %} md-nav__button">
{% if config.extra.logo %}
<i class="md-logo md-nav__button">
<img src="{{ base_url }}/{{ config.extra.logo }}">
{% endif %}
</i>
{% else %}
<i class="md-icon md-icon--home md-nav__button"></i>
{% endif %}
{{ config.site_name }}
</label>
{% if config.repo_url %}
@ -15,6 +17,7 @@
<ul class="md-nav__list" data-md-scrollfix>
{% for nav_item in nav %}
{% set path = "nav-" + loop.index | string %}
{% set level = 1 %}
{% include "partials/nav-item.html" %}
{% endfor %}
</ul>

View File

@ -3,7 +3,7 @@
<div class="md-search__overlay"></div>
<div class="md-search__inner">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" placeholder="{{ lang.t('search.placeholder') }}" accesskey="s" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false">
<input type="text" class="md-search__input" name="query" placeholder="{{ lang.t('search.placeholder') }}" accesskey="s" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="query">
<label class="md-icon md-search__icon" for="search"></label>
</form>
<div class="md-search__output">

View File

@ -0,0 +1,31 @@
{% if nav_item.is_homepage %}
<li class="md-tabs__item">
{% if not page.ancestors | length and nav | selectattr("url", page.url) %}
<a href="{{ nav_item.url }}" title="{{ nav_item.title }}" class="md-tabs__link md-tabs__link--active">
{{ nav_item.title }}
</a>
{% else %}
<a href="{{ nav_item.url }}" title="{{ nav_item.title }}" class="md-tabs__link">
{{ nav_item.title }}
</a>
{% endif %}
</li>
{% elif nav_item.children and nav_item.children | length > 0 %}
{% set title = title | default(nav_item.title) %}
{% if (nav_item.children | first).children | length > 0 %}
{% set nav_item = nav_item.children | first %}
{% include "partials/tabs-item.html" %}
{% else %}
<li class="md-tabs__item">
{% if nav_item.active %}
<a href="{{ (nav_item.children | first).url }}" title="{{ title }}" class="md-tabs__link md-tabs__link--active">
{{ title }}
</a>
{% else %}
<a href="{{ (nav_item.children | first).url }}" title="{{ title }}" class="md-tabs__link">
{{ title }}
</a>
{% endif %}
</li>
{% endif %}
{% endif %}

View File

@ -0,0 +1,13 @@
{% set class = "md-tabs" %}
{% if page.ancestors | length > 0 %}
{% set class = "md-tabs md-tabs--active" %}
{% endif %}
<nav class="{{ class }}" data-md-component="tabs">
<div class="md-tabs__inner md-grid">
<ul class="md-tabs__list">
{% for nav_item in nav %}
{% include "partials/tabs-item.html" %}
{% endfor %}
</ul>
</div>
</nav>

View File

@ -1,15 +1,29 @@
{% import "partials/language.html" as lang %}
<nav class="md-nav md-nav--secondary">
{% set toc_ = page.toc %}
{% if "\x3ch1 id=" in page.content %}
{% if toc_ | first is defined and "\x3ch1 id=" in page.content %}
{% set toc_ = (toc_ | first).children %}
{% endif %}
{% if toc_ and (toc_ | first) %}
{% if toc_ | first is defined %}
<label class="md-nav__title" for="toc">{{ lang.t('toc.title') }}</label>
<ul class="md-nav__list" data-md-scrollfix>
{% for toc_item in toc_ %}
{% include "partials/toc-item.html" %}
{% endfor %}
{% if page.meta.source and page.meta.source | length > 0 %}
<li class="md-nav__item">
<a href="#__source" title="{{ lang.t('meta.source') }}" class="md-nav__link md-nav__link--active">
{{ lang.t('meta.source') }}
</a>
</li>
{% endif %}
{% if config.extra.disqus and not page.is_homepage %}
<li class="md-nav__item">
<a href="#__comments" title="{{ lang.t('meta.comments') }}" class="md-nav__link md-nav__link--active">
{{ lang.t('meta.comments') }}
</a>
</li>
{% endif %}
</ul>
{% endif %}
</nav>

View File

@ -31,15 +31,19 @@ repo_url: https://github.com/squidfunk/mkdocs-material
# Copyright
copyright: 'Copyright &copy; 2016 - 2017 Martin Donath'
# Documentation and theme
# Theme directory
theme_dir: material
# Options
extra:
feature:
tabs: false
palette:
primary: indigo
accent: indigo
social:
- type: globe
link: http://struct.cc
- type: github-alt
link: https://github.com/squidfunk
- type: twitter
@ -51,6 +55,7 @@ extra:
markdown_extensions:
- markdown.extensions.admonition
- markdown.extensions.codehilite(guess_lang=false)
- markdown.extensions.def_list
- markdown.extensions.footnotes
- markdown.extensions.meta
- markdown.extensions.toc(permalink=true)
@ -76,6 +81,7 @@ pages:
- Admonition: extensions/admonition.md
- CodeHilite: extensions/codehilite.md
- Footnotes: extensions/footnotes.md
- Metadata: extensions/metadata.md
- Permalinks: extensions/permalinks.md
- PyMdown: extensions/pymdown.md
- Specimen: specimen.md

View File

@ -1,6 +1,6 @@
{
"name": "mkdocs-material",
"version": "1.0.3",
"version": "1.3.0",
"description": "A Material Design theme for MkDocs",
"keywords": [
"mkdocs",
@ -25,32 +25,39 @@
"scripts": {
"build": "scripts/build",
"clean": "scripts/clean",
"flow": "scripts/flow",
"help": "scripts/help",
"lint": "scripts/lint",
"start": "scripts/start",
"test": "scripts/test"
"test:visual:run": "scripts/test/visual/run",
"test:visual:update": "scripts/test/visual/update",
"test:visual:session": "scripts/test/visual/session"
},
"dependencies": {},
"devDependencies": {
"autoprefixer": "^6.6.1",
"autoprefixer": "^6.7.3",
"babel-core": "^6.23.0",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-loader": "^6.3.1",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-react-jsx": "^6.8.0",
"babel-polyfill": "^6.20.0",
"babel-preset-es2015": "^6.22.0",
"babel-register": "^6.18.0",
"babel-register": "^6.23.0",
"babel-root-import": "^4.1.5",
"chai": "^3.5.0",
"chalk": "^1.1.3",
"core-js": "^2.4.1",
"css-mqpacker": "^5.0.1",
"custom-event-polyfill": "^0.3.0",
"del": "^2.2.2",
"eslint": "^3.14.0",
"eslint-plugin-mocha": "^4.8.0",
"ecstatic": "^2.1.0",
"eslint": "^3.16.0",
"fastclick": "^1.0.6",
"flow-bin": "^0.41.0",
"flow-jsdoc": "^0.3.0",
"git-hooks": "^1.1.7",
"gulp": "^3.9.1",
"gulp-changed": "^1.3.2",
"gulp-changed": "^2.0.0",
"gulp-concat": "^2.6.1",
"gulp-cssnano": "^2.1.2",
"gulp-htmlmin": "^3.0.0",
@ -70,34 +77,33 @@
"gulp-uglify": "^2.0.0",
"gulp-util": "^3.0.8",
"js-cookie": "^2.1.3",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-notify-reporter": "^1.0.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.26",
"karma-webpack": "^2.0.1",
"lunr": "^0.7.2",
"material-design-color": "^2.3.2",
"material-shadows": "^3.0.1",
"mocha": "^3.2.0",
"modularscale-sass": "^2.1.1",
"node-notifier": "^4.6.1",
"selenium-standalone": "^5.9.1",
"stylelint": "^7.7.1",
"stylelint-config-standard": "^15.0.1",
"stylelint-order": "^0.2.2",
"lunr": "^1.0.0",
"material-design-color": "2.3.2",
"material-shadows": "3.0.1",
"modularscale-sass": "2.1.1",
"node-notifier": "^5.0.0",
"postcss-pseudo-classes": "^0.2.0",
"stylelint": "^7.8.0",
"stylelint-config-standard": "^16.0.0",
"stylelint-order": "^0.3.0",
"stylelint-scss": "^1.4.1",
"through2": "^2.0.3",
"vinyl-paths": "^2.1.0",
"webpack": "^1.14.0",
"webpack": "^2.2.1",
"webpack-stream": "^3.2.0",
"whatwg-fetch": "^2.0.1",
"yargs": "^6.6.0"
"yargs": "^7.0.2"
},
"optionalDependencies": {
"eslint-plugin-mocha": "^4.8.0",
"gemini": "^4.14.3",
"moniker": "^0.1.2",
"sauce-connect-launcher": "^1.2.0",
"saucelabs": "^1.4.0",
"selenium-standalone": "^6.0.0"
},
"engines": {
"node": ">= 4.5.0"
"node": ">= 5.0.0"
},
"private": true
}

View File

@ -20,4 +20,4 @@
mkdocs>=0.16
pygments
pymdown-extensions
pymdown-extensions>=1.2

View File

@ -20,12 +20,12 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "npm install" was executed
if [[ ! -d `npm bin` ]]; then
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "npm install"
echo "yarn install"
exit 1
fi
# Run command
`npm bin`/gulp build --clean --optimize --revision
"$(yarn bin)"/gulp build --clean --optimize --revision "$@"

View File

@ -20,12 +20,12 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "npm install" was executed
if [[ ! -d `npm bin` ]]; then
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "npm install"
echo "yarn install"
exit 1
fi
# Run command
`npm bin`/gulp clean
"$(yarn bin)"/gulp clean "$@"

44
scripts/flow Executable file
View File

@ -0,0 +1,44 @@
#!/bin/bash
# Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "yarn install"
exit 1
fi
# Annotate source files
"$(yarn bin)"/gulp assets:javascripts:annotate "$@"
FLOW_JSDOC=$?
# Run flow typecheck
"$(yarn bin)"/flow check tmp
FLOW=$?
# If one command failed, exit with error
if [ $FLOW_JSDOC -gt 0 ] || [ $FLOW -gt 0 ]; then
exit 1
fi;
# Otherwise return with success
exit 0

View File

@ -20,12 +20,12 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "npm install" was executed
if [[ ! -d `npm bin` ]]; then
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "npm install"
echo "yarn install"
exit 1
fi
# Run command
`npm bin`/gulp test
"$(yarn bin)"/gulp help "@"

View File

@ -20,19 +20,19 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "npm install" was executed
if [[ ! -d `npm bin` ]]; then
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "npm install"
echo "yarn install"
exit 1
fi
# Run ESLint
`npm bin`/eslint .
"$(yarn bin)"/eslint --max-warnings 0 .
ESLINT=$?
# Run Stylelint
`npm bin`/stylelint `find src/assets -name *.scss`
"$(yarn bin)"/stylelint `find src/assets -name *.scss`
STYLELINT=$?
# If one command failed, exit with error

View File

@ -20,12 +20,13 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "npm install" was executed
if [[ ! -d `npm bin` ]]; then
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "npm install"
echo "yarn install"
exit 1
fi
# Run command
`npm bin`/gulp watch --no-lint
"$(yarn bin)"/gulp clean && \
"$(yarn bin)"/gulp watch --no-lint "$@"

31
scripts/test/visual/run Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "yarn install"
exit 1
fi
# Run command
"$(yarn bin)"/gulp tests:visual:run --clean --no-optimize "$@"

31
scripts/test/visual/session Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "yarn install"
exit 1
fi
# Run command
"$(yarn bin)"/gulp tests:visual:session "$@"

31
scripts/test/visual/update Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# Check if "yarn install" was executed
if [[ ! -d "$(yarn bin)" ]]; then
echo "\"node_modules\" not found:"
echo "yarn install"
exit 1
fi
# Run command
"$(yarn bin)"/gulp tests:visual:update "$@"

10
src/.babelrc Normal file
View File

@ -0,0 +1,10 @@
{
"presets": [
["es2015", { "modules": false }]
],
"plugins": [
["transform-react-jsx", {
"pragma": "Jsx.createElement"
}]
]
}

View File

@ -20,7 +20,7 @@
IN THE SOFTWARE.
-->
{% extends "main.html" %}
{% extends "base.html" %}
<!-- Content block -->
{% block content %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -27,50 +27,40 @@ import Material from "./components/Material"
* Application
* ------------------------------------------------------------------------- */
export default class Application {
/**
* Create the application
/**
* Initialize Material for MkDocs
*
* @constructor
* @param {object} config Configuration object
* @param {Object} config - Configuration
*/
constructor(config) {
this.config_ = config
}
/**
* Initialize all components and listeners
*/
initialize() {
function initialize(config) { // eslint-disable-line func-style
/* Initialize Modernizr and FastClick */
new Material.Event.Listener(document, "DOMContentLoaded", () => {
if (!(document.body instanceof HTMLElement))
throw new ReferenceError
/* Attach FastClick to mitigate 300ms delay on touch devices */
FastClick.attach(document.body)
/* Test for iOS */
Modernizr.addTest("ios", () => {
return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
})
/* Test for web application context */
Modernizr.addTest("standalone", () => {
return !!navigator.standalone
})
/* Attack FastClick to mitigate 300ms delay on touch devices */
FastClick.attach(document.body)
/* Wrap all data tables for better overflow scrolling */
const tables = document.querySelectorAll("table:not([class])")
Array.prototype.forEach.call(tables, table => {
const wrap = document.createElement("div")
wrap.classList.add("md-typeset__table")
const wrap = (
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table"></div>
</div>
)
if (table.nextSibling) {
table.parentNode.insertBefore(wrap, table.nextSibling)
} else {
table.parentNode.appendChild(wrap)
}
wrap.appendChild(table)
wrap.children[0].appendChild(table)
})
/* Force 1px scroll offset to trigger overflow scrolling */
@ -93,29 +83,41 @@ export default class Application {
}
}).listen()
/* Component: sidebar container */
if (!Modernizr.csscalc)
new Material.Event.MatchMedia("(min-width: 960px)",
/* Component: header shadow toggle */
new Material.Event.MatchMedia("(min-width: 1220px)",
new Material.Event.Listener(window, [
"resize", "orientationchange"
], new Material.Sidebar.Container("[data-md-component=container]")))
"scroll", "resize", "orientationchange"
], new Material.Header.Shadow(
"[data-md-component=container]",
"[data-md-component=header]")))
/* Component: tabs visibility toggle */
if (document.querySelector("[data-md-component=tabs]"))
new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange"
], new Material.Tabs.Toggle("[data-md-component=tabs]")).listen()
/* Component: sidebar with navigation */
new Material.Event.MatchMedia("(min-width: 1220px)",
new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange"
], new Material.Sidebar.Position("[data-md-component=navigation]")))
], new Material.Sidebar.Position(
"[data-md-component=navigation]",
"[data-md-component=header]")))
/* Component: sidebar with table of contents */
/* Component: sidebar with table of contents - register two separate
listeners, as the offset at the top might change */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange"
], new Material.Sidebar.Position("[data-md-component=toc]")))
], new Material.Sidebar.Position(
"[data-md-component=toc]",
"[data-md-component=header]")))
/* Component: link blurring for table of contents */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(window, "scroll",
new Material.Nav.Blur("[data-md-component=toc] .md-nav__link")))
new Material.Nav.Blur("[data-md-component=toc] [href]")))
/* Component: collapsible elements for navigation */
const collapsibles =
@ -138,15 +140,15 @@ export default class Application {
new Material.Search.Lock("[data-md-toggle=search]")))
/* Component: search results */
new Material.Event.Listener(document.forms.search.query, [
new Material.Event.Listener("[data-md-component=query]", [
"focus", "keyup"
], new Material.Search.Result("[data-md-component=result]", () => {
return fetch(`${this.config_.url.base}/mkdocs/search_index.json`, {
return fetch(`${config.url.base}/mkdocs/search_index.json`, {
credentials: "same-origin"
}).then(response => response.json())
.then(data => {
return data.docs.map(doc => {
doc.location = this.config_.url.base + doc.location
doc.location = config.url.base + doc.location
return doc
})
})
@ -162,6 +164,8 @@ export default class Application {
new Material.Event.Listener("[data-md-component=navigation] [href^='#']",
"click", () => {
const toggle = document.querySelector("[data-md-toggle=drawer]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
@ -171,16 +175,23 @@ export default class Application {
/* Listener: focus input after opening search */
new Material.Event.Listener("[data-md-toggle=search]", "change", ev => {
setTimeout(toggle => {
const query = document.forms.search.query
if (toggle.checked)
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) {
const query = document.querySelector("[data-md-component=query]")
if (!(query instanceof HTMLInputElement))
throw new ReferenceError
query.focus()
}
}, 400, ev.target)
}).listen()
/* Listener: open search on focus */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(document.forms.search.query, "focus", () => {
new Material.Event.Listener("[data-md-component=query]", "focus", () => {
const toggle = document.querySelector("[data-md-toggle=search]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (!toggle.checked) {
toggle.checked = true
toggle.dispatchEvent(new CustomEvent("change"))
@ -191,6 +202,8 @@ export default class Application {
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(document.body, "click", () => {
const toggle = document.querySelector("[data-md-toggle=search]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
@ -202,10 +215,15 @@ export default class Application {
const code = ev.keyCode || ev.which
if (code === 27) {
const toggle = document.querySelector("[data-md-toggle=search]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
document.forms.search.query.blur()
const query = document.querySelector("[data-md-component=query]")
if (!(query instanceof HTMLInputElement))
throw new ReferenceError
query.focus()
}
}
}).listen()
@ -223,13 +241,16 @@ export default class Application {
/* Retrieve facts for the given repository type */
;(() => {
const el = document.querySelector("[data-md-source]")
if (!el) return Promise.resolve([])
if (!el)
return Promise.resolve([])
else if (!(el instanceof HTMLAnchorElement))
throw new ReferenceError
switch (el.dataset.mdSource) {
case "github": return new Material.Source.Adapter.GitHub(el).fetch()
default: return Promise.resolve([])
}
/* Render repository source information */
/* Render repository information */
})().then(facts => {
const sources = document.querySelectorAll("[data-md-source]")
Array.prototype.forEach.call(sources, source => {
@ -237,5 +258,12 @@ export default class Application {
.initialize(facts)
})
})
}
}
/* ----------------------------------------------------------------------------
* Exports
* ------------------------------------------------------------------------- */
export {
initialize
}

View File

@ -21,10 +21,12 @@
*/
import Event from "./Material/Event"
import Header from "./Material/Header"
import Nav from "./Material/Nav"
import Search from "./Material/Search"
import Sidebar from "./Material/Sidebar"
import Source from "./Material/Source"
import Tabs from "./Material/Tabs"
/* ----------------------------------------------------------------------------
* Module
@ -32,8 +34,10 @@ import Source from "./Material/Source"
export default {
Event,
Header,
Nav,
Search,
Sidebar,
Source
Source,
Tabs
}

View File

@ -30,14 +30,22 @@ export default class Listener {
* Generic event listener
*
* @constructor
* @param {(string|NodeList<HTMLElement>)} els - Selector or HTML elements
* @param {Array.<string>} events - Event names
* @param {(object|function)} handler - Handler to be invoked
*
* @property {(Array<EventTarget>)} els_ - Event targets
* @property {Object} handler_- Event handlers
* @property {Array<string>} events_ - Event names
* @property {Function} update_ - Update handler
*
* @param {?(string|EventTarget|NodeList<EventTarget>)} els -
* Selector or Event targets
* @param {(string|Array<string>)} events - Event names
* @param {(Object|Function)} handler - Handler to be invoked
*/
constructor(els, events, handler) {
this.els_ = (typeof els === "string")
this.els_ = Array.prototype.slice.call(
(typeof els === "string")
? document.querySelectorAll(els)
: [].concat(els)
: [].concat(els))
/* Set handler as function or directly as object */
this.handler_ = typeof handler === "function"
@ -53,7 +61,7 @@ export default class Listener {
* Register listener for all relevant events
*/
listen() {
Array.prototype.forEach.call(this.els_, el => {
this.els_.forEach(el => {
this.events_.forEach(event => {
el.addEventListener(event, this.update_, false)
})
@ -68,7 +76,7 @@ export default class Listener {
* Unregister listener for all relevant events
*/
unlisten() {
Array.prototype.forEach.call(this.els_, el => {
this.els_.forEach(el => {
this.events_.forEach(event => {
el.removeEventListener(event, this.update_)
})

View File

@ -20,6 +20,8 @@
* IN THE SOFTWARE.
*/
import Listener from "./Listener" // eslint-disable-line no-unused-vars
/* ----------------------------------------------------------------------------
* Class
* ------------------------------------------------------------------------- */
@ -33,6 +35,9 @@ export default class MatchMedia {
* switches the given listeners on or off.
*
* @constructor
*
* @property {Function} handler_ - Media query event handler
*
* @param {string} query - Media query to test for
* @param {Listener} listener - Event listener
*/

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import Shadow from "./Header/Shadow"
/* ----------------------------------------------------------------------------
* Module
* ------------------------------------------------------------------------- */
export default {
Shadow
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Class
* ------------------------------------------------------------------------- */
export default class Shadow {
/**
* Show or hide header shadow depending on page y-offset
*
* @constructor
*
* @property {HTMLElement} el_ - Content container
* @property {HTMLElement} header_ - Header
* @property {number} height_ - Offset height of previous nodes
* @property {boolean} active_ - Header shadow state
*
* @param {(string|HTMLElement)} el - Selector or HTML element
* @param {(string|HTMLElement)} header - Selector or HTML element
*/
constructor(el, header) {
let ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof HTMLElement) ||
!(ref.parentNode instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref.parentNode
/* Retrieve header */
ref = (typeof header === "string")
? document.querySelector(header)
: header
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.header_ = ref
/* Initialize height and state */
this.height_ = 0
this.active_ = false
}
/**
* Calculate total height of previous nodes
*/
setup() {
let current = this.el_
while ((current = current.previousElementSibling)) {
if (!(current instanceof HTMLElement))
throw new ReferenceError
this.height_ += current.offsetHeight
}
this.update()
}
/**
* Update shadow state
*/
update() {
const active = window.pageYOffset >= this.height_
if (active !== this.active_)
this.header_.dataset.mdState = (this.active_ = active) ? "shadow" : ""
}
/**
* Reset shadow state
*/
reset() {
this.header_.dataset.mdState = ""
this.height_ = 0
this.active_ = false
}
}

View File

@ -27,9 +27,16 @@
export default class Blur {
/**
* Blur anchors within the navigation above current page y-offset
* Blur links within the table of contents above current page y-offset
*
* @constructor
*
* @property {NodeList<HTMLElement>} els_ - Table of contents links
* @property {Array<HTMLElement>} anchors_ - Referenced anchor nodes
* @property {number} index_ - Current link index
* @property {number} offset_ - Current page y-offset
* @property {boolean} dir_ - Scroll direction change
*
* @param {(string|NodeList<HTMLElement>)} els - Selector or HTML elements
*/
constructor(els) {
@ -45,20 +52,21 @@ export default class Blur {
this.dir_ = false
/* Index anchor node offsets for fast lookup */
this.anchors_ = [].map.call(this.els_, el => {
return document.getElementById(el.hash.substring(1))
})
this.anchors_ = [].reduce.call(this.els_, (anchors, el) => {
return anchors.concat(
document.getElementById(el.hash.substring(1)) || [])
}, [])
}
/**
* Initialize anchor states
* Initialize blur states
*/
setup() {
this.update()
}
/**
* Update anchor states
* Update blur states
*
* Deduct the static offset of the header (56px) and sidebar offset (24px),
* see _permalinks.scss for more information.
@ -67,7 +75,7 @@ export default class Blur {
const offset = window.pageYOffset
const dir = this.offset_ - offset < 0
/* Hack: reset index if direction changed, to catch very fast scrolling,
/* Hack: reset index if direction changed to catch very fast scrolling,
because otherwise we would have to register a timer and that sucks */
if (this.dir_ !== dir)
this.index_ = dir
@ -109,7 +117,7 @@ export default class Blur {
}
/**
* Reset anchor states
* Reset blur states
*/
reset() {
Array.prototype.forEach.call(this.els_, el => {

View File

@ -30,12 +30,18 @@ export default class Collapse {
* Expand or collapse navigation on toggle
*
* @constructor
*
* @property {HTMLElement} el_ - Navigation list
*
* @param {(string|HTMLElement)} el - Selector or HTML element
*/
constructor(el) {
this.el_ = (typeof el === "string")
const ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
}
/**
@ -75,11 +81,16 @@ export default class Collapse {
/* Remove state on end of transition */
const end = ev => {
ev.target.removeAttribute("data-md-state")
ev.target.style.maxHeight = ""
const target = ev.target
if (!(target instanceof HTMLElement))
throw new ReferenceError
/* Reset height and state */
target.removeAttribute("data-md-state")
target.style.maxHeight = ""
/* Only fire once, so directly remove event listener */
ev.target.removeEventListener("transitionend", end)
target.removeEventListener("transitionend", end)
}
this.el_.addEventListener("transitionend", end, false)
}

View File

@ -30,12 +30,18 @@ export default class Scrolling {
* Set overflow scrolling on the current active pane (for iOS)
*
* @constructor
*
* @property {HTMLElement} el_ - Primary navigation
*
* @param {(string|HTMLElement)} el - Selector or HTML element
*/
constructor(el) {
this.el_ = (typeof el === "string")
const ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
}
/**
@ -49,13 +55,22 @@ export default class Scrolling {
/* Find all toggles and check which one is active */
const toggles = this.el_.querySelectorAll("[data-md-toggle]")
Array.prototype.forEach.call(toggles, toggle => {
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) {
/* Find corresponding navigational pane */
let pane = toggle.nextElementSibling
while (pane.tagName !== "NAV")
if (!(pane instanceof HTMLElement))
throw new ReferenceError
while (pane.tagName !== "NAV" && pane.nextElementSibling)
pane = pane.nextElementSibling
/* Check references */
if (!(toggle.parentNode instanceof HTMLElement) ||
!(toggle.parentNode.parentNode instanceof HTMLElement))
throw new ReferenceError
/* Find current and parent list elements */
const parent = toggle.parentNode.parentNode
const target = pane.children[pane.children.length - 1]
@ -73,34 +88,48 @@ export default class Scrolling {
* @param {Event} ev - Change event
*/
update(ev) {
const target = ev.target
if (!(target instanceof HTMLElement))
throw new ReferenceError
/* Find corresponding navigational pane */
let pane = ev.target.nextElementSibling
while (pane.tagName !== "NAV")
let pane = target.nextElementSibling
if (!(pane instanceof HTMLElement))
throw new ReferenceError
while (pane.tagName !== "NAV" && pane.nextElementSibling)
pane = pane.nextElementSibling
/* Find current and parent list elements */
const parent = ev.target.parentNode.parentNode
const target = pane.children[pane.children.length - 1]
/* Check references */
if (!(target.parentNode instanceof HTMLElement) ||
!(target.parentNode.parentNode instanceof HTMLElement))
throw new ReferenceError
/* Find parent and active panes */
const parent = target.parentNode.parentNode
const active = pane.children[pane.children.length - 1]
/* Always reset all lists when transitioning */
parent.style.webkitOverflowScrolling = ""
target.style.webkitOverflowScrolling = ""
active.style.webkitOverflowScrolling = ""
/* Set overflow scrolling on parent */
if (!ev.target.checked) {
/* Set overflow scrolling on parent pane */
if (!target.checked) {
const end = () => {
if (pane instanceof HTMLElement) {
parent.style.webkitOverflowScrolling = "touch"
pane.removeEventListener("transitionend", end)
}
}
pane.addEventListener("transitionend", end, false)
}
/* Set overflow scrolling on target */
if (ev.target.checked) {
/* Set overflow scrolling on active pane */
if (target.checked) {
const end = () => {
target.style.webkitOverflowScrolling = "touch"
pane.removeEventListener("transitionend", end, false)
if (pane instanceof HTMLElement) {
active.style.webkitOverflowScrolling = "touch"
pane.removeEventListener("transitionend", end)
}
}
pane.addEventListener("transitionend", end, false)
}
@ -117,20 +146,29 @@ export default class Scrolling {
/* Find all toggles and check which one is active */
const toggles = this.el_.querySelectorAll("[data-md-toggle]")
Array.prototype.forEach.call(toggles, toggle => {
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) {
/* Find corresponding navigational pane */
let pane = toggle.nextElementSibling
while (pane.tagName !== "NAV")
if (!(pane instanceof HTMLElement))
throw new ReferenceError
while (pane.tagName !== "NAV" && pane.nextElementSibling)
pane = pane.nextElementSibling
/* Find current and parent list elements */
/* Check references */
if (!(toggle.parentNode instanceof HTMLElement) ||
!(toggle.parentNode.parentNode instanceof HTMLElement))
throw new ReferenceError
/* Find parent and active panes */
const parent = toggle.parentNode.parentNode
const target = pane.children[pane.children.length - 1]
const active = pane.children[pane.children.length - 1]
/* Always reset all lists when transitioning */
parent.style.webkitOverflowScrolling = ""
target.style.webkitOverflowScrolling = ""
active.style.webkitOverflowScrolling = ""
}
})
}

View File

@ -30,12 +30,25 @@ export default class Lock {
* Lock body for full-screen search modal
*
* @constructor
*
* @property {HTMLInputElement} el_ - Lock toggle
* @property {HTMLElement} lock_ - Element to lock (document body)
* @property {number} offset_ - Current page y-offset
*
* @param {(string|HTMLElement)} el - Selector or HTML element
*/
constructor(el) {
this.el_ = (typeof el === "string")
const ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof HTMLInputElement))
throw new ReferenceError
this.el_ = ref
/* Retrieve element to lock (= body) */
if (!document.body)
throw new ReferenceError
this.lock_ = document.body
}
/**
@ -60,13 +73,13 @@ export default class Lock {
/* Lock body after finishing transition */
if (this.el_.checked) {
document.body.dataset.mdState = "lock"
this.lock_.dataset.mdState = "lock"
}
}, 400)
/* Exiting search mode */
} else {
document.body.dataset.mdState = ""
this.lock_.dataset.mdState = ""
/* Scroll to former position, but wait for 100ms to prevent flashes on
iOS. A short timeout seems to do the trick */
@ -81,8 +94,8 @@ export default class Lock {
* Reset locked state and page y-offset
*/
reset() {
if (document.body.dataset.mdState === "lock")
if (this.lock_.dataset.mdState === "lock")
window.scrollTo(0, this.offset_)
document.body.dataset.mdState = ""
this.lock_.dataset.mdState = ""
}
}

View File

@ -32,13 +32,24 @@ export default class Result {
* Perform search and update results on keyboard events
*
* @constructor
*
* @property {HTMLElement} el_ - Search result container
* @property {(Array<Object>|Function)} data_ - Raw document data
* @property {Object} docs_ - Indexed documents
* @property {HTMLElement} meta_ - Search meta information
* @property {HTMLElement} list_ - Search result list
* @property {Object} index_ - Search index
*
* @param {(string|HTMLElement)} el - Selector or HTML element
* @param {(Array.<object>|Function)} data - Promise or array providing data
* @param {(Array<Object>|Function)} data - Function providing data or array
*/
constructor(el, data) {
this.el_ = (typeof el === "string")
const ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
/* Set data and create metadata and list elements */
this.data_ = data
@ -54,12 +65,20 @@ export default class Result {
/* Inject created elements */
this.el_.appendChild(this.meta_)
this.el_.appendChild(this.list_)
}
/* Truncate a string after the given number of characters - this is not
a reasonable approach, since the summaries kind of suck. It would be
better to create something more intelligent, highlighting the search
occurrences and making a better summary out of it */
this.truncate_ = function(string, n) {
/**
* Truncate a string after the given number of character
*
* This is not a reasonable approach, since the summaries kind of suck. It
* would be better to create something more intelligent, highlighting the
* search occurrences and making a better summary out of it
*
* @param {string} string - String to be truncated
* @param {number} n - Number of characters
* @return {string} Truncated string
*/
truncate_(string, n) {
let i = n
if (string.length > i) {
while (string[i] !== " " && --i > 0);
@ -67,7 +86,6 @@ export default class Result {
}
return string
}
}
/**
* Update search results
@ -129,7 +147,7 @@ export default class Result {
const data2 = trimmed
/* Index documents */
this.data_ = data2.reduce((docs, doc) => {
this.docs_ = data2.reduce((docs, doc) => {
this.index_.add(doc)
docs[doc.location] = doc
return docs
@ -143,16 +161,21 @@ export default class Result {
: init(this.data_)
}, 250)
/* Execute search on new input event after clearing current list */
/* Execute search on new input event */
} else if (ev.type === "keyup") {
const target = ev.target
if (!(target instanceof HTMLInputElement))
throw new ReferenceError
/* Clear current list */
while (this.list_.firstChild)
this.list_.removeChild(this.list_.firstChild)
/* Perform search on index and render documents */
const result = this.index_.search(ev.target.value)
const result = this.index_.search(target.value)
// process results!
const re = new RegExp(`\\b${ev.target.value}`, "img")
const re = new RegExp(`\\b${target.value}`, "img")
// result.map(item => {
// // console.time("someFunction")
// text = text.replace(re, "*XXX*") // do in parallel and collect!
@ -205,6 +228,8 @@ export default class Result {
Array.prototype.forEach.call(anchors, anchor => {
anchor.addEventListener("click", ev2 => {
const toggle = document.querySelector("[data-md-toggle=search]")
if (!(toggle instanceof HTMLInputElement))
throw new ReferenceError
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))

View File

@ -20,7 +20,6 @@
* IN THE SOFTWARE.
*/
import Container from "./Sidebar/Container"
import Position from "./Sidebar/Position"
/* ----------------------------------------------------------------------------
@ -28,6 +27,5 @@ import Position from "./Sidebar/Position"
* ------------------------------------------------------------------------- */
export default {
Container,
Position
}

View File

@ -30,23 +30,51 @@ export default class Position {
* Set sidebars to locked state and limit height to parent node
*
* @constructor
*
* @property {HTMLElement} el_ - Sidebar
* @property {HTMLElement} parent_ - Sidebar container
* @property {HTMLElement} header_ - Header
* @property {number} height_ - Current sidebar height
* @property {number} offset_ - Current page y-offset
* @property {boolean} pad_ - Pad when header is fixed
*
* @param {(string|HTMLElement)} el - Selector or HTML element
* @param {(string|HTMLElement)} header - Selector or HTML element
*/
constructor(el) {
this.el_ = (typeof el === "string")
constructor(el, header) {
let ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof HTMLElement) ||
!(ref.parentNode instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
this.parent_ = ref.parentNode
/* Initialize parent container and current height */
this.parent_ = this.el_.parentNode
/* Retrieve header */
ref = (typeof header === "string")
? document.querySelector(header)
: header
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.header_ = ref
/* Initialize current height and test whether header is fixed */
this.height_ = 0
this.pad_ = window.getComputedStyle(this.header_).position === "fixed"
}
/**
* Initialize sidebar state
*/
setup() {
this.offset_ = this.el_.offsetTop - this.parent_.offsetTop
const top = Array.prototype.reduce.call(
this.parent_.children, (offset, child) => {
return Math.max(offset, child.offsetTop)
}, 0)
/* Set lock offset for element with largest top offset */
this.offset_ = top - (this.pad_ ? this.header_.offsetHeight : 0)
this.update()
}
@ -55,25 +83,31 @@ export default class Position {
*
* The inner height of the window (= the visible area) is the maximum
* possible height for the stretching sidebar. This height must be deducted
* by the top offset of the parent container, which accounts for the fixed
* header, setting the baseline. Depending on the page y-offset, the top
* offset of the sidebar must be taken into account, as well as the case
* where the window is scrolled beyond the sidebar container.
* by the height of the fixed header (56px). Depending on the page y-offset,
* the top offset of the sidebar must be taken into account, as well as the
* case where the window is scrolled beyond the sidebar container.
*
* @param {Event?} ev - Event
*/
update() {
update(ev) {
const offset = window.pageYOffset
const visible = window.innerHeight
/* Calculate bounds of sidebar container */
this.bounds_ = {
top: this.parent_.offsetTop,
/* Update offset, in case window is resized */
if (ev && ev.type === "resize")
this.setup()
/* Set bounds of sidebar container - must be calculated on every run, as
the height of the content might change due to loading images etc. */
const bounds = {
top: this.pad_ ? this.header_.offsetHeight : 0,
bottom: this.parent_.offsetTop + this.parent_.offsetHeight
}
/* Calculate new offset and height */
const height = visible - this.bounds_.top
const height = visible - bounds.top
- Math.max(0, this.offset_ - offset)
- Math.max(0, offset + visible - this.bounds_.bottom)
- Math.max(0, offset + visible - bounds.bottom)
/* If height changed, update element */
if (height !== this.height_)

View File

@ -29,16 +29,25 @@ import Cookies from "js-cookie"
export default class Abstract {
/**
* Retrieve source information
* Retrieve repository information
*
* @constructor
* @param {(string|HTMLElement)} el - Selector or HTML element
*
* @property {HTMLAnchorElement} el_ - Link to repository
* @property {string} base_ - API base URL
* @property {number} salt_ - Unique identifier
*
* @param {(string|HTMLAnchorElement)} el - Selector or HTML element
*/
constructor(el) {
this.el_ = (typeof el === "string")
const ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof HTMLAnchorElement))
throw new ReferenceError
this.el_ = ref
/* Retrieve base URL */
this.base_ = this.el_.href
this.salt_ = this.hash_(this.base_)
@ -47,7 +56,7 @@ export default class Abstract {
/**
* Retrieve data from Cookie or fetch from respective API
*
* @return {Promise} Promise that returns an array of facts
* @return {Promise<Array<string>>} Promise that returns an array of facts
*/
fetch() {
return new Promise(resolve => {
@ -70,7 +79,6 @@ export default class Abstract {
* Abstract private function that fetches relevant repository information
*
* @abstract
* @return {Promise} Promise that provides the facts in an array
*/
fetch_() {
throw new Error("fetch_(): Not implemented")
@ -79,15 +87,15 @@ export default class Abstract {
/**
* Format a number with suffix
*
* @param {Number} number - Number to format
* @return {Number} Formatted number
* @param {number} number - Number to format
* @return {string} Formatted number
*/
format_(number) {
if (number > 10000)
return `${(number / 1000).toFixed(0)}k`
else if (number > 1000)
return `${(number / 1000).toFixed(1)}k`
return number
return `${number}`
}
/**
@ -96,7 +104,7 @@ export default class Abstract {
* Taken from http://stackoverflow.com/a/7616484/1065584
*
* @param {string} str - Input string
* @return {string} Hashed string
* @return {number} Hashed string
*/
hash_(str) {
let hash = 0

View File

@ -29,22 +29,24 @@ import Abstract from "./Abstract"
export default class GitHub extends Abstract {
/**
* Retrieve source information from GitHub
* Retrieve repository information from GitHub
*
* @constructor
* @param {(string|HTMLElement)} el - Selector or HTML element
* @param {(string|HTMLAnchorElement)} el - Selector or HTML element
*/
constructor(el) {
super(el)
/* Adjust base URL to reach API endpoints */
this.base_ = this.base_.replace("github.com/", "api.github.com/repos/")
/* Adjust base URL to reach API endpoints and remove trailing slash */
this.base_ = this.base_
.replace("github.com/", "api.github.com/repos/")
.replace(/\/$/, "")
}
/**
* Fetch relevant source information from GitHub
* Fetch relevant repository information from GitHub
*
* @return {function} Promise returning an array of facts
* @return {Promise<Array<string>>} Promise returning an array of facts
*/
fetch_() {
return fetch(this.base_)

View File

@ -30,21 +30,27 @@ export default class Repository {
* Render repository information
*
* @constructor
*
* @property {HTMLElement} el_ - Repository information
*
* @param {(string|HTMLElement)} el - Selector or HTML element
*/
constructor(el) {
this.el_ = (typeof el === "string")
const ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof HTMLElement))
throw new ReferenceError
this.el_ = ref
}
/**
* Initialize the source repository
* Initialize the repository
*
* @param {Array.<string>} facts - Facts to be rendered
* @param {Array<string>} facts - Facts to be rendered
*/
initialize(facts) {
if (facts.length)
if (facts.length && this.el_.children.length)
this.el_.children[this.el_.children.length - 1].appendChild(
<ul class="md-source__facts">
{facts.map(fact => <li class="md-source__fact">{fact}</li>)}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2016-2017 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import Toggle from "./Tabs/Toggle"
/* ----------------------------------------------------------------------------
* Module
* ------------------------------------------------------------------------- */
export default {
Toggle
}

View File

@ -24,42 +24,46 @@
* Class
* ------------------------------------------------------------------------- */
export default class Container {
export default class Toggle {
/**
* Monitor window height to stretch sidebar container to viewport
* Toggle tabs visibility depending on page y-offset
*
* @constructor
*
* @property {HTMLElement} el_ - Content container
* @property {number} offset_ - Toggle page-y offset
* @property {boolean} active_ - Tabs visibility
*
* @param {(string|HTMLElement)} el - Selector or HTML element
*/
constructor(el) {
this.el_ = (typeof el === "string")
const ref = (typeof el === "string")
? document.querySelector(el)
: el
if (!(ref instanceof Node))
throw new ReferenceError
this.el_ = ref
/* Retrieve parent node */
this.parent_ = this.el_.parentNode
/* Initialize offset and state */
this.offset_ = 5
this.active_ = false
}
/**
* Initialize container state
*/
setup() {
this.update()
}
/**
* Update minimum height
* Update visibility
*/
update() {
const height = this.parent_.offsetHeight - this.el_.offsetTop
this.el_.style.minHeight = `${height}px`
const active = window.pageYOffset >= this.offset_
if (active !== this.active_)
this.el_.dataset.mdState = (this.active_ = active) ? "hidden" : ""
}
/**
* Reset minimum height
* Reset visibility
*/
reset() {
this.el_.style.minHeight = ""
this.el_.dataset.mdState = ""
this.active_ = false
}
}

Some files were not shown because too many files have changed in this diff Show More