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:
Rico Sta. Cruz 2017-10-02 06:00:52 +08:00
commit cca2b80996
No known key found for this signature in database
GPG Key ID: CAAD38AE2962619A
37 changed files with 7160 additions and 383 deletions

10
.babelrc Normal file
View File

@ -0,0 +1,10 @@
{
"presets": [
[
"env",
{
"forceAllTransforms": true
}
]
]
}

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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%}

20
_js/app.js Normal file
View File

@ -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$/))

22
_js/behaviors/dismiss.js Normal file
View File

@ -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)
})
})

View File

@ -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')
}
})

17
_js/behaviors/disqus.js Normal file
View File

@ -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)
})

View File

@ -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
})
})

View File

View File

@ -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')
}
})

View File

@ -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
})
})

View File

@ -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) })
}
})

View File

@ -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(' '))
})

View File

@ -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(' '))
})

View File

@ -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)
}
}
})

20
_js/helpers/data.js Normal file
View File

@ -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))
}

28
_js/helpers/dismiss.js Normal file
View File

@ -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]
}

View File

@ -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)
}

71
_js/helpers/permutate.js Normal file
View File

@ -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
}

7
_js/helpers/preview.js Normal file
View File

@ -0,0 +1,7 @@
/**
* Checks if we're in preview mode (?preview=1).
*/
export function isPreview () {
return window.location.search.indexOf('preview=1') !== -1
}

18
_js/helpers/qs.js Normal file
View File

@ -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
}

40
_js/helpers/search.js Normal file
View File

@ -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')
})
}

29
_js/helpers/store.js Normal file
View File

@ -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] || '{}')
}

View File

@ -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() })
})

View File

@ -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>
`;

View File

@ -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)
}
}

63
_js/wrapify/index.js Normal file
View File

@ -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
}
}

View File

@ -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';

View File

@ -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
}
})
]
}

View File

@ -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
}

64
assets/packed/app.js Normal file

File diff suppressed because one or more lines are too long

59
package.json Normal file
View File

@ -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"
]
}
}

1
webpack.config.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./_support/webpack.config.js')

6147
yarn.lock Normal file

File diff suppressed because it is too large Load Diff