Merge branch 'feature/webpack' into gh-pages
* feature/webpack: Use 'yarn run dev' for everything Fix home page search Add extra languages Load fon more efficiently Fix isotope Compress better Add packed Add some tests Remove more jQuery Fix up more behaviors Move some languages into JS bundle Behaviorizes search Trim some more 3rd-party deps Refactor more behaviors Move some behaviors into Webpack Begin to convert script.js to webpack Initial support for Webpack
This commit is contained in:
commit
cca2b80996
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"forceAllTransforms": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -2,16 +2,32 @@
|
|||
|
||||
## Starting a local instance
|
||||
|
||||
This is a mere Jekyll site, and `bundle exec jekyll serve` should be fine. But I suggest you use this instead:
|
||||
This starts Jekyll and Webpack.
|
||||
|
||||
```
|
||||
make
|
||||
yarn install
|
||||
bundle install
|
||||
env PORT=4001 yarn run dev
|
||||
```
|
||||
|
||||
## CSS classes
|
||||
|
||||
See <https://devhints.io/cheatsheet-styles> for a reference on styling.
|
||||
|
||||
## JavaScript
|
||||
|
||||
When updating JavaScript, be sure webpack is running (`yarn run dev` takes care of this).
|
||||
|
||||
This auto-updates `/assets/packed/` with sources in `_js/`.
|
||||
|
||||
## JavaScript tests
|
||||
|
||||
There are also automated tests:
|
||||
|
||||
```
|
||||
yarn run test --watch
|
||||
```
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Each sheet supports these metadata:
|
||||
|
|
11
Makefile
11
Makefile
|
@ -1,11 +0,0 @@
|
|||
PORT ?= 3000
|
||||
|
||||
start: bundle
|
||||
bundle exec jekyll serve --safe --drafts --watch --port ${PORT} --incremental
|
||||
|
||||
build: bundle
|
||||
bundle exec jekyll build --safe
|
||||
|
||||
bundle:
|
||||
ruby -v
|
||||
bundle
|
14
_config.yml
14
_config.yml
|
@ -16,6 +16,12 @@ exclude:
|
|||
- Gemfile.lock
|
||||
- CNAME
|
||||
- vendor
|
||||
- package.json
|
||||
- .babelrc
|
||||
- yarn.lock
|
||||
- package-lock.json
|
||||
- webpack.config.js
|
||||
- node_modules
|
||||
|
||||
# Markdown
|
||||
|
||||
|
@ -40,13 +46,7 @@ defaults:
|
|||
type: article
|
||||
category: "Others"
|
||||
excerpt_separator: "<!--more-->"
|
||||
prism_languages:
|
||||
- jsx
|
||||
- bash
|
||||
- scss
|
||||
- css
|
||||
- elixir
|
||||
- ruby
|
||||
prism_languages: []
|
||||
|
||||
# Site info
|
||||
|
||||
|
|
|
@ -4,26 +4,14 @@
|
|||
{% include meta.html %}
|
||||
{% include polyfills.html %}
|
||||
|
||||
<!-- script -->
|
||||
<script src='{{base}}/assets/packed/app.js?t={{ timestamp }}'></script>
|
||||
|
||||
<!-- 3rd-party libs -->
|
||||
<script src='https://code.jquery.com/jquery-3.1.0.min.js'></script>
|
||||
<script src='https://cdn.jsdelivr.net/npm/isotope-layout@3.0.4/dist/isotope.pkgd.min.js'></script>
|
||||
<script src='https://cdn.jsdelivr.net/npm/prismjs@1.6.0'></script>
|
||||
{% for lang in page.prism_languages %}
|
||||
<script src='https://cdn.jsdelivr.net/npm/prismjs@1.6.0/components/prism-{{lang}}.min.js'></script>{% endfor %}
|
||||
<script src='https://cdn.jsdelivr.net/npm/prismjs@1.6.0/plugins/line-highlight/prism-line-highlight.min.js'></script>
|
||||
|
||||
<!-- 3rd-party CSS -->
|
||||
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/sanitize.css@5.0.0/sanitize.css'>
|
||||
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/prismjs@1.6.0/plugins/line-highlight/prism-line-highlight.css'>
|
||||
|
||||
{% comment %}
|
||||
<!-- No custom fonts -->
|
||||
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Fira+Code:400'>
|
||||
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:300,400,700'>
|
||||
{% endcomment %}
|
||||
{% for lang in page.prism_languages %}<script src='https://cdn.jsdelivr.net/npm/prismjs@1.6.0/components/prism-{{lang}}.min.js'></script>{% endfor %}
|
||||
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Cousine'>
|
||||
|
||||
<!-- 2017 layout -->
|
||||
<link href='{{base}}/assets/2017/style.css?t={{ timestamp }}' rel='stylesheet'>
|
||||
<script src='{{base}}/assets/2017/script.js?t={{ timestamp }}'></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<!-- legacy IE polyfills -->
|
||||
<script src='https://cdn.polyfill.io/v2/polyfill.min.js'></script>
|
||||
<!--[if lt IE 9]>{%comment%}
|
||||
{%endcomment%}<script src='//cdnjs.cloudflare.com/ajax/libs/nwmatcher/1.2.5/nwmatcher.min.js'></script>{%comment%}
|
||||
{%endcomment%}<script src='//cdnjs.cloudflare.com/ajax/libs/json2/20140204/json2.js'></script>{%comment%}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// 3rd party libs
|
||||
window.jQuery = window.$ = require('jquery')
|
||||
window.Prism = require('prismjs')
|
||||
require('prismjs/plugins/line-highlight/prism-line-highlight.min.js')
|
||||
|
||||
// CSS
|
||||
require('sanitize.css')
|
||||
require('prismjs/plugins/line-highlight/prism-line-highlight.css')
|
||||
require('hint.css/hint.min.css')
|
||||
require('prismjs/components/prism-jsx.min.js')
|
||||
require('prismjs/components/prism-bash.min.js')
|
||||
require('prismjs/components/prism-scss.min.js')
|
||||
require('prismjs/components/prism-css.min.js')
|
||||
require('prismjs/components/prism-elixir.min.js')
|
||||
require('prismjs/components/prism-ruby.min.js')
|
||||
|
||||
// All the others
|
||||
function requireAll (r) { r.keys().forEach(r) }
|
||||
requireAll(require.context('./initializers/', true, /\.js$/))
|
||||
requireAll(require.context('./behaviors/', true, /\.js$/))
|
|
@ -0,0 +1,22 @@
|
|||
import closest from 'dom101/closest'
|
||||
import remove from 'dom101/remove'
|
||||
import on from 'dom101/on'
|
||||
import { getData } from '../helpers/data'
|
||||
import onmount from 'onmount'
|
||||
import * as Dismiss from '../helpers/dismiss'
|
||||
|
||||
/**
|
||||
* Dismiss button
|
||||
*/
|
||||
|
||||
onmount('[data-js-dismiss]', function () {
|
||||
const parent = closest(this, '[data-js-dismissable]')
|
||||
const dismissable = getData(parent, 'js-dismissable')
|
||||
const id = (dismissable && dismissable.id) || ''
|
||||
|
||||
on(this, 'click', e => {
|
||||
Dismiss.setDismissed(id)
|
||||
e.preventDefault()
|
||||
if (parent) remove(parent)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
import onmount from 'onmount'
|
||||
import remove from 'dom101/remove'
|
||||
import removeClass from 'dom101/remove-class'
|
||||
|
||||
import { getData } from '../helpers/data'
|
||||
import { isDismissed } from '../helpers/dismiss'
|
||||
import { isPreview } from '../helpers/preview'
|
||||
|
||||
onmount('[data-js-dismissable]', function () {
|
||||
const id = getData(this, 'js-dismissable').id || ''
|
||||
|
||||
if (isPreview() || isDismissed(id)) {
|
||||
remove(this)
|
||||
} else {
|
||||
removeClass(this, '-hide')
|
||||
}
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
import onmount from 'onmount'
|
||||
import injectDisqus from '../helpers/inject_disqus'
|
||||
|
||||
/**
|
||||
* Injects Disqus onto the page.
|
||||
*/
|
||||
|
||||
onmount('[data-js-disqus]', function () {
|
||||
const data = JSON.parse(this.getAttribute('data-js-disqus'))
|
||||
|
||||
window.disqus_config = function () {
|
||||
this.page.url = data.url
|
||||
this.page.identifier = data.identifier
|
||||
}
|
||||
|
||||
injectDisqus(data.host)
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
/* eslint-disable no-new */
|
||||
|
||||
import Isotope from 'isotope-layout/dist/isotope.pkgd.js'
|
||||
import onmount from 'onmount'
|
||||
|
||||
/*
|
||||
* Behavior: Isotope
|
||||
*/
|
||||
|
||||
onmount('[data-js-h3-section-list]', function () {
|
||||
new Isotope(this, {
|
||||
itemSelector: '.h3-section',
|
||||
transitionDuration: 0
|
||||
})
|
||||
})
|
|
@ -0,0 +1,16 @@
|
|||
import remove from 'dom101/remove'
|
||||
import onmount from 'onmount'
|
||||
import addClass from 'dom101/add-class'
|
||||
|
||||
import { isPreview } from '../helpers/preview'
|
||||
|
||||
/*
|
||||
* Behavior: Things to remove when preview mode is on
|
||||
*/
|
||||
|
||||
onmount('[data-js-no-preview]', function (b) {
|
||||
if (isPreview()) {
|
||||
remove(this)
|
||||
addClass(document.documentElement, 'PreviewMode')
|
||||
}
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
import onmount from 'onmount'
|
||||
import on from 'dom101/on'
|
||||
|
||||
/**
|
||||
* Submitting the search form
|
||||
*/
|
||||
|
||||
onmount('[data-js-search-form]', function () {
|
||||
on(this, 'submit', e => {
|
||||
e.preventDefault()
|
||||
|
||||
const link = document.querySelector('a[data-search-index]:visible')
|
||||
const href = link && link.getAttribute('href')
|
||||
|
||||
if (href) window.location = href
|
||||
})
|
||||
})
|
|
@ -0,0 +1,22 @@
|
|||
import onmount from 'onmount'
|
||||
import * as Search from '../helpers/search'
|
||||
import qs from '../helpers/qs'
|
||||
import on from 'dom101/on'
|
||||
|
||||
onmount('[data-js-search-input]', function () {
|
||||
on(this, 'input', () => {
|
||||
const val = this.value
|
||||
|
||||
if (val === '') {
|
||||
Search.showAll()
|
||||
} else {
|
||||
Search.show(val)
|
||||
}
|
||||
})
|
||||
|
||||
const query = (qs(window.location.search) || {}).q
|
||||
if (query && query.length) {
|
||||
this.value = query
|
||||
setTimeout(() => { Search.show(query) })
|
||||
}
|
||||
})
|
|
@ -0,0 +1,24 @@
|
|||
import onmount from 'onmount'
|
||||
import $ from 'jquery'
|
||||
|
||||
// Ensure that search-index is set first
|
||||
import './searchable-item'
|
||||
|
||||
/**
|
||||
* Propagate item search indices to headers
|
||||
*/
|
||||
|
||||
onmount('[data-js-searchable-header]', function () {
|
||||
const $this = $(this)
|
||||
const $els = $this
|
||||
.nextUntil('[data-js-searchable-header]')
|
||||
.filter('[data-search-index]')
|
||||
|
||||
const keywords = $els
|
||||
.map(function () { return $(this).attr('data-search-index') })
|
||||
.get()
|
||||
.join(' ')
|
||||
.split(' ')
|
||||
|
||||
$this.attr('data-search-index', keywords.join(' '))
|
||||
})
|
|
@ -0,0 +1,13 @@
|
|||
import onmount from 'onmount'
|
||||
import permutate from '../helpers/permutate'
|
||||
|
||||
/**
|
||||
* Sets search indices (`data-search-index` attribute)
|
||||
*/
|
||||
|
||||
onmount('[data-js-searchable-item]', function () {
|
||||
const data = JSON.parse(this.getAttribute('data-js-searchable-item') || '{}')
|
||||
const words = permutate(data)
|
||||
|
||||
this.setAttribute('data-search-index', words.join(' '))
|
||||
})
|
|
@ -0,0 +1,21 @@
|
|||
/* eslint-env jest */
|
||||
import qs from '../qs'
|
||||
|
||||
describe('qs()', () => {
|
||||
test('basic', run({
|
||||
input: '?preview=1',
|
||||
output: { preview: '1' }
|
||||
}))
|
||||
|
||||
test('two fragments', run({
|
||||
input: '?a=1&b=2',
|
||||
output: { a: '1', b: '2' }
|
||||
}))
|
||||
|
||||
function run ({ input, output }) {
|
||||
return function () {
|
||||
const result = qs(input)
|
||||
expect(result).toEqual(output)
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Stores and retrieves data from an element. Works like jQuery.data().
|
||||
*/
|
||||
|
||||
export function data (el, key, val) {
|
||||
if (typeof val !== 'undefined') {
|
||||
return getData(el, key)
|
||||
} else {
|
||||
return setData(el, key, val)
|
||||
}
|
||||
}
|
||||
|
||||
export function getData (el, key) {
|
||||
const str = el.getAttribute('data-' + key)
|
||||
return JSON.parse(str || '{}')
|
||||
}
|
||||
|
||||
export function setData (el, key, val) {
|
||||
el.setAttribute('data-' + key, JSON.stringify(val))
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import * as Store from './store'
|
||||
|
||||
/**
|
||||
* Dismisses an announcement.
|
||||
*
|
||||
* @example
|
||||
* setDismissed('2017-09-02-happy-birthday')
|
||||
*/
|
||||
|
||||
export function setDismissed (id) {
|
||||
Store.update('dismissed', function (data) {
|
||||
data[id] = true
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an announcement has been dismissed before.
|
||||
*
|
||||
* @example
|
||||
* setDismissed('2017-09-02-happy-birthday')
|
||||
* isDismissed('2017-09-02-happy-birthday') => true
|
||||
*/
|
||||
|
||||
export function isDismissed (id) {
|
||||
const data = Store.fetch('dismissed')
|
||||
return data && data[id]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Injects disqus's scripts into the page.
|
||||
*
|
||||
* @example
|
||||
* injectDisqus('devhints.disqus.com')
|
||||
*/
|
||||
|
||||
export default function injectDisqus (host) {
|
||||
const d = document
|
||||
const s = d.createElement('script')
|
||||
s.src = 'https://' + host + '/embed.js'
|
||||
s.setAttribute('data-timestamp', +new Date())
|
||||
;(d.head || d.body).appendChild(s)
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Permutates a searcheable item.
|
||||
*
|
||||
* permutate({
|
||||
* slug: 'hello-world',
|
||||
* category: 'greetings'
|
||||
* })
|
||||
*/
|
||||
|
||||
export default function permutate (data) {
|
||||
let words = []
|
||||
if (data.slug) {
|
||||
words = words.concat(permutateString(data.slug))
|
||||
}
|
||||
if (data.category) {
|
||||
words = words.concat(permutateString(data.category))
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
/*
|
||||
* Permutates strings.
|
||||
*
|
||||
* @example
|
||||
* permutateString('hi joe')
|
||||
* => ['h', 'hi', 'j', 'jo', 'joe']
|
||||
*/
|
||||
|
||||
export function permutateString (str) {
|
||||
let words = []
|
||||
let inputs = splitwords(str)
|
||||
|
||||
inputs.forEach(word => {
|
||||
words = words.concat(permutateWord(word))
|
||||
})
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
/**
|
||||
* Permutates a word.
|
||||
*
|
||||
* @example
|
||||
* permutateWord('hello')
|
||||
* => ['h', 'he', 'hel', 'hell', 'hello']
|
||||
*/
|
||||
|
||||
export function permutateWord (str) {
|
||||
let words = []
|
||||
const len = str.length
|
||||
for (var i = 1; i <= len; ++i) {
|
||||
words.push(str.substr(0, i))
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for splitting to words.
|
||||
*
|
||||
* @example
|
||||
* splitWords('Hello, world!')
|
||||
* => ['hello', 'world']
|
||||
*/
|
||||
|
||||
export function splitwords (str) {
|
||||
const words = str.toLowerCase()
|
||||
.split(/[ \/\-_]/)
|
||||
.filter(k => k && k.length !== 0)
|
||||
|
||||
return words
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Checks if we're in preview mode (?preview=1).
|
||||
*/
|
||||
|
||||
export function isPreview () {
|
||||
return window.location.search.indexOf('preview=1') !== -1
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Helper: minimal qs implementation
|
||||
*/
|
||||
|
||||
export default function qs (search) {
|
||||
search = search.substr(1)
|
||||
const parts = search.split('&').map(p => p.split('='))
|
||||
return parts.reduce((result, part) => {
|
||||
result[part[0]] = qsdecode(part[1])
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function qsdecode (string) {
|
||||
if (!string) string = ''
|
||||
string = string.replace(/\+/g, ' ')
|
||||
return string
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { splitwords } from './permutate'
|
||||
import qsa from 'dom101/query-selector-all'
|
||||
|
||||
/**
|
||||
* Show everything.
|
||||
*
|
||||
* @example
|
||||
* Search.showAll()
|
||||
*/
|
||||
|
||||
export function showAll () {
|
||||
qsa('[data-search-index]').forEach(el => {
|
||||
el.removeAttribute('aria-hidden')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a given keyword.
|
||||
*
|
||||
* @example
|
||||
* Search.show('hello')
|
||||
*/
|
||||
|
||||
export function show (val) {
|
||||
const keywords = splitwords(val)
|
||||
|
||||
if (!keywords.length) return showAll()
|
||||
|
||||
const selectors = keywords
|
||||
.map(k => `[data-search-index~=${JSON.stringify(k)}]`)
|
||||
.join('')
|
||||
|
||||
qsa('[data-search-index]').forEach(el => {
|
||||
el.setAttribute('aria-hidden', true)
|
||||
})
|
||||
|
||||
qsa(selectors).forEach(el => {
|
||||
el.removeAttribute('aria-hidden')
|
||||
})
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Updates a local storage key. If it doesn't exist, it defaults to an empty
|
||||
* object.
|
||||
*
|
||||
* @example
|
||||
* update('dismissed', (data) => {
|
||||
* data.lol = true
|
||||
* return data
|
||||
* })
|
||||
*/
|
||||
|
||||
export function update (key, fn) {
|
||||
if (!window.localStorage) return
|
||||
let data = JSON.parse(window.localStorage[key] || '{}')
|
||||
data = fn(data)
|
||||
window.localStorage[key] = JSON.stringify(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a local storage key.
|
||||
*
|
||||
* @example
|
||||
* const data = fetch('dismissed')
|
||||
*/
|
||||
|
||||
export function fetch (key) {
|
||||
if (!window.localStorage) return
|
||||
return JSON.parse(window.localStorage[key] || '{}')
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import wrapify from '../wrapify'
|
||||
import ready from 'dom101/ready'
|
||||
import onmount from 'onmount'
|
||||
|
||||
/**
|
||||
* Behavior: Wrapping
|
||||
*/
|
||||
|
||||
ready(() => {
|
||||
const body = document.querySelector('[data-js-main-body]')
|
||||
if (body) { wrapify(body) }
|
||||
setTimeout(() => { onmount() })
|
||||
})
|
|
@ -0,0 +1,238 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`h3 with class 1`] = `
|
||||
<div>
|
||||
|
||||
|
||||
<div
|
||||
class="h2-section -hello"
|
||||
>
|
||||
<div
|
||||
class="body h3-section-list -hello"
|
||||
data-js-h3-section-list=""
|
||||
>
|
||||
<div
|
||||
class="h3-section -hello"
|
||||
>
|
||||
<h3
|
||||
class="-hello"
|
||||
>
|
||||
install
|
||||
</h3>
|
||||
<div
|
||||
class="body -hello"
|
||||
>
|
||||
<p>
|
||||
(install)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`multiple h2s 1`] = `
|
||||
<div>
|
||||
|
||||
|
||||
<div
|
||||
class="h2-section"
|
||||
>
|
||||
<h2>
|
||||
multiple h2
|
||||
</h2>
|
||||
<div
|
||||
class="body h3-section-list"
|
||||
data-js-h3-section-list=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="h2-section"
|
||||
>
|
||||
<h2>
|
||||
|
||||
|
||||
|
||||
</h2>
|
||||
<div
|
||||
class="body h3-section-list"
|
||||
data-js-h3-section-list=""
|
||||
>
|
||||
<div
|
||||
class="h3-section"
|
||||
>
|
||||
<h3>
|
||||
install
|
||||
</h3>
|
||||
<div
|
||||
class="body"
|
||||
>
|
||||
<p>
|
||||
(install)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="h3-section"
|
||||
>
|
||||
<h3>
|
||||
usage
|
||||
</h3>
|
||||
<div
|
||||
class="body"
|
||||
>
|
||||
<p>
|
||||
(usage)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div
|
||||
class="h2-section"
|
||||
>
|
||||
<h2>
|
||||
getting started
|
||||
</h2>
|
||||
<div
|
||||
class="body h3-section-list"
|
||||
data-js-h3-section-list=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="h2-section"
|
||||
>
|
||||
<h2>
|
||||
|
||||
|
||||
|
||||
</h2>
|
||||
<div
|
||||
class="body h3-section-list"
|
||||
data-js-h3-section-list=""
|
||||
>
|
||||
<div
|
||||
class="h3-section"
|
||||
>
|
||||
<h3>
|
||||
first
|
||||
</h3>
|
||||
<div
|
||||
class="body"
|
||||
>
|
||||
<p>
|
||||
(first)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="h3-section"
|
||||
>
|
||||
<h3>
|
||||
second
|
||||
</h3>
|
||||
<div
|
||||
class="body"
|
||||
>
|
||||
<p>
|
||||
(second)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`simple usage 1`] = `
|
||||
<div>
|
||||
|
||||
|
||||
<div
|
||||
class="h2-section"
|
||||
>
|
||||
<h2>
|
||||
simple usage
|
||||
</h2>
|
||||
<div
|
||||
class="body h3-section-list"
|
||||
data-js-h3-section-list=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="h2-section"
|
||||
>
|
||||
<h2>
|
||||
|
||||
|
||||
|
||||
</h2>
|
||||
<div
|
||||
class="body h3-section-list"
|
||||
data-js-h3-section-list=""
|
||||
>
|
||||
<div
|
||||
class="h3-section"
|
||||
>
|
||||
<h3>
|
||||
install
|
||||
</h3>
|
||||
<div
|
||||
class="body"
|
||||
>
|
||||
<p>
|
||||
(install)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="h3-section"
|
||||
>
|
||||
<h3>
|
||||
usage
|
||||
</h3>
|
||||
<div
|
||||
class="body"
|
||||
>
|
||||
<p>
|
||||
(usage)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,56 @@
|
|||
/* eslint-env jest */
|
||||
import wrapify from '../index'
|
||||
import $ from 'jquery'
|
||||
|
||||
it('simple usage', run(`
|
||||
<div>
|
||||
<h2>simple usage<h2>
|
||||
|
||||
<h3>install</h3>
|
||||
<p>(install)</p>
|
||||
|
||||
<h3>usage</h3>
|
||||
<p>(usage)</p>
|
||||
</div>
|
||||
`, $div => {
|
||||
expect($div.find('.h2-section .h3-section-list .h3-section').length).toEqual(2)
|
||||
}))
|
||||
|
||||
it('h3 with class', run(`
|
||||
<div>
|
||||
<h3 class='-hello'>install</h3>
|
||||
<p>(install)</p>
|
||||
</div>
|
||||
`, $div => {
|
||||
expect($div.find('div.h3-section.-hello').length).toEqual(1)
|
||||
expect($div.find('div.h3-section-list.-hello').length).toEqual(1)
|
||||
}))
|
||||
|
||||
it('multiple h2s', run(`
|
||||
<div>
|
||||
<h2>multiple h2<h2>
|
||||
|
||||
<h3>install</h3>
|
||||
<p>(install)</p>
|
||||
|
||||
<h3>usage</h3>
|
||||
<p>(usage)</p>
|
||||
|
||||
<h2>getting started<h2>
|
||||
|
||||
<h3>first</h3>
|
||||
<p>(first)</p>
|
||||
|
||||
<h3>second</h3>
|
||||
<p>(second)</p>
|
||||
</div>
|
||||
`))
|
||||
|
||||
function run (input, fn) {
|
||||
return function () {
|
||||
const $div = $(input)
|
||||
wrapify($div[0])
|
||||
expect($div[0]).toMatchSnapshot()
|
||||
if (fn) fn($div)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import $ from 'jquery'
|
||||
|
||||
/*
|
||||
* Wraps h2 sections into h2-section.
|
||||
* Wraps h3 sections into h3-section.
|
||||
*/
|
||||
|
||||
export default function wrapify (root) {
|
||||
const $root = $(root)
|
||||
const $h2sections = groupify($root, {
|
||||
tag: 'h2',
|
||||
wrapper: '<div class="h2-section">',
|
||||
body: '<div class="body h3-section-list" data-js-h3-section-list>'
|
||||
})
|
||||
|
||||
$h2sections.each(function () {
|
||||
const $body = $(this).children('[data-js-h3-section-list]')
|
||||
|
||||
groupify($body, {
|
||||
tag: 'h3',
|
||||
wrapper: '<div class="h3-section">',
|
||||
body: '<div class="body">'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Groups stuff
|
||||
*/
|
||||
|
||||
export function groupify ($this, { tag, wrapper, body }) {
|
||||
const $first = $this.children(':first-child')
|
||||
let $result = $()
|
||||
|
||||
// Handle the markup before the first h2
|
||||
if (!$first.is(tag)) {
|
||||
const $sibs = $first.nextUntil(tag)
|
||||
$result = $result.add(wrap($first, null, $first.add($sibs)))
|
||||
}
|
||||
|
||||
$this.children(tag).each(function () {
|
||||
const $sibs = $(this).nextUntil(tag)
|
||||
const $heading = $(this)
|
||||
$result = $result.add(wrap($heading, $heading, $sibs))
|
||||
})
|
||||
|
||||
return $result
|
||||
|
||||
function wrap ($pivot, $first, $sibs) {
|
||||
const $wrap = $(wrapper)
|
||||
$wrap.addClass($pivot.attr('class'))
|
||||
$pivot.before($wrap)
|
||||
|
||||
const $body = $(body)
|
||||
$body.addClass($pivot.attr('class'))
|
||||
$body.append($sibs)
|
||||
|
||||
if ($first) $wrap.append($first)
|
||||
$wrap.append($body)
|
||||
|
||||
return $wrap
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
@import url('https://unpkg.com/hint.css@2.5.0/hint.min.css');
|
||||
@import url('https://fonts.googleapis.com/css?family=Cousine');
|
||||
@import './variables';
|
||||
@import '../vendor/modularscale/modularscale';
|
||||
@import '../vendor/iconfonts/ionicons@3';
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
const join = require('path').resolve
|
||||
const webpack = require('webpack')
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
context: join(__dirname, '..'),
|
||||
entry: {
|
||||
app: './_js/app.js'
|
||||
},
|
||||
output: {
|
||||
path: join(__dirname, '..', 'assets', 'packed'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{ loader: 'babel-loader' }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{ loader: 'style-loader' },
|
||||
{ loader: 'css-loader' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
stats: 'minimal',
|
||||
plugins: [
|
||||
// Don't include debug symbols ever
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production'
|
||||
}),
|
||||
|
||||
// Always minify, even in development.
|
||||
new UglifyJSPlugin({
|
||||
uglifyOptions: {
|
||||
compress: true,
|
||||
mangle: true
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
|
@ -1,343 +0,0 @@
|
|||
/*
|
||||
* Behavior: preview=1
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
if (~window.location.search.indexOf('preview=1')) {
|
||||
$('[data-js-no-preview]').remove()
|
||||
$('html').addClass('PreviewMode')
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* Behavior: Wrapping
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
const $root = $('[data-js-main-body]')
|
||||
wrapify($root)
|
||||
})
|
||||
|
||||
/*
|
||||
* Behavior: Isotope
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
$('[data-js-h3-section-list]').each(function () {
|
||||
var iso = new Isotope(this, {
|
||||
itemSelector: '.h3-section',
|
||||
transitionDuration: 0
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Behavior: Search
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
$('[data-js-searchable-item]').each(function () {
|
||||
const $this = $(this)
|
||||
const data = $this.data('js-searchable-item')
|
||||
const words = permutate(data)
|
||||
|
||||
$this.attr('data-search-index', words.join(' '))
|
||||
})
|
||||
|
||||
// Propagate item search indices to headers
|
||||
$('[data-js-searchable-header]').each(function () {
|
||||
const $this = $(this)
|
||||
const $els = $this
|
||||
.nextUntil('[data-js-searchable-header]')
|
||||
.filter('[data-search-index]')
|
||||
|
||||
const keywords = $els
|
||||
.map(function () { return $(this).attr('data-search-index') })
|
||||
.get()
|
||||
.join(' ')
|
||||
.split(' ')
|
||||
|
||||
$this.attr('data-search-index', keywords.join(' '))
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Behavior: search input
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
$('[data-js-search-input]').each(function () {
|
||||
const $this = $(this)
|
||||
const val = $this.val()
|
||||
|
||||
$this.on('input', () => {
|
||||
const val = $this.val()
|
||||
|
||||
if (val === '') {
|
||||
Search.showAll()
|
||||
} else {
|
||||
Search.show(val)
|
||||
}
|
||||
})
|
||||
|
||||
const query = (qs(window.location.search) || {}).q
|
||||
if (query && query.length) {
|
||||
$this.val(query)
|
||||
Search.show(query)
|
||||
}
|
||||
})
|
||||
|
||||
$('[data-js-search-form]').each(function () {
|
||||
const $this = $(this)
|
||||
|
||||
$this.on('submit', e => {
|
||||
e.preventDefault()
|
||||
const href = $('a[data-search-index]:visible').eq(0).attr('href')
|
||||
if (href) window.location = href
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Behavior: Disqus
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
$('[data-js-disqus]').each(function () {
|
||||
const $this = $(this)
|
||||
const data = $this.data('js-disqus')
|
||||
|
||||
window.disqus_config = function () {
|
||||
this.page.url = data.url
|
||||
this.page.identifier = data.identifier
|
||||
}
|
||||
|
||||
injectDisqus(data.host)
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Behavior: dismiss button
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
$('[data-js-dismiss]').each(function () {
|
||||
var $button = $(this)
|
||||
var $parent = $button.closest('[data-js-dismissable]')
|
||||
var id = $parent.data('js-dismissable').id || ''
|
||||
|
||||
$button.on('click', function (e) {
|
||||
Dismiss.setDismissed(id)
|
||||
e.preventDefault()
|
||||
$parent.remove()
|
||||
})
|
||||
})
|
||||
|
||||
$('[data-js-dismissable]').each(function () {
|
||||
var $this = $(this)
|
||||
var id = $this.data('js-dismissable').id || ''
|
||||
const isDismissed = Dismiss.isDismissed(id)
|
||||
if (isDismissed || window.location.search.indexOf('preview') !== -1) {
|
||||
$this.remove()
|
||||
} else {
|
||||
$this.removeClass('-hide')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* Helper: dismissed
|
||||
*/
|
||||
|
||||
const Dismiss = {
|
||||
setDismissed: function (id) {
|
||||
Store.update('dismissed', function (data) {
|
||||
data[id] = true
|
||||
return data
|
||||
})
|
||||
},
|
||||
|
||||
isDismissed: function (id) {
|
||||
const data = Store.fetch('dismissed')
|
||||
return data && data[id]
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Simple LocalStorage shim
|
||||
*/
|
||||
|
||||
const Store = {
|
||||
update: function (key, fn) {
|
||||
if (!window.localStorage) return
|
||||
let data = JSON.parse(window.localStorage[key] || '{}')
|
||||
data = fn(data)
|
||||
window.localStorage[key] = JSON.stringify(data)
|
||||
},
|
||||
|
||||
fetch: function (key) {
|
||||
if (!window.localStorage) return
|
||||
return JSON.parse(window.localStorage[key] || '{}')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper: injects disqus
|
||||
*/
|
||||
|
||||
function injectDisqus (host) {
|
||||
var d = document, s = d.createElement('script')
|
||||
s.src = 'https://' + host + '/embed.js'
|
||||
s.setAttribute('data-timestamp', +new Date())
|
||||
;(d.head || d.body).appendChild(s)
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper for splitting to words
|
||||
*/
|
||||
|
||||
function splitwords (str) {
|
||||
const words = str.toLowerCase()
|
||||
.split(/[ \/\-_]/)
|
||||
.filter(k => k && k.length !== 0)
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
/*
|
||||
* Search
|
||||
*/
|
||||
|
||||
const Search = {
|
||||
showAll () {
|
||||
$('[data-search-index]').removeAttr('aria-hidden')
|
||||
},
|
||||
|
||||
show (val) {
|
||||
const keywords = splitwords(val)
|
||||
|
||||
if (!keywords.length) return Search.showAll()
|
||||
|
||||
const selectors = keywords
|
||||
.map(k => `[data-search-index~=${JSON.stringify(k)}]`)
|
||||
.join('')
|
||||
|
||||
$('[data-search-index]').attr('aria-hidden', true)
|
||||
$(selectors).removeAttr('aria-hidden')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Permutator
|
||||
*/
|
||||
|
||||
function permutate (data) {
|
||||
let words = []
|
||||
if (data.slug) {
|
||||
words = words.concat(permutateString(data.slug))
|
||||
}
|
||||
if (data.category) {
|
||||
words = words.concat(permutateString(data.category))
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
function permutateString (str) {
|
||||
let words = []
|
||||
let inputs = splitwords(str)
|
||||
|
||||
inputs.forEach(word => {
|
||||
words = words.concat(permutateWord(word))
|
||||
})
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
function permutateWord (str) {
|
||||
let words = []
|
||||
const len = str.length
|
||||
for (var i = 1; i <= len; ++i) {
|
||||
words.push(str.substr(0, i))
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
/*
|
||||
* Wraps h2 sections into h2-section.
|
||||
* Wraps h3 sections into h3-section.
|
||||
*/
|
||||
|
||||
function wrapify ($root) {
|
||||
const $h2sections = groupify($root, {
|
||||
tag: 'h2',
|
||||
wrapper: '<div class="h2-section">',
|
||||
body: '<div class="body h3-section-list" data-js-h3-section-list>'
|
||||
})
|
||||
|
||||
$h2sections.each(function () {
|
||||
const $body = $(this).children('[data-js-h3-section-list]')
|
||||
|
||||
groupify($body, {
|
||||
tag: 'h3',
|
||||
wrapper: '<div class="h3-section">',
|
||||
body: '<div class="body">'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Groups stuff
|
||||
*/
|
||||
|
||||
function groupify ($this, { tag, wrapper, body }) {
|
||||
const $first = $this.children(':first-child')
|
||||
let $result = $()
|
||||
|
||||
// Handle the markup before the first h2
|
||||
if (!$first.is(tag)) {
|
||||
const $sibs = $first.nextUntil(tag)
|
||||
$result = $result.add(wrap($first, null, $first.add($sibs)))
|
||||
}
|
||||
|
||||
$this.children(tag).each(function () {
|
||||
const $sibs = $(this).nextUntil(tag)
|
||||
const $heading = $(this)
|
||||
$result = $result.add(wrap($heading, $heading, $sibs))
|
||||
})
|
||||
|
||||
return $result
|
||||
|
||||
function wrap ($pivot, $first, $sibs) {
|
||||
const $wrap = $(wrapper)
|
||||
$wrap.addClass($pivot.attr('class'))
|
||||
$pivot.before($wrap)
|
||||
|
||||
const $body = $(body)
|
||||
$body.addClass($pivot.attr('class'))
|
||||
$body.append($sibs)
|
||||
|
||||
if ($first) $wrap.append($first)
|
||||
$wrap.append($body)
|
||||
|
||||
return $wrap
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper: minimal qs implementation
|
||||
*/
|
||||
|
||||
function qs (search) {
|
||||
search = search.substr(1)
|
||||
const parts = search.split('&').map(p => p.split('='))
|
||||
return parts.reduce((result, part) => {
|
||||
result[part[0]] = qsdecode(part[1])
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
function qsdecode (string) {
|
||||
if (!string) string = ''
|
||||
string = string.replace(/\+/g, ' ')
|
||||
return string
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "cheatsheets",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/rstacruz/cheatsheets.git",
|
||||
"author": "Rico Sta. Cruz <rstacruz@users.noreply.github.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"babel-core": "6.26.0",
|
||||
"babel-eslint": "8.0.1",
|
||||
"babel-jest": "21.2.0",
|
||||
"babel-loader": "7.1.2",
|
||||
"babel-preset-env": "1.6.0",
|
||||
"css-loader": "0.28.7",
|
||||
"eslint-plugin-flowtype": "2.37.0",
|
||||
"jest": "21.2.1",
|
||||
"jest-html": "1.3.5",
|
||||
"npm-run-all": "4.1.1",
|
||||
"prettier-standard": "7.0.1",
|
||||
"standard": "10.0.3",
|
||||
"style-loader": "0.18.2",
|
||||
"uglifyjs-webpack-plugin": "0.4.6",
|
||||
"webpack": "3.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "run-p dev:webpack dev:jekyll",
|
||||
"dev:webpack": "webpack --watch --colors -p",
|
||||
"dev:jekyll": "bundle exec jekyll serve --safe --drafts --watch --port $PORT --incremental",
|
||||
"prepublish": "webpack -p",
|
||||
"test": "jest",
|
||||
"test:all": "run-s test lint",
|
||||
"lint": "standard -v",
|
||||
"jest-html": "jest-html"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "6.26.0",
|
||||
"dom101": "2.0.1",
|
||||
"hint.css": "2.5.0",
|
||||
"isotope-layout": "3.0.4",
|
||||
"jquery": "3.2.1",
|
||||
"onmount": "1.3.0",
|
||||
"prismjs": "1.8.1",
|
||||
"sanitize.css": "5.0.0"
|
||||
},
|
||||
"standard": {
|
||||
"parser": "babel-eslint",
|
||||
"ignore": [
|
||||
"assets/script.js"
|
||||
],
|
||||
"plugins": [
|
||||
"flowtype"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"<rootDir>/node_modules/jest-html"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('./_support/webpack.config.js')
|
Loading…
Reference in New Issue