Merge branch 'master' into refactor/sidebar-height-spacing

This commit is contained in:
squidfunk 2017-02-11 23:02:12 +01:00
commit 6a68347a87
242 changed files with 9464 additions and 439 deletions

View File

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

View File

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

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 # Perform install and prune of NPM dependencies if package.json changed
if $(echo "$CHANGED" | grep --quiet package.json); then if $(echo "$CHANGED" | grep --quiet package.json); then
echo "Hook[post-merge]: Updating dependencies..." echo "Hook[post-merge]: Updating dependencies"
npm install && npm prune npm install && npm prune
fi fi

View File

@ -22,7 +22,7 @@
# Determine current branch # Determine current branch
BRANCH=$(git rev-parse --abbrev-ref HEAD) BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Hook[pre-commit]: Checking branch..." echo "Hook[pre-commit]: Checking branch"
# If we're on master, abort commit # If we're on master, abort commit
if [[ "$BRANCH" == "master" ]]; then if [[ "$BRANCH" == "master" ]]; then

View File

@ -46,6 +46,6 @@ FILES=$(git diff --cached --name-only --diff-filter=ACMR | \
# Run the check and print indicator # Run the check and print indicator
if [ "$FILES" ]; then if [ "$FILES" ]; then
echo "Hook[pre-commit]: Running linter..." echo "Hook[pre-commit]: Running linter"
npm run lint --silent || exit 1 npm run lint --silent || exit 1
fi fi

9
.gitignore vendored
View File

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

View File

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

View File

@ -23,19 +23,48 @@ sudo: false
# Node.js versions # Node.js versions
node_js: node_js:
- 4
- 5 - 5
- 6 - 6
- 7
# Build visual tests separately
matrix:
include:
- 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
# Limit clone depth to 5, to speed up build
git:
depth: 5
# Cache dependencies # Cache dependencies
cache: cache:
pip: true pip: true
yarn: true
directories: directories:
- node_modules - node_modules
# Install yarn as Travis doesn't support it out of the box
before_install: npm install -g yarn
# Do not install optional dependencies by default
install: yarn install --ignore-optional
# Install dependencies # Install dependencies
before_script: before_script: pip install --user -r requirements.txt
- pip install --user -r requirements.txt
# Perform build and tests # Perform build and tests
script: npm run build script: yarn run build

View File

@ -35,14 +35,17 @@ const config = {
src: "src/assets", /* Source directory for assets */ src: "src/assets", /* Source directory for assets */
build: "material/assets" /* Target 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: { views: {
src: "src", /* Source directory for views */ src: "src", /* Source directory for views */
build: "material" /* Target directory for views */ build: "material" /* Target directory for views */
} }
} }
const args = yargs let args = yargs
.default("clean", false) /* Clean before build */ .default("clean", false) /* Clean before build */
.default("karma", true) /* Karma watchdog */ .default("karma", true) /* Karma watchdog */
.default("lint", true) /* Lint sources */ .default("lint", true) /* Lint sources */
@ -52,6 +55,12 @@ const args = yargs
.default("sourcemaps", false) /* Create sourcemaps */ .default("sourcemaps", false) /* Create sourcemaps */
.argv .argv
/* Only use the last value seen, so overrides are possible */
args = Object.keys(args).reduce((result, arg) => {
result[arg] = [].concat(args[arg]).pop()
return result
}, {})
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Overrides and helpers * Overrides and helpers
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -110,7 +119,7 @@ const load = task => {
* Copy favicon * Copy favicon
*/ */
gulp.task("assets:images:build:ico", [ gulp.task("assets:images:build:ico", [
args.clean ? "assets:images:clean" : null args.clean ? "assets:images:clean" : false
].filter(t => t), ].filter(t => t),
load("assets/images/build/ico")) load("assets/images/build/ico"))
@ -118,7 +127,7 @@ gulp.task("assets:images:build:ico", [
* Copy and minify vector graphics * Copy and minify vector graphics
*/ */
gulp.task("assets:images:build:svg", [ gulp.task("assets:images:build:svg", [
args.clean ? "assets:images:clean" : null args.clean ? "assets:images:clean" : false
].filter(t => t), ].filter(t => t),
load("assets/images/build/svg")) load("assets/images/build/svg"))
@ -145,8 +154,8 @@ gulp.task("assets:images:clean",
*/ */
gulp.task("assets:javascripts:build:application", [ gulp.task("assets:javascripts:build:application", [
args.clean ? "assets:javascripts:clean" : null, args.clean ? "assets:javascripts:clean" : false,
args.lint ? "assets:javascripts:lint" : null args.lint ? "assets:javascripts:lint" : false
].filter(t => t), ].filter(t => t),
load("assets/javascripts/build/application")) load("assets/javascripts/build/application"))
@ -155,8 +164,8 @@ gulp.task("assets:javascripts:build:application", [
*/ */
gulp.task("assets:javascripts:build:modernizr", [ gulp.task("assets:javascripts:build:modernizr", [
"assets:stylesheets:build", "assets:stylesheets:build",
args.clean ? "assets:javascripts:clean" : null, args.clean ? "assets:javascripts:clean" : false,
args.lint ? "assets:javascripts:lint" : null args.lint ? "assets:javascripts:lint" : false
].filter(t => t), ].filter(t => t),
load("assets/javascripts/build/modernizr")) load("assets/javascripts/build/modernizr"))
@ -188,8 +197,8 @@ gulp.task("assets:javascripts:lint",
* Build stylesheets from SASS source * Build stylesheets from SASS source
*/ */
gulp.task("assets:stylesheets:build", [ gulp.task("assets:stylesheets:build", [
args.clean ? "assets:stylesheets:clean" : null, args.clean ? "assets:stylesheets:clean" : false,
args.lint ? "assets:stylesheets:lint" : null args.lint ? "assets:stylesheets:lint" : false
].filter(t => t), ].filter(t => t),
load("assets/stylesheets/build")) load("assets/stylesheets/build"))
@ -236,10 +245,10 @@ gulp.task("assets:clean", [
*/ */
gulp.task("views:build", [ gulp.task("views:build", [
args.revision ? "assets:images:build" : null, args.revision ? "assets:images:build" : false,
args.revision ? "assets:stylesheets:build" : null, args.revision ? "assets:stylesheets:build" : false,
args.revision ? "assets:javascripts:build" : null, args.revision ? "assets:javascripts:build" : false,
args.clean ? "views:clean" : null args.clean ? "views:clean" : false
].filter(t => t), ].filter(t => t),
load("views/build")) load("views/build"))
@ -260,7 +269,7 @@ gulp.task("mkdocs:build", [
"assets:build", "assets:build",
"views:build", "views:build",
"mkdocs:clean" "mkdocs:clean"
], ].filter(t => t),
load("mkdocs/build")) load("mkdocs/build"))
/* /*
@ -276,14 +285,44 @@ gulp.task("mkdocs:serve",
load("mkdocs/serve")) load("mkdocs/serve"))
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Tests * Visual tests
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/* /*
* Start karma test runner * Generate visual tests
*/ */
gulp.task("tests:unit:watch", gulp.task("tests:visual:generate", [
load("tests/unit/watch")) 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 * Interface
@ -295,7 +334,7 @@ gulp.task("tests:unit:watch",
gulp.task("build", [ gulp.task("build", [
"assets:build", "assets:build",
"views:build", "views:build",
args.mkdocs ? "mkdocs:build" : null args.mkdocs ? "mkdocs:build" : false
].filter(f => f)) ].filter(f => f))
/* /*

6
lib/.eslintrc Normal file
View File

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

View File

@ -21,7 +21,7 @@
*/ */
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Definition * Module
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
export default /* JSX */ { export default /* JSX */ {

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

@ -50,12 +50,13 @@ export default (gulp, config, args) => {
], ],
output: { output: {
filename: "application.js", filename: "application.js",
library: "Application" library: "app",
libraryTarget: "window"
}, },
module: { module: {
/* Transpile ES6 to ES5 with Babel */ /* Transpile ES6 to ES5 with Babel */
loaders: [ rules: [
{ {
loader: "babel-loader", loader: "babel-loader",
test: /\.jsx?$/ test: /\.jsx?$/
@ -65,7 +66,7 @@ export default (gulp, config, args) => {
plugins: [ plugins: [
/* Don't emit assets that include errors */ /* Don't emit assets that include errors */
new webpack.NoErrorsPlugin(), new webpack.NoEmitOnErrorsPlugin(),
/* Provide JSX helper */ /* Provide JSX helper */
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
@ -77,19 +78,30 @@ export default (gulp, config, args) => {
args.optimize ? [ args.optimize ? [
new webpack.optimize.UglifyJsPlugin({ new webpack.optimize.UglifyJsPlugin({
compress: { 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 */ /* Module resolver */
resolve: { resolve: {
modulesDirectories: [ modules: [
"src/assets/javascripts", "src/assets/javascripts",
"node_modules" "node_modules"
], ],
extensions: [ extensions: [
"",
".js", ".js",
".jsx" ".jsx"
] ]
@ -101,8 +113,8 @@ export default (gulp, config, args) => {
}, },
/* Sourcemap support */ /* Sourcemap support */
devtool: args.sourcemaps ? "source-map" : "" devtool: args.sourcemaps ? "inline-source-map" : ""
})) }, webpack))
/* Revisioning */ /* Revisioning */
.pipe(gulpif(args.revision, rev())) .pipe(gulpif(args.revision, rev()))

View File

@ -23,6 +23,7 @@
import path from "path" import path from "path"
import through from "through2" import through from "through2"
import util from "gulp-util" import util from "gulp-util"
import { CLIEngine } from "eslint" import { CLIEngine } from "eslint"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------

View File

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

View File

@ -39,7 +39,7 @@ export default () => {
server.kill() server.kill()
/* Spawn MkDocs server */ /* 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" 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.browsers ? [].concat(args.browsers) : 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,74 @@
/*
* 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) => {
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)
})
})
/* Close tunnel on CTRL-C */
.then(() => {
return new Promise(resolve => {
process.on("SIGINT", () => {
sauce.stop(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))
}
}

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

@ -19,7 +19,7 @@
{% else %} {% else %}
<link rel="shortcut icon" href="{{ base_url }}/assets/images/favicon.png"> <link rel="shortcut icon" href="{{ base_url }}/assets/images/favicon.png">
{% endif %} {% endif %}
<meta name="generator" content="mkdocs+mkdocs-material#1.0.3"> <meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-1.0.3">
{% endblock %} {% endblock %}
{% block htmltitle %} {% block htmltitle %}
{% if page.title and not page.is_homepage %} {% if page.title and not page.is_homepage %}
@ -29,7 +29,13 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block libs %} {% block libs %}
<script src="{{ base_url }}/assets/javascripts/modernizr-facb31f4a3.js"></script> <script src="{{ base_url }}/assets/javascripts/modernizr-5b0c41c2b5.js"></script>
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-932e030699.css">
{% if config.extra.palette %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-02ce7adcc2.palette.css">
{% endif %}
{% endblock %} {% endblock %}
{% block fonts %} {% block fonts %}
{% if config.extra.font != "none" %} {% if config.extra.font != "none" %}
@ -42,15 +48,9 @@
{% endif %} {% endif %}
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
{% endblock %} {% endblock %}
{% block styles %} {% for path in extra_css %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-22ac52ce22.css"> <link rel="stylesheet" href="{{ path }}">
{% if config.extra.palette %} {% endfor %}
<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 %} {% block extrahead %}{% endblock %}
</head> </head>
{% set palette = config.extra.get("palette", {}) %} {% set palette = config.extra.get("palette", {}) %}
@ -112,7 +112,7 @@
<a href="{{ page.edit_url }}" title="{{ lang.t('edit.link.title') }}" class="md-icon md-content__edit">edit</a> <a href="{{ page.edit_url }}" title="{{ lang.t('edit.link.title') }}" class="md-icon md-content__edit">edit</a>
{% endif %} {% endif %}
{% block content %} {% block content %}
{% if not "\x3ch1 id=" in page.content %} {% if not "\x3ch1" in page.content %}
<h1>{{ page.title | default(config.site_name, true)}}</h1> <h1>{{ page.title | default(config.site_name, true)}}</h1>
{% endif %} {% endif %}
{{ page.content }} {{ page.content }}
@ -126,8 +126,8 @@
{% endblock %} {% endblock %}
</div> </div>
{% block scripts %} {% block scripts %}
<script src="{{ base_url }}/assets/javascripts/application-baa758dc1b.js"></script> <script src="{{ base_url }}/assets/javascripts/application-c7c31dee65.js"></script>
<script>var config={url:{base:"{{ base_url }}"}},app=new Application(config);app.initialize()</script> <script>app.initialize({url:{base:"{{ base_url }}"}})</script>
{% for path in extra_javascript %} {% for path in extra_javascript %}
<script src="{{ path }}"></script> <script src="{{ path }}"></script>
{% endfor %} {% endfor %}

View File

@ -26,10 +26,10 @@
<li class="md-nav__item"> <li class="md-nav__item">
{% set toc_ = page.toc %} {% set toc_ = page.toc %}
<input class="md-toggle md-nav__toggle" data-md-toggle="toc" type="checkbox" id="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 %} {% set toc_ = (toc_ | first).children %}
{% endif %} {% endif %}
{% if toc_ and (toc_ | first) %} {% if toc_ | first is defined %}
<label class="md-nav__link md-nav__link--active" for="toc"> <label class="md-nav__link md-nav__link--active" for="toc">
{{ nav_item.title }} {{ nav_item.title }}
</label> </label>
@ -37,7 +37,7 @@
<a href="{{ nav_item.url }}" title="{{ nav_item.title }}" class="md-nav__link md-nav__link--active"> <a href="{{ nav_item.url }}" title="{{ nav_item.title }}" class="md-nav__link md-nav__link--active">
{{ nav_item.title }} {{ nav_item.title }}
</a> </a>
{% if page.toc %} {% if toc_ | first is defined %}
{% include "partials/toc.html" %} {% include "partials/toc.html" %}
{% endif %} {% endif %}
</li> </li>

View File

@ -1,10 +1,10 @@
{% import "partials/language.html" as lang %} {% import "partials/language.html" as lang %}
<nav class="md-nav md-nav--secondary"> <nav class="md-nav md-nav--secondary">
{% set toc_ = page.toc %} {% 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 %} {% set toc_ = (toc_ | first).children %}
{% endif %} {% endif %}
{% if toc_ and (toc_ | first) %} {% if toc_ | first is defined %}
<label class="md-nav__title" for="toc">{{ lang.t('toc.title') }}</label> <label class="md-nav__title" for="toc">{{ lang.t('toc.title') }}</label>
<ul class="md-nav__list" data-md-scrollfix> <ul class="md-nav__list" data-md-scrollfix>
{% for toc_item in toc_ %} {% for toc_item in toc_ %}

View File

@ -31,7 +31,7 @@ repo_url: https://github.com/squidfunk/mkdocs-material
# Copyright # Copyright
copyright: 'Copyright &copy; 2016 - 2017 Martin Donath' copyright: 'Copyright &copy; 2016 - 2017 Martin Donath'
# Documentation and theme # Theme directory
theme_dir: material theme_dir: material
# Options # Options

View File

@ -27,11 +27,14 @@
"clean": "scripts/clean", "clean": "scripts/clean",
"lint": "scripts/lint", "lint": "scripts/lint",
"start": "scripts/start", "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": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.6.1", "autoprefixer": "^6.6.1",
"babel-core": "^6.0.0",
"babel-eslint": "^7.1.1", "babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10", "babel-loader": "^6.2.10",
"babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-add-module-exports": "^0.2.1",
@ -40,13 +43,12 @@
"babel-preset-es2015": "^6.22.0", "babel-preset-es2015": "^6.22.0",
"babel-register": "^6.18.0", "babel-register": "^6.18.0",
"babel-root-import": "^4.1.5", "babel-root-import": "^4.1.5",
"chai": "^3.5.0",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"css-mqpacker": "^5.0.1", "css-mqpacker": "^5.0.1",
"custom-event-polyfill": "^0.3.0", "custom-event-polyfill": "^0.3.0",
"del": "^2.2.2", "del": "^2.2.2",
"ecstatic": "^2.1.0",
"eslint": "^3.14.0", "eslint": "^3.14.0",
"eslint-plugin-mocha": "^4.8.0",
"fastclick": "^1.0.6", "fastclick": "^1.0.6",
"git-hooks": "^1.1.7", "git-hooks": "^1.1.7",
"gulp": "^3.9.1", "gulp": "^3.9.1",
@ -70,6 +72,27 @@
"gulp-uglify": "^2.0.0", "gulp-uglify": "^2.0.0",
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"js-cookie": "^2.1.3", "js-cookie": "^2.1.3",
"lunr": "^0.7.2",
"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.1.0",
"stylelint": "^7.8.0",
"stylelint-config-standard": "^16.0.0",
"stylelint-order": "^0.2.2",
"stylelint-scss": "^1.4.1",
"through2": "^2.0.3",
"vinyl-paths": "^2.1.0",
"webpack": "^2.2.1",
"webpack-stream": "^3.2.0",
"whatwg-fetch": "^2.0.1",
"yargs": "^6.6.0"
},
"optionalDependencies": {
"chai": "^3.5.0",
"eslint-plugin-mocha": "^4.8.0",
"gemini": "^4.14.3",
"karma": "^1.3.0", "karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0", "karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.1.1", "karma-coverage": "^1.1.1",
@ -78,26 +101,14 @@
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.26", "karma-spec-reporter": "0.0.26",
"karma-webpack": "^2.0.1", "karma-webpack": "^2.0.1",
"lunr": "^0.7.2",
"material-design-color": "^2.3.2",
"material-shadows": "^3.0.1",
"mocha": "^3.2.0", "mocha": "^3.2.0",
"modularscale-sass": "^2.1.1", "moniker": "^0.1.2",
"node-notifier": "^5.0.0", "saucelabs": "^1.4.0",
"selenium-standalone": "^5.9.1", "sauce-connect-launcher": "^1.2.0",
"stylelint": "^7.7.1", "selenium-standalone": "^6.0.0"
"stylelint-config-standard": "^15.0.1",
"stylelint-order": "^0.2.2",
"stylelint-scss": "^1.4.1",
"through2": "^2.0.3",
"vinyl-paths": "^2.1.0",
"webpack": "^1.14.0",
"webpack-stream": "^3.2.0",
"whatwg-fetch": "^2.0.1",
"yargs": "^6.6.0"
}, },
"engines": { "engines": {
"node": ">= 4.5.0" "node": ">= 5.0.0"
}, },
"private": true "private": true
} }

View File

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

View File

@ -28,4 +28,4 @@ if [[ ! -d `npm bin` ]]; then
fi fi
# Run command # Run command
`npm bin`/gulp build --clean --optimize --revision `npm bin`/gulp build --clean --optimize --revision "$@"

View File

@ -28,4 +28,4 @@ if [[ ! -d `npm bin` ]]; then
fi fi
# Run command # Run command
`npm bin`/gulp clean `npm bin`/gulp clean "$@"

View File

@ -28,4 +28,4 @@ if [[ ! -d `npm bin` ]]; then
fi fi
# Run command # Run command
`npm bin`/gulp watch --no-lint `npm 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 "npm install" was executed
if [[ ! -d `npm bin` ]]; then
echo "\"node_modules\" not found:"
echo "npm install"
exit 1
fi
# Run command
`npm 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 "npm install" was executed
if [[ ! -d `npm bin` ]]; then
echo "\"node_modules\" not found:"
echo "npm install"
exit 1
fi
# Run command
`npm bin`/gulp tests:visual:session "$@"

View File

@ -28,4 +28,4 @@ if [[ ! -d `npm bin` ]]; then
fi fi
# Run command # Run command
`npm bin`/gulp test `npm 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

@ -27,231 +27,215 @@ import Material from "./components/Material"
* Application * Application
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
export default class Application { export const initialize = config => {
/** /* Initialize Modernizr and FastClick */
* Create the application new Material.Event.Listener(document, "DOMContentLoaded", () => {
*
* @constructor
* @param {object} config Configuration object
*/
constructor(config) {
this.config_ = config
}
/** /* Test for iOS */
* Initialize all components and listeners Modernizr.addTest("ios", () => {
*/ return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
initialize() { })
/* Initialize Modernizr and FastClick */ /* Test for web application context */
new Material.Event.Listener(document, "DOMContentLoaded", () => { Modernizr.addTest("standalone", () => {
return !!navigator.standalone
})
/* Test for iOS */ /* Attach FastClick to mitigate 300ms delay on touch devices */
Modernizr.addTest("ios", () => { FastClick.attach(document.body)
return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
})
/* Test for web application context */ /* Wrap all data tables for better overflow scrolling */
Modernizr.addTest("standalone", () => { const tables = document.querySelectorAll("table:not([class])")
return !!navigator.standalone Array.prototype.forEach.call(tables, table => {
}) const wrap = document.createElement("div")
wrap.classList.add("md-typeset__table")
if (table.nextSibling) {
table.parentNode.insertBefore(wrap, table.nextSibling)
} else {
table.parentNode.appendChild(wrap)
}
wrap.appendChild(table)
})
/* Attack FastClick to mitigate 300ms delay on touch devices */ /* Force 1px scroll offset to trigger overflow scrolling */
FastClick.attach(document.body) if (Modernizr.ios) {
const scrollable = document.querySelectorAll("[data-md-scrollfix]")
Array.prototype.forEach.call(scrollable, item => {
item.addEventListener("touchstart", () => {
const top = item.scrollTop
/* Wrap all data tables for better overflow scrolling */ /* We're at the top of the container */
const tables = document.querySelectorAll("table:not([class])") if (top === 0) {
Array.prototype.forEach.call(tables, table => { item.scrollTop = 1
const wrap = document.createElement("div")
wrap.classList.add("md-typeset__table")
if (table.nextSibling) {
table.parentNode.insertBefore(wrap, table.nextSibling)
} else {
table.parentNode.appendChild(wrap)
}
wrap.appendChild(table)
})
/* Force 1px scroll offset to trigger overflow scrolling */
if (Modernizr.ios) {
const scrollable = document.querySelectorAll("[data-md-scrollfix]")
Array.prototype.forEach.call(scrollable, item => {
item.addEventListener("touchstart", () => {
const top = item.scrollTop
/* We're at the top of the container */
if (top === 0) {
item.scrollTop = 1
/* We're at the bottom of the container */ /* We're at the bottom of the container */
} else if (top + item.offsetHeight === item.scrollHeight) { } else if (top + item.offsetHeight === item.scrollHeight) {
item.scrollTop = top - 1 item.scrollTop = top - 1
} }
})
}) })
} })
}).listen() }
}).listen()
/* Component: header shadow toggle */ /* Component: header shadow toggle */
new Material.Event.MatchMedia("(min-width: 1220px)", new Material.Event.MatchMedia("(min-width: 1220px)",
new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange"
], new Material.Header.Shadow("[data-md-component=container]")))
/* Component: tabs visibility toggle */
new Material.Event.Listener(window, [ new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange" "scroll", "resize", "orientationchange"
], new Material.Tabs.Toggle("[data-md-component=tabs]")).listen() ], new Material.Header.Shadow("[data-md-component=container]")))
/* Component: sidebar container */ /* Component: tabs visibility toggle */
if (!Modernizr.csscalc) new Material.Event.Listener(window, [
new Material.Event.MatchMedia("(min-width: 960px)", "scroll", "resize", "orientationchange"
new Material.Event.Listener(window, [ ], new Material.Tabs.Toggle("[data-md-component=tabs]")).listen()
"resize", "orientationchange"
], new Material.Sidebar.Container("[data-md-component=container]")))
/* Component: sidebar with navigation */ /* Component: sidebar container */
new Material.Event.MatchMedia("(min-width: 1220px)", if (!Modernizr.csscalc)
new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange"
], new Material.Sidebar.Position("[data-md-component=navigation]")))
/* 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) and (max-width: 1219px)",
new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange"
], new Material.Sidebar.Position("[data-md-component=toc]")))
new Material.Event.MatchMedia("(min-width: 1220px)",
new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange"
], new Material.Sidebar.Position("[data-md-component=toc]")))
/* Component: link blurring for table of contents */
new Material.Event.MatchMedia("(min-width: 960px)", new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(window, "scroll", new Material.Event.Listener(window, [
new Material.Nav.Blur("[data-md-component=toc] [href]"))) "resize", "orientationchange"
], new Material.Sidebar.Container("[data-md-component=container]")))
/* Component: collapsible elements for navigation */ /* Component: sidebar with navigation */
const collapsibles = new Material.Event.MatchMedia("(min-width: 1220px)",
document.querySelectorAll("[data-md-component=collapsible]") new Material.Event.Listener(window, [
Array.prototype.forEach.call(collapsibles, collapse => { "scroll", "resize", "orientationchange"
new Material.Event.MatchMedia("(min-width: 1220px)", ], new Material.Sidebar.Position("[data-md-component=navigation]")))
new Material.Event.Listener(collapse.previousElementSibling, "click",
new Material.Nav.Collapse(collapse)))
})
/* Component: active pane monitor for iOS scrolling fixes */ /* Component: sidebar with table of contents - register two separate
new Material.Event.MatchMedia("(max-width: 1219px)", listeners, as the offset at the top might change */
new Material.Event.Listener( new Material.Event.MatchMedia("(min-width: 960px) and (max-width: 1219px)",
"[data-md-component=navigation] [data-md-toggle]", "change", new Material.Event.Listener(window, [
new Material.Nav.Scrolling("[data-md-component=navigation] nav"))) "scroll", "resize", "orientationchange"
], new Material.Sidebar.Position("[data-md-component=toc]")))
new Material.Event.MatchMedia("(min-width: 1220px)",
new Material.Event.Listener(window, [
"scroll", "resize", "orientationchange"
], new Material.Sidebar.Position("[data-md-component=toc]")))
/* Component: search body lock for mobile */ /* Component: link blurring for table of contents */
new Material.Event.MatchMedia("(max-width: 959px)", new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener("[data-md-toggle=search]", "change", new Material.Event.Listener(window, "scroll",
new Material.Search.Lock("[data-md-toggle=search]"))) new Material.Nav.Blur("[data-md-component=toc] [href]")))
/* Component: search results */ /* Component: collapsible elements for navigation */
new Material.Event.Listener(document.forms.search.query, [ const collapsibles =
"focus", "keyup" document.querySelectorAll("[data-md-component=collapsible]")
], new Material.Search.Result("[data-md-component=result]", () => { Array.prototype.forEach.call(collapsibles, collapse => {
return fetch(`${this.config_.url.base}/mkdocs/search_index.json`, { new Material.Event.MatchMedia("(min-width: 1220px)",
credentials: "same-origin" new Material.Event.Listener(collapse.previousElementSibling, "click",
}).then(response => response.json()) new Material.Nav.Collapse(collapse)))
.then(data => { })
return data.docs.map(doc => {
doc.location = this.config_.url.base + doc.location /* Component: active pane monitor for iOS scrolling fixes */
return doc new Material.Event.MatchMedia("(max-width: 1219px)",
}) new Material.Event.Listener(
"[data-md-component=navigation] [data-md-toggle]", "change",
new Material.Nav.Scrolling("[data-md-component=navigation] nav")))
/* Component: search body lock for mobile */
new Material.Event.MatchMedia("(max-width: 959px)",
new Material.Event.Listener("[data-md-toggle=search]", "change",
new Material.Search.Lock("[data-md-toggle=search]")))
/* Component: search results */
new Material.Event.Listener(document.forms.search.query, [
"focus", "keyup"
], new Material.Search.Result("[data-md-component=result]", () => {
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 = config.url.base + doc.location
return doc
}) })
})).listen()
/* Listener: prevent touches on overlay if navigation is active */
new Material.Event.MatchMedia("(max-width: 1219px)",
new Material.Event.Listener("[data-md-component=overlay]", "touchstart",
ev => ev.preventDefault()))
/* Listener: close drawer when anchor links are clicked */
new Material.Event.MatchMedia("(max-width: 959px)",
new Material.Event.Listener("[data-md-component=navigation] [href^='#']",
"click", () => {
const toggle = document.querySelector("[data-md-toggle=drawer]")
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
}
}))
/* 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)
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", () => {
const toggle = document.querySelector("[data-md-toggle=search]")
if (!toggle.checked) {
toggle.checked = true
toggle.dispatchEvent(new CustomEvent("change"))
}
}))
/* Listener: close search when clicking outside */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(document.body, "click", () => {
const toggle = document.querySelector("[data-md-toggle=search]")
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
}
}))
/* Listener: disable search when ESC key is pressed */
new Material.Event.Listener(window, "keyup", ev => {
const code = ev.keyCode || ev.which
if (code === 27) {
const toggle = document.querySelector("[data-md-toggle=search]")
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
document.forms.search.query.blur()
}
}
}).listen()
/* Listener: fix unclickable toggle due to blur handler */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener("[data-md-toggle=search]", "click",
ev => ev.stopPropagation()))
/* Listener: prevent search from closing when clicking */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener("[data-md-component=search]", "click",
ev => ev.stopPropagation()))
/* Retrieve facts for the given repository type */
;(() => {
const el = document.querySelector("[data-md-source]")
if (!el) return Promise.resolve([])
switch (el.dataset.mdSource) {
case "github": return new Material.Source.Adapter.GitHub(el).fetch()
default: return Promise.resolve([])
}
/* Render repository source information */
})().then(facts => {
const sources = document.querySelectorAll("[data-md-source]")
Array.prototype.forEach.call(sources, source => {
new Material.Source.Repository(source)
.initialize(facts)
}) })
})).listen()
/* Listener: prevent touches on overlay if navigation is active */
new Material.Event.MatchMedia("(max-width: 1219px)",
new Material.Event.Listener("[data-md-component=overlay]", "touchstart",
ev => ev.preventDefault()))
/* Listener: close drawer when anchor links are clicked */
new Material.Event.MatchMedia("(max-width: 959px)",
new Material.Event.Listener("[data-md-component=navigation] [href^='#']",
"click", () => {
const toggle = document.querySelector("[data-md-toggle=drawer]")
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
}
}))
/* 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)
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", () => {
const toggle = document.querySelector("[data-md-toggle=search]")
if (!toggle.checked) {
toggle.checked = true
toggle.dispatchEvent(new CustomEvent("change"))
}
}))
/* Listener: close search when clicking outside */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener(document.body, "click", () => {
const toggle = document.querySelector("[data-md-toggle=search]")
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
}
}))
/* Listener: disable search when ESC key is pressed */
new Material.Event.Listener(window, "keyup", ev => {
const code = ev.keyCode || ev.which
if (code === 27) {
const toggle = document.querySelector("[data-md-toggle=search]")
if (toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new CustomEvent("change"))
document.forms.search.query.blur()
}
}
}).listen()
/* Listener: fix unclickable toggle due to blur handler */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener("[data-md-toggle=search]", "click",
ev => ev.stopPropagation()))
/* Listener: prevent search from closing when clicking */
new Material.Event.MatchMedia("(min-width: 960px)",
new Material.Event.Listener("[data-md-component=search]", "click",
ev => ev.stopPropagation()))
/* Retrieve facts for the given repository type */
;(() => {
const el = document.querySelector("[data-md-source]")
if (!el) return Promise.resolve([])
switch (el.dataset.mdSource) {
case "github": return new Material.Source.Adapter.GitHub(el).fetch()
default: return Promise.resolve([])
}
/* Render repository source information */
})().then(facts => {
const sources = document.querySelectorAll("[data-md-source]")
Array.prototype.forEach.call(sources, source => {
new Material.Source.Repository(source)
.initialize(facts)
}) })
} })
} }

View File

@ -34,14 +34,9 @@ body {
body, body,
input { input {
color: $md-color-black; color: $md-color-black;
// font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-feature-settings: "kern", "onum", "liga"; font-feature-settings: "kern", "onum", "liga";
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 400; font-weight: 400;
// Use system fonts, if browser doesn't support webfonts
.no-fontface & {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
} }
// Proportionally spaced fonts // Proportionally spaced fonts
@ -49,14 +44,9 @@ pre,
code, code,
kbd { kbd {
color: $md-color-black; color: $md-color-black;
// font-family: "Roboto Mono", "Courier New", Courier, monospace;
font-feature-settings: "kern", "onum", "liga"; font-feature-settings: "kern", "onum", "liga";
font-family: "Courier New", Courier, monospace;
font-weight: 400; font-weight: 400;
// Use system fonts, if browser doesn't support webfonts
.no-fontface & {
font-family: "Courier New", Courier, monospace;
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -50,8 +50,8 @@
padding-bottom: 0.8rem; padding-bottom: 0.8rem;
transition: opacity 0.25s; transition: opacity 0.25s;
// [mobile landscape +]: Set proportional width // [tablet +]: Set proportional width
@include break-from-device(mobile landscape) { @include break-from-device(tablet) {
width: 50%; width: 50%;
} }
@ -68,8 +68,8 @@
// Title // Title
.md-footer-nav__title { .md-footer-nav__title {
// [mobile portrait -]: Hide title for previous page // [mobile -]: Hide title for previous page
@include break-to-device(mobile portrait) { @include break-to-device(mobile) {
display: none; display: none;
} }
} }

View File

@ -84,7 +84,7 @@
} }
} }
// Icon buttons // Button with logo
&__button { &__button {
@extend %md-icon, %md-icon__button; @extend %md-icon, %md-icon__button;
@ -141,7 +141,8 @@
color: $md-color-primary; color: $md-color-primary;
} }
// Hovered link // Focused or hovered item
&:focus,
&:hover { &:hover {
color: $md-color-accent; color: $md-color-accent;
} }

View File

@ -56,7 +56,8 @@
{% endif %} {% endif %}
<!-- Generator banner --> <!-- Generator banner -->
<meta name="generator" content="mkdocs+$theme-name$#$theme-version$" /> <meta name="generator"
content="mkdocs-{{ mkdocs_version }}, $theme-name$-$theme-version$" />
{% endblock %} {% endblock %}
<!-- Block: site title --> <!-- Block: site title -->
@ -73,6 +74,20 @@
<script src="{{ base_url }}/assets/javascripts/modernizr.js"></script> <script src="{{ base_url }}/assets/javascripts/modernizr.js"></script>
{% endblock %} {% endblock %}
<!-- Block: stylesheets -->
{% block styles %}
<!-- Theme-related stylesheets -->
<link rel="stylesheet" type="text/css"
href="{{ base_url }}/assets/stylesheets/application.css" />
<!-- Extra color palette -->
{% if config.extra.palette %}
<link rel="stylesheet" type="text/css"
href="{{ base_url }}/assets/stylesheets/application.palette.css" />
{% endif %}
{% endblock %}
<!-- Block: webfonts --> <!-- Block: webfonts -->
{% block fonts %} {% block fonts %}
{% if config.extra.font != "none" %} {% if config.extra.font != "none" %}
@ -99,24 +114,10 @@
href="https://fonts.googleapis.com/icon?family=Material+Icons" /> href="https://fonts.googleapis.com/icon?family=Material+Icons" />
{% endblock %} {% endblock %}
<!-- Block: stylesheets --> <!-- Custom stylesheets -->
{% block styles %} {% for path in extra_css %}
<link rel="stylesheet" type="text/css" href="{{ path }}" />
<!-- Theme-related stylesheets --> {% endfor %}
<link rel="stylesheet" type="text/css"
href="{{ base_url }}/assets/stylesheets/application.css" />
<!-- Extra color palette -->
{% if config.extra.palette %}
<link rel="stylesheet" type="text/css"
href="{{ base_url }}/assets/stylesheets/application.palette.css" />
{% endif %}
<!-- Custom stylesheets -->
{% for path in extra_css %}
<link rel="stylesheet" type="text/css" href="{{ path }}" />
{% endfor %}
{% endblock %}
<!-- Block: custom front matter --> <!-- Block: custom front matter -->
{% block extrahead %}{% endblock %} {% block extrahead %}{% endblock %}
@ -220,11 +221,11 @@
{% block content %} {% block content %}
<!-- <!--
This is a nasty hack that checks whether the content contains Hack: check whether the content contains a h1 headline. If it
a h1 headline. If it doesn't, the page title (or respectively doesn't, the page title (or respectively site name) is used
site name) is used as the main headline. as the main headline.
--> -->
{% if not "\x3ch1 id=" in page.content %} {% if not "\x3ch1" in page.content %}
<h1>{{ page.title | default(config.site_name, true)}}</h1> <h1>{{ page.title | default(config.site_name, true)}}</h1>
{% endif %} {% endif %}
@ -246,17 +247,7 @@
{% block scripts %} {% block scripts %}
<script src="{{ base_url }}/assets/javascripts/application.js"></script> <script src="{{ base_url }}/assets/javascripts/application.js"></script>
<script> <script>
app.initialize({ url: { base: "{{ base_url }}", } });
/* Configuration for application */
var config = {
url: {
base: "{{ base_url }}",
}
};
/* Initialize application */
var app = new Application(config);
app.initialize();
</script> </script>
{% for path in extra_javascript %} {% for path in extra_javascript %}
<script src="{{ path }}"></script> <script src="{{ path }}"></script>

View File

@ -64,13 +64,13 @@
<input class="md-toggle md-nav__toggle" data-md-toggle="toc" <input class="md-toggle md-nav__toggle" data-md-toggle="toc"
type="checkbox" id="toc" /> type="checkbox" id="toc" />
<!-- Nasty hack - see partials/toc.html for more information --> <!-- Hack: see partials/toc.html for more information -->
{% if "\x3ch1 id=" in page.content %} {% if toc_ | first is defined %}
{% set toc_ = (toc_ | first).children %} {% set toc_ = (toc_ | first).children %}
{% endif %} {% endif %}
<!-- Render table of contents, if not empty --> <!-- Render table of contents, if not empty -->
{% if toc_ and (toc_ | first) %} {% if toc_ | first is defined %}
<label class="md-nav__link md-nav__link--active" for="toc"> <label class="md-nav__link md-nav__link--active" for="toc">
{{ nav_item.title }} {{ nav_item.title }}
</label> </label>
@ -81,7 +81,7 @@
</a> </a>
<!-- Show table of contents --> <!-- Show table of contents -->
{% if page.toc %} {% if toc_ | first is defined %}
{% include "partials/toc.html" %} {% include "partials/toc.html" %}
{% endif %} {% endif %}
</li> </li>

View File

@ -27,17 +27,17 @@
{% set toc_ = page.toc %} {% set toc_ = page.toc %}
<!-- <!--
This is a nasty hack that checks whether the content contains a h1 Hack: check whether the content contains a h1 headline. If it does, the
headline. If it does, the top-level anchor must be skipped, since it would top-level anchor must be skipped, since it would be redundant to the link
be redundant to the link to the current page that is located just above the to the current page that is located just above the anchor. Therefore we
anchor. Therefore we directly continue with the children of the anchor. directly continue with the children of the anchor.
--> -->
{% if "\x3ch1 id=" in page.content %} {% if toc_ | first is defined and "\x3ch1 id=" in page.content %}
{% set toc_ = (toc_ | first).children %} {% set toc_ = (toc_ | first).children %}
{% endif %} {% endif %}
<!-- Render item list --> <!-- Render item list -->
{% if toc_ and (toc_ | first) %} {% if toc_ | first is defined %}
<label class="md-nav__title" for="toc">{{ lang.t('toc.title') }}</label> <label class="md-nav__title" for="toc">{{ lang.t('toc.title') }}</label>
<ul class="md-nav__list" data-md-scrollfix> <ul class="md-nav__list" data-md-scrollfix>
{% for toc_item in toc_ %} {% for toc_item in toc_ %}

8
tests/visual/.eslintrc Normal file
View File

@ -0,0 +1,8 @@
{
"globals": {
"gemini": true
},
"rules": {
"no-loop-func": 0
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

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