1
0
mirror of https://github.com/onkelbeh/cheatsheets.git synced 2025-06-14 22:27:33 +02:00

Major rewrite (!) (#2130)

This commit is contained in:
Rico Sta. Cruz 2024-03-28 19:59:22 +11:00 committed by GitHub
parent bf059536c6
commit 44bdd413fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
263 changed files with 11279 additions and 13673 deletions

View File

@ -1,11 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"targets": "> 2%"
}
]
]
}

View File

@ -1 +0,0 @@
node_modules

37
.eslintrc.cjs Normal file
View File

@ -0,0 +1,37 @@
/* eslint-env node */
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:astro/recommended',
'prettier'
],
env: {
browser: true // enables window, document, etc
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
ignorePatterns: ['dist/**'],
overrides: [
{
files: ['*.test.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off'
}
},
{
files: ['*.astro'],
parser: 'astro-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
extraFileExtensions: ['.astro']
}
// rules: {
// override/add rules settings here, such as:
// "astro/no-set-html-directive": "error"
// },
}
]
}

1
.gitattributes vendored
View File

@ -1 +0,0 @@
yarn.lock binary

View File

Before

Width:  |  Height:  |  Size: 504 KiB

After

Width:  |  Height:  |  Size: 504 KiB

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

@ -1,22 +1,35 @@
name: Build and test
on: [push, pull_request]
name: Run tests
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
- name: Use Ruby
uses: ruby/setup-ruby@v1
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- uses: pnpm/action-setup@v3
with: { run_install: false }
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
- run: yarn test:smoke
# https://github.com/pnpm/action-setup?tab=readme-ov-file#use-cache-to-reduce-installation-time
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install playwright browsers
run: pnpm playwright install --with-deps chromium
- name: Run tests
run: pnpm run ci

35
.gitignore vendored
View File

@ -1,11 +1,26 @@
_output
_site
.jekyll-metadata
/node_modules
/vendor
.idea/
.cache/
# build output
dist/
# generated types
.astro/
# Generated by 'yarn dev'
/_includes/2017/critical/*
/assets/packed/*
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
.cache
# playwright
test-results

View File

@ -1,15 +0,0 @@
image: gitpod/workspace-full
ports:
- port: 4001
onOpen: open-preview
tasks:
- init: yarn install && bundle install
command: env PORT=4001 yarn run dev
github:
# Prebuild the docker image for gitpod - https://www.gitpod.io/docs/prebuilds/
prebuilds:
# enable for the master/default branch
master: true

View File

@ -1 +1 @@
18.19.1
20.11.1

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
src/sass/vendor
vendor
.cache
dist
*.md
pnpm-lock.yaml

View File

@ -1,6 +1,6 @@
{
"semi": false,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "none"
"trailingComma": "none",
"plugins": ["prettier-plugin-astro"]
}

View File

@ -1,13 +0,0 @@
{
"*.md": {
"type": "cheat",
"template": [
"---",
"title: {basename|capitalize}",
"category: Ruby",
"layout: 2017/sheet",
"updated: DATE",
"---"
]
}
}

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

8
.vscode/markdown.code-snippets vendored Normal file
View File

@ -0,0 +1,8 @@
{
"Test snip": {
"prefix": ["about"],
"body": "Copyright. Foo Corp 2028",
"description": "Adds copyright...",
"scope": "markdown"
}
}

View File

@ -1,5 +0,0 @@
---
layout: 2017/not_found
type: error
permalink: /404.html
---

View File

@ -19,7 +19,7 @@ Or using a button:<br>
To preview the website you need to first build it then you can navigate to file that you are trying to contribute and preview directly.
<img src='_docs/images/gitpod_preview_tut.png' width=828 height=459/>
<img src='.github/images/gitpod_preview_tut.png' width=828 height=459/>
## Starting a local instance

View File

@ -1,11 +0,0 @@
FROM ruby:2.7.1
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update -qq && apt-get install -qq --no-install-recommends \
nodejs \
yarn \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app
WORKDIR /app

View File

@ -1,5 +1,4 @@
source 'https://rubygems.org'
gem 'webrick'
gem 'github-pages', group: :jekyll_plugins
gem 'json'
gem 'csv'
source "https://rubygems.org"
gem "minitest"
gem "kramdown"
gem "kramdown-parser-gfm"

View File

@ -1,293 +1,21 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (7.1.3)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
base64 (0.2.0)
bigdecimal (3.1.6)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.23.10)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
csv (3.2.8)
dnsruby (1.70.0)
simpleidn (~> 0.2.1)
drb (2.2.0)
ruby2_keywords
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
ethon (0.16.0)
ffi (>= 1.15.0)
eventmachine (1.2.7)
execjs (2.9.1)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-net_http (3.1.0)
net-http
ffi (1.16.3)
forwardable-extended (2.6.0)
gemoji (3.0.1)
github-pages (228)
github-pages-health-check (= 1.17.9)
jekyll (= 3.9.3)
jekyll-avatar (= 0.7.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.4.0)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.15.1)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.13.0)
jekyll-include-cache (= 0.2.1)
jekyll-mentions (= 1.6.0)
jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.3.0)
jekyll-redirect-from (= 0.16.0)
jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.3)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.8.0)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.2.0)
jekyll-theme-cayman (= 0.2.0)
jekyll-theme-dinky (= 0.2.0)
jekyll-theme-hacker (= 0.2.0)
jekyll-theme-leap-day (= 0.2.0)
jekyll-theme-merlot (= 0.2.0)
jekyll-theme-midnight (= 0.2.0)
jekyll-theme-minimal (= 0.2.0)
jekyll-theme-modernist (= 0.2.0)
jekyll-theme-primer (= 0.6.0)
jekyll-theme-slate (= 0.2.0)
jekyll-theme-tactile (= 0.2.0)
jekyll-theme-time-machine (= 0.2.0)
jekyll-titles-from-headings (= 0.5.3)
jemoji (= 0.12.0)
kramdown (= 2.3.2)
kramdown-parser-gfm (= 1.1.0)
liquid (= 4.0.4)
mercenary (~> 0.3)
minima (= 2.5.1)
nokogiri (>= 1.13.6, < 2.0)
rouge (= 3.26.0)
terminal-table (~> 1.4)
github-pages-health-check (1.17.9)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (~> 4.0)
public_suffix (>= 3.0, < 5.0)
typhoeus (~> 1.3)
html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.8.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
jekyll (3.9.3)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (>= 0.7, < 2)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (>= 1.17, < 3)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.7.0)
jekyll (>= 3.0, < 5.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.4.0)
commonmarker (~> 0.22)
jekyll-commonmark-ghpages (0.4.0)
commonmarker (~> 0.23.7)
jekyll (~> 3.9.0)
jekyll-commonmark (~> 1.4.0)
rouge (>= 2.0, < 5.0)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.15.1)
jekyll (>= 3.7, < 5.0)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.13.0)
jekyll (>= 3.4, < 5.0)
octokit (~> 4.0, != 4.4.0)
jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0)
jekyll-mentions (1.6.0)
html-pipeline (~> 2.3)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
jekyll (>= 3.0, < 5.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
jekyll-redirect-from (0.16.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-remote-theme (0.4.3)
addressable (~> 2.0)
jekyll (>= 3.5, < 5.0)
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
rubyzip (>= 1.3.0, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0)
jekyll-theme-architect (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.6.0)
jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.12.0)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
json (2.7.1)
kramdown (2.3.2)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.21.2)
mutex_m (0.2.0)
net-http (0.4.1)
uri
nokogiri (1.16.1-aarch64-linux)
racc (~> 1.4)
nokogiri (1.16.1-arm-linux)
racc (~> 1.4)
nokogiri (1.16.1-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.1-x86-linux)
racc (~> 1.4)
nokogiri (1.16.1-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.1-x86_64-linux)
racc (~> 1.4)
octokit (4.25.1)
faraday (>= 1, < 3)
sawyer (~> 0.9)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (4.0.7)
racc (1.7.3)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.6)
rouge (3.26.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
simpleidn (0.2.1)
unf (~> 0.1.4)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.9.1)
unicode-display_width (1.8.0)
uri (0.13.0)
webrick (1.8.1)
minitest (5.18.0)
rexml (3.2.5)
PLATFORMS
aarch64-linux
arm-linux
arm64-darwin
x86-linux
x86_64-darwin
aarch64-linux-android
x86_64-linux
DEPENDENCIES
csv
github-pages
json
webrick
kramdown
kramdown-parser-gfm
minitest
BUNDLED WITH
2.5.3
2.4.1

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Rico Sta. Cruz and contributors
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 NONINFRINGEMENT. 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.

View File

@ -1,19 +0,0 @@
npmbin := ./node_modules/.bin
PORT ?= 3000
HOST ?= 127.0.0.1
help:
@echo
@echo Makefile targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@echo
# Builds intermediate files. Needs a _site built first though
update: _site
# Builds _site
_site:
yarn build
dev:
yarn dev

View File

@ -4,16 +4,10 @@
TL;DR for developer documentation - a ridiculous collection of cheatsheets
</blockquote>
<p align='center'>
<a href='https://travis-ci.org/rstacruz/cheatsheets'><img src='https://travis-ci.org/rstacruz/cheatsheets.svg?branch=master' alt='See test builds'></a>
<a href='https://github.com/rstacruz/cheatsheets/actions?query=workflow%3ADeploy'><img src='https://github.com/rstacruz/cheatsheets/workflows/Deploy/badge.svg' alt='GitHub pages deploy status'></a>
<a href='https://app.netlify.com/sites/devhints-cheatsheets/deploys'><img src='https://api.netlify.com/api/v1/badges/c66b2a8b-5147-4243-9bf6-e2143126f6c8/deploy-status' alt='Netlify deploy status'></a>
</p>
<br>
<p align='center'>
<a href='https://devhints.io/'><img src='_docs/images/screenshot.png' width=600></a>
<a href='https://devhints.io/'><img src='.github/images/screenshot.png' width=600></a>
<br>
<b><a href='https://devhints.io/'>devhints.io</a></b>
</p>

View File

@ -1,67 +0,0 @@
# Jekyll configuration
whitelist:
- jekyll-redirect-from
- jekyll-github-metadata
plugins:
- jekyll-redirect-from
- jekyll-github-metadata
exclude:
- .babelrc
- .cache
- CNAME
- CONTRIBUTING.md
- cssnano.config.js
- docker_compose.yml
- Dockerfile
- Gemfile
- Gemfile.lock
- Makefile
- node_modules
- package.json
- package-lock.json
- postcss.config.js
- README.md
- vendor
- webpack.config.js
- yarn-error.log
- yarn.lock
# Markdown
highlighter: false
markdown: kramdown
kramdown:
input: GFM
hard_wrap: false
parse_block_html: true
syntax_highlighter_opts:
disable: true
# Defaults
defaults:
- scope:
path: ''
type: pages
values:
layout: 'default'
og_type: article
type: article
category: 'Others'
excerpt_separator: '<!--more-->'
prism_languages: []
# Site info
url: https://devhints.io
title: Devhints.io cheatsheets
# GitHub metadata
# https://help.github.com/articles/repository-metadata-on-github-pages/
repository: rstacruz/cheatsheets
include:
- _redirects

View File

@ -1,2 +0,0 @@
# No trailing slash
preview_host: https://assets.devhints.io/previews

View File

@ -1,2 +0,0 @@
enabled: true
src: https://cdn.carbonads.com/carbon.js?serve=CE7IK5QM&placement=devhintsio

View File

@ -1,30 +0,0 @@
enabled: true
names:
- Analytics
- Ansible
- Apps
- C-like
- CLI
- CSS
- Databases
- Devops
- Elixir
- Git
- HTML
- Java & JVM
- JavaScript
- JavaScript libraries
- Jekyll
- Ledger
- Markup
- macOS
- Node.js
- PHP
- Python
- Rails
- React
- Ruby
- Ruby libraries
- Vim
- Fitness
- Others

View File

@ -1,2 +0,0 @@
enabled: true
# token: "c2c8bc62-c275-4c7a-a304-74335c5a1cd0"

View File

@ -1,51 +0,0 @@
home:
title: "Rico's cheatsheets"
tagline: |
Hey! I'm <a href='https://ricostacruz.com'>@rstacruz</a> and this is a modest collection of cheatsheets I've written.
top_nav:
title: Devhints.io
edit: Edit
edit_on_github: Edit this page on GitHub
sheet:
suffix: cheatsheet
social_list:
default_description: 'Ridiculous collection of web development cheatsheets'
description: 'The ultimate cheatsheet for TITLE.'
facebook_share: Share on Facebook
twitter_share: Share on Twitter
related_posts_callout:
description: Over SIZE curated cheatsheets, by developers for developers.
link: Devhints home
related_posts_group:
top: Top cheatsheets
other: Other cheatsheets
category: Other CATEGORY cheatsheets
search_form:
default_placeholder: Search SIZE+ cheatsheets
home_placeholder: Search...
prefix: devhints.io
comments_area:
suffix: for this cheatsheet.
link: 'Write yours!'
not_found:
title: Not found
description: Sorry, we don't have a cheatsheet for this yet. Try searching!
home: Back to home
announcement:
# id: 2017-10-26-twitter
id: 2023-12-14
title: |
We're on Twitter ♥️
body: |
Follow [@devhints](https://twitter.com/devhints) on X/Twitter for daily "today I learned" snippets.
Also: I've started a new blog with some insights on web development. Have a look! [**ricostacruz.com/posts**](https://ricostacruz.com/posts?utm_source=devhints)

View File

@ -1,2 +0,0 @@
enabled: true
host: devhints.disqus.com

View File

@ -1,4 +0,0 @@
enabled: true
hostname: devhints.io
id: "G-N7TC6B227L"
# id: "UA-106902774-1"

View File

@ -1,10 +0,0 @@
{% if site.data.content.announcement %}
<div class='announcements-list'>
<div class='announcements-item item -hide' data-js-dismissable='{"id":"{{ site.data.content.announcement.id }}"}'>
<h3 class='title'>{{ site.data.content.announcement.title }}</h3>
<div class='body'>{{ site.data.content.announcement.body | markdownify }}</div>
<button data-js-dismiss class='close'></button>
</div>
</div>
{% endif %}

View File

@ -1,34 +0,0 @@
<script type='application/ld+json'>
{
"@context": "http://schema.org",
"@type": "NewsArticle",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://google.com/article"
},
"headline": {{ meta_title | jsonify }},
"image": [ {{ meta_image | jsonify }} ],
"description": {{ meta_description | jsonify }}
}
</script>
<script type='application/ld+json'>
{
"@context": "http://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [{
"@type": "ListItem",
"position": 1,
"item": {
"@id": "{{ site.url }}/#{{ page.category | downcase | replace: ' ', '-' }}",
"name": "{{ page.category }}"
}
},{
"@type": "ListItem",
"position": 2,
"item": {
"@id": {{ page_url | jsonify }},
"name": {{ page.title | jsonify }}
}
}]
}
</script>

View File

@ -1,22 +0,0 @@
{% assign identifier = include.page.url | remove: '.html' | remove_first: '/' %}
{% if site.data.disqus.enabled %}
<section class='comments-area' id='comments' data-js-no-preview>
<div class='container'>
<details class='comments-details'>
<summary>
<strong class='count'>
<span class='disqus-comment-count' data-disqus-identifier="{{ identifier }}" data-disqus-url='{{ site.url }}/{{ identifier }}'>0 Comments</span>
</strong>
<span class='suffix'>{{ site.data.content.comments_area.suffix }}</span>
<span class='fauxlink'>{{ site.data.content.comments_area.link }}</span>
</summary>
<div class='comments-section'>
<div class='comments'>
<div id='disqus_thread'></div>
</div>
</div>
</details>
</div>
<noscript data-js-disqus='{"host":"{{ site.data.disqus.host }}","url":"{{ site.url }}/{{ identifier }}","identifier":"{{ identifier }}"}'></noscript>
</section>
{% endif %}

View File

@ -1,3 +0,0 @@
<script>{% include 2017/critical/critical.js %}</script>
<script src='{{base}}/assets/packed/app.js?t={{ timestamp }}'></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 %}

View File

@ -1,24 +0,0 @@
{% include meta.html %}
{% include polyfills.html %}
<!-- critical css -->
{% if include.critical == 'home'
%}<style id='critical-css'>{% include 2017/critical/critical-home.css %}</style>{%
endif
%}{%
if include.critical == 'sheet'
%}<style id='critical-css'>{% include 2017/critical/critical-sheet.css %}</style>{%
endif %}
<!-- allow disabling critical CSS optimization by passing ?nocrit=1 -->
<script id='critical-css-disable'>if (~window.location.search.indexOf('nocrit')){;[].slice.call(document.querySelectorAll('#critical-css')).map(function(e){e.parentNode.removeChild(e)})}</script>
<!-- deferred css -->
<script id='deferred-css'>;(function(links){(requestAnimationFrame||mozRequestAnimationFrame||webkitRequestAnimationFrame||msRequestAnimationFrame||(function(fn){window.addEventListener('load',fn)}))(function(){var h=document.getElementsByTagName('head')[0],l,i;for (i=0;i<links.length;i++){l=document.createElement('link');l.rel='stylesheet';l.href=links[i];h.appendChild(l);}})})([
'https://fonts.googleapis.com/css?family=Cousine',
'{{base}}/assets/2017/style.css?t={{ timestamp }}'
])</script>
<noscript id='deferred-css-fallback'>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Cousine'>
<link rel='stylesheet' href='{{base}}/assets/2017/style.css?t={{ timestamp }}'>
</noscript>

View File

@ -1,7 +0,0 @@
<div class='HeadlinePub' role='complementary'>
<script async src='{{ site.data.carbon.src }}' id="_carbonads_js"></script>
<span class='placeholder -one'></span>
<span class='placeholder -two'></span>
<span class='placeholder -three'></span>
<span class='placeholder -four'></span>
</div>

View File

@ -1,41 +0,0 @@
{% comment %}
This partial assigns these variables:
meta_image: "https://assets.devhints.io/previews/react.jpg"
meta_description: "A comprehensive cheatsheet for React."
meta_title: "React cheatsheet"
depth: "1"
base: "./"
timestamp: "293048189123"
page_url: "https://devhints.io/react"
It emits some blank lines because Jekyll, lol.
{% endcomment %}
{% assign depth = page.url | split: '/' | size | minus: 1 %}
{% assign base = '' %}
{% if depth <= 1 %}{% assign base = '.' %}
{% elsif depth == 2 %}{% assign base = '..' %}
{% elsif depth == 3 %}{% assign base = '../..' %}
{% elsif depth == 4 %}{% assign base = '../../..' %}{% endif %}
{% assign timestamp = site.time | date: "%Y%m%d%H%M%S" %}
{% if site.data.assets.preview_host %}{% capture meta_image %}{%
if page.url == '/'
%}{{ site.data.assets.preview_host }}/index.jpg?t={{ timestamp }}{%
else
%}{{ site.data.assets.preview_host }}{{ include.page.url | remove: '.html' }}.jpg?t={{ timestamp }}{%
endif
%}{% endcapture %}{% endif %}
{% capture meta_title %}{% include values/title.html page=include.page %}{% endcapture %}
{% assign meta_title = meta_title | strip_newlines %}
{% capture meta_description %}{% include values/description.html page=include.page %}{% endcapture %}
{% assign meta_description = meta_description | strip_newlines %}
{% capture page_url %}{{ site.url }}{{ page.url | remove: '.html' }}{% endcapture %}

View File

@ -1,14 +0,0 @@
{% assign slug = include.page.url | remove: '.html' | remove_first: '/' %}
<a class='{{ include.class }} -item-{{ slug }}'
href="{{base}}{{ include.page.url | remove: '.html' }}"
data-js-searchable-item='{"slug":"{{ slug }}","category":"{{ include.page.category }}"}'>
<span class='info'>
<code class='slug'>{{ slug }}</code>
{% if include.page.layout == '2017/sheet' %}
<abbr class='attribute-peg -new-layout hint--bottom' data-hint='New layout!'><span></span></abbr>
{% endif %}
<span class='title'>{{ include.page.title }} {{ include.page.redirect_to }}</span>
</span>
</a>

View File

@ -1,12 +0,0 @@
<li class='{{ include.class }}'>
<a href='{{ base }}{{ include.page.url | remove: '.html' }}'>
<strong>{{ include.page.title }}</strong>
<span>
cheatsheet
{% if include.page.layout == '2017/sheet' %}
<abbr class='attribute-peg -new-layout hint--bottom' data-hint='New layout!'><span></span></abbr>
{% endif %}
</span>
</a>
</li>

View File

@ -1,62 +0,0 @@
{% assign category_pages = site.pages
| where: "category", include.page.category
| where_exp: "page", "page.url != include.page.url"
| where_exp: "page", "page.deprecated != true"
| where_exp: "page", "page.redirect_to == null"
| sort: "weight", "last"
%}
{% assign top_pages = site.pages
| where_exp: "page", "page.url != include.page.url"
| where_exp: "page", "page.deprecated != true"
| sort: "weight", "last"
%}
{% assign size = site.pages | size %}
<footer class='related-posts-area' id='related' data-js-no-preview>
<div class='container'>
<div class='related-posts-section'>
<div class='callout'>
<a class='related-posts-callout' href='{{ base }}'>
<div class='text'>
<i class='icon'></i>
<span class='description'>
{{ site.data.content.related_posts_callout.description | replace: "SIZE", size }}
</span>
<span class='push-button -dark'>
{{ site.data.content.related_posts_callout.link }}
</span>
</div>
</a>
</div>
<div class='group'>
<div class='related-posts-group'>
{% if include.page.category == 'Others' %}
<h3>{{ site.data.content.related_posts_group.other }}</h3>
{% else %}
<h3>{{ site.data.content.related_posts_group.category | replace: "CATEGORY", include.page.category }}</h3>
{% endif %}
<ul class='related-post-list'>
{% for page in category_pages limit: 6 %}
{% include 2017/related-posts-item.html page=page class='item related-post-item' %}
{% endfor %}
</ul>
</div>
</div>
<div class='group'>
<div class='related-posts-group'>
<h3>{{ site.data.content.related_posts_group.top }}</h3>
<ul class='related-post-list'>
{% for page in top_pages limit: 6 %}
{% include 2017/related-posts-item.html page=page class='item related-post-item' %}
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</footer>

View File

@ -1,12 +0,0 @@
<footer class='search-footer' data-js-no-preview>
<div class='container'>
<div class='search-footer-section'>
<div class='search'>
{% include 2017/search-form.html class="-small" %}
</div>
<div class='links'>
<a class='home-button' href='{{ base }}'><i></i></a>
</div>
</div>
</div>
</footer>

View File

@ -1,19 +0,0 @@
<form
{% if include.live %}data-js-search-form{% endif %}
class='search' action='{{ base }}' method='get'>
<label class='search-box {{ include.class }}'>
<span class='prefix'>{{ site.data.content.search_form.prefix }}</span>
<span class='sep'>/</span>
<input name='q'
type='text'
{% if include.live %}
{% assign placeholder = site.data.content.search_form.home_placeholder | replace: "%{size}", size %}
autofocus data-js-search-input
placeholder='{{ placeholder }}'
{% else %}
{% assign size = site.pages | size %}
{% assign placeholder = site.data.content.search_form.default_placeholder | replace: "SIZE", size %}
placeholder='{{ placeholder }}'
{% endif %}>
</label>
</form>

View File

@ -1,35 +0,0 @@
{% comment %}
Params:
- noshare
- noedit
- noback
{% endcomment %}
<nav class='top-nav' data-js-no-preview role='navigation'>
<div class='container'>
{% unless include.noback %}
<div class='left'>
<a class='home back-button' href='{{base}}'></a>
</div>
{% endunless %}
<a class='brand' href='{{base}}'>
{{ site.data.content.top_nav.title }}
</a>
{% unless include.noshare %}
<div class='actions'>
{% include social-list.html class="social page-actions" page=include.page %}
{% unless include.noedit %}
<ul class='page-actions'>
<li class='link github -button hint--bottom' data-hint='{{ site.data.content.top_nav.edit_on_github }}'>
<a href='{{ site.github.repository_url }}/blob/master/{{ page.path | remove: '.html' }}'>
<span class='text -visible'>{{ site.data.content.top_nav.edit }}</span>
</a>
</li>
</ul>
{% endunless %}
</div>
{% endunless %}
</div>
</nav>

View File

@ -1,20 +0,0 @@
<div class="about-the-site">
<div class="container">
<p class='blurb'>
<strong><a href="{{ base }}">{{ site.title }}</a></strong> is a collection of cheatsheets I've written over the years.
Suggestions and corrections? <a href='https://github.com/rstacruz/cheatsheets/issues/907'>Send them in</a>.
<i class='fleuron'></i>
I'm <a href="http://ricostacruz.com">Rico Sta. Cruz</a>.
Check out my <a href="http://ricostacruz.com/posts">Today I learned blog</a> for more.
</p>
{% if page.url != '/index.html' %}
<p class='back'>
<a class='big-button -back -slim' href='.#toc'></a>
</p>
{% endif %}
<p>
</p>
</div>
</div>

View File

@ -1,14 +0,0 @@
{% include about-the-site.html %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.5/highlight.min.js"></script>
{% comment %}<!-- https://github.com/highlightjs/cdn-release/tree/master/build/languages -->{% endcomment %}
{% for lang in page.hljs_languages %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.5/languages/{{lang}}.min.js"></script>
{% endfor %}
<script src="https://cdn.rawgit.com/rstacruz/unorphan/v1.0.1/index.js"></script>
<script>hljs.initHighlightingOnLoad()</script>
<script>unorphan('h1, h2, h3, p, li, .unorphan')</script>
</body>
</html>

View File

@ -1,12 +0,0 @@
<!doctype html>
<html lang='en' class='no-js {{ page.html_class }}'>
<head>
{% include meta.html %}
{% include polyfills.html %}
<style>html{opacity:0}</style>
<link rel="stylesheet" href="{{base}}/assets/2015/style.css?t={{ timestamp }}">
<link href="{{base}}/assets/style.css?t={{ timestamp }}" rel="stylesheet" />
<link href="{{base}}/assets/print.css?t={{ timestamp }}" rel="stylesheet" media="print" />
</head>
<body>
<div class='all'>

View File

@ -1,85 +0,0 @@
{% include 2017/meta-vars.html page=page %}
<meta charset='utf-8'>
<meta content='width=device-width, initial-scale=1.0' name='viewport'>
<link href='{{ base }}/assets/favicon.png' rel='shortcut icon'>
<meta content='{{ page.url | escape }}' name='app:pageurl'>
{% if meta_title %}
<title>{{ meta_title | escape }}</title>
<meta content='{{ meta_title | escape }}' property='og:title'>
<meta content='{{ meta_title | escape }}' property='twitter:title'>
<meta content='{{ page.og_type | default: "article" | escape }}' property='og:type'>
{% endif %}
{% if meta_image %}
<meta content='{{ meta_image | escape }}' property='og:image'>
<meta content='{{ meta_image | escape }}' property='twitter:image'>
<meta content='900' property='og:image:width'>
<meta content='471' property='og:image:height'>
{% endif %}
{% if meta_description %}
<meta content="{{ meta_description | escape }}" name="description">
<meta content="{{ meta_description | escape }}" property="og:description">
<meta content="{{ meta_description | escape }}" property="twitter:description">
{% endif %}
<link rel="canonical" href="{{ page_url | escape }}">
<meta name="og:url" content="{{ page_url | escape }}">
{% if page.url == '/' %}
<link rel="prefetch" href="{{ site.url | escape }}">
<link rel="prerender" href="{{ site.url | escape }}">
{% endif %}
{% if page.author %}
{% for author in site.authors | where: "name", page.author %}
<meta content='{{ author.name }}' name='author'>
{% if author.ogp %}
<meta content='{{ author.ogp }}' property='article:author'>
{% endif %}
{% endfor %}
{% endif %}
{% if site.title %}
<meta content='{{ site.title | escape }}' property='og:site_name'>
{% endif %}
{% if site.facebook.app_id %}
<meta content='{{ site.facebook.app_id | escape }}' property='fb:app_id'>
{% endif %}
{% if site.facebook.admin %}
<meta content='{{ site.facebook.admin | escape }}' property='fb:admins'>
{% endif %}
{% if page.date %}
<meta content='{{ page.date | date: "%Y-%m-%d" }}' property='article:published_date'>
{% endif %}
{% if page.category %}
<meta content='{{ page.category | escape }}' property='article:section'>
{% endif %}
{% if page.tags %}
{% for tag in page.tags %}
<meta content='{{ tag | escape }}' property='article:tag'>
{% endfor %}
{% endif %}
{% if site.data.google_analytics.enabled %}
<script async src='https://www.googletagmanager.com/gtag/js?id={{ site.data.google_analytics.id }}'></script>
<script>
{% comment %} if(~location.hostname.indexOf('{{site.data.google_analytics.hostname}}')){ {% endcomment %}
window.dataLayer=window.dataLayer||[];
function gtag(){dataLayer.push(arguments)};
gtag('js',new Date());
gtag('config','{{ site.data.google_analytics.id }}');
</script>
{% endif %}
{% if depth %}
<meta property='page:depth' content='{{depth}}'>
{% endif %}
<script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script>
<script>(function(H){H.className=H.className.replace(/\bNoJs\b/,'WithJs')})(document.documentElement)</script>

View File

@ -1,9 +0,0 @@
<script>(function(d,s){if(window.Promise&&[].includes&&Object.assign&&window.Map)return;var js,sc=d.getElementsByTagName(s)[0];js=d.createElement(s);js.src='https://cdn.polyfill.io/v2/polyfill.min.js';sc.parentNode.insertBefore(js, sc);}(document,'script'))</script>
<!--[if lt IE 9]>{%comment%}
{%endcomment%}<script src='https://cdnjs.cloudflare.com/ajax/libs/nwmatcher/1.2.5/nwmatcher.min.js'></script>{%comment%}
{%endcomment%}<script src='https://cdnjs.cloudflare.com/ajax/libs/json2/20140204/json2.js'></script>{%comment%}
{%endcomment%}<script src='https://cdn.rawgit.com/gisu/selectivizr/1.0.3/selectivizr.js'></script>{%comment%}
{%endcomment%}<script src='https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js'></script>{%comment%}
{%endcomment%}<script src='https://cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.js'></script>{%comment%}
{%endcomment%}<![endif]-->

View File

@ -1,6 +0,0 @@
<div class='site-header'>
<div class='container'>
This is <a href="."><em>{{ site.title }}</em></a> &mdash; a collection of cheatsheets I've written.
</div>
</div>

View File

@ -1,12 +0,0 @@
{% if include.page.type == 'home' %}
{% assign description = site.data.content.social_list.default_description %}
{% else %}
{% assign description = site.data.content.social_list.description | replace: "TITLE", include.page.title %}
{% endif %}
<ul class="social-list {{ include.class }}">
<li class="facebook link hint--bottom" data-hint="{{ site.data.content.social_list.facebook_share }}"><a href="https://www.facebook.com/sharer/sharer.php?u={{ site.url | uri_escape }}{{ include.page.url | uri_escape }}" target="share"><span class="text"></span></a></li>
<li class="twitter link hint--bottom" data-hint="{{ site.data.content.social_list.twitter_share }}"><a href="https://twitter.com/intent/tweet?text={{ description | uri_escape }}%20{{ site.url | uri_escape }}{{ include.page.url | uri_escape }}" target="share"><span class="text"></span></a></li>
{% comment %}
<li class="googleplus link hint--bottom" data-hint="Share on Google Plus"><a href="https://plus.google.com/share?url={{ site.url }}{{ include.page.url }}" target="share"><span class="text">+1</span></a></li>-->
{% endcomment %}
</ul>

View File

@ -1,17 +0,0 @@
{%
if page.description and page.intro
%}{{ page.description }} {{ page.intro | markdownify | strip_html }}{%
elsif page.description
%}{{ page.description }} · One-page guide to {{ page.title }}{%
elsif page.keywords and page.intro
%}{{ page.keywords | join: ' · ' }} · {{ page.intro | markdownify | strip_html }}{%
elsif page.keywords
%}{{ page.keywords | join: ' · ' }} · One-page guide to {{ page.title }}{%
elsif page.intro
%}One-page guide to {{ page.title }}: usage, examples, and more. {{ page.intro | markdownify | strip_html }}{%
elsif page.type == 'article'
%}The one-page guide to {{ page.title }}: usage, examples, links, snippets, and more.{%
else
%}The one-page guide to {{ page.title }}: usage, examples, links, snippets, and more.{%
endif
%}

View File

@ -1,11 +0,0 @@
{%
if page.full_title
%}{{ page.full_title }}{%
elsif page.type == 'article'
%}{{ page.title }} cheatsheet{%
elsif page.title
%}{{ page.title }}{%
else
%}{{ site.title }}{%
endif
%}

View File

@ -1,78 +0,0 @@
<!doctype html>
<html class='NoJs' lang='en'><head>
{% assign featured_pages = site.pages
| where_exp: "page", "page.tags contains 'Featured'"
%}
{% assign recent_pages = site.pages
| where_exp: "page", "page.updated"
| sort: "updated", "first"
%}
{% include 2017/head.html critical='home' %}
</head><body class='UseCompactHeader HighlightPubFirstLine'>
{% include 2017/top-nav.html page=page is_home=true noedit=true noback=true %}
<div class='body-area -slim'>
<div class='site-header' role='banner'>
<h1>
{{ site.data.content.home.title }}
</h1>
<p>
{{ site.data.content.home.tagline }}
</p>
{% include 2017/search-form.html live=true %}
{% if site.data.carbon.enabled %}
<div class='pubbox'>
{% include 2017/headline-pub.html %}
</div>
{% endif %}
</div>
<div class='pages-list' role='main'>
{% for page in featured_pages %}
{% include 2017/pages-list-item.html page=page class='item top-sheet' %}
{% endfor %}
<h2 class='category item' data-js-searchable-header>
<span>Recently updated</span>
</h2>
{% for page in recent_pages reversed %}
{% if forloop.index <= 18 %}
{% include 2017/pages-list-item.html page=page class='article item' %}
{% endif %}
{% endfor %}
{% for category in site.data.categories.names %}
<h2 class='category item' id='{{ category | downcase | replace: " ", "-" }}' data-js-searchable-header>
<span>{{ category }}</span>
</h2>
{% for page in site.pages %}
{% if page.category == category %}
{% if page.title %}
{% include 2017/pages-list-item.html page=page class='article item' %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
<div class='message item missing-message'>
<h3>See something missing?</h3>
<p>
<a class='push-button' href='{{ site.github.repository_url }}/issues/907'>Request cheatsheet</a>
</p>
</div>
</div>
</div>
{% include 2017/announcements-list.html %}
{% include 2017/foot.html %}
</body>
</html>

View File

@ -1,26 +0,0 @@
<!doctype html>
<html lang='en'><head>
{% include 2017/head.html %}
</head><body>
{% include 2017/top-nav.html page=page noshare=true %}
<div class='body-area -slim'>
<div class='site-header'>
<h1>{{ site.data.content.not_found.title }}</h1>
<p>{{ site.data.content.not_found.description }}</p>
{% include 2017/search-form.html %}
<p class='action'>
<a class='push-button' href='./'>{{ site.data.content.not_found.home }}</a>
</p>
</div>
</div>
{% include 2017/foot.html %}
</body>
</html>

View File

@ -1,54 +0,0 @@
<!doctype html>
<html class='NoJs' lang='en'><head>
{% include 2017/head.html critical='sheet' %}
{% include 2017/article-schema.html page=page %}
</head><body class='UseCompactHeader HighlightPubFirstLine'>
{% include 2017/top-nav.html page=page %}
<div class='body-area'>
<header class='main-heading -center' role='banner'>
<h1 class='h1'>{{ page.title }} <em>{{ site.data.content.sheet.suffix }}</em></h1>
<div class='pubbox' data-js-no-preview>
{% if site.data.carbon.enabled %}
{% include 2017/headline-pub.html %}
{% endif %}
</div>
</header>
{% if page.tags contains 'WIP' %}
<aside class='notice-box'>
This page is a work in progress. You can help by <a href='{{ site.github.repository_url }}/blob/master/{{ page.path | remove: '.html' }}'>suggesting edits</a>!
</aside>
{% endif %}
{% if page.deprecated_by %}
<aside class='notice-box'>
<strong>Deprecated:</strong> This guide covers an older version.
<a href='{{ base }}{{ page.deprecated_by }}'>A newer version is available here.</a>
</aside>
{% endif %}
{% if page.intro %}
<div class='intro-content MarkdownBody'>
{{ page.intro | markdownify }}
</div>
{% endif %}
<main class='post-content MarkdownBody' data-js-main-body data-js-anchors role='main'>
{{ content }}
</main>
</div>
<div class='pre-footer' data-js-no-preview><i class='icon'></i></div>
{% include 2017/comments-area.html page=page %}
{% include 2017/search-footer.html %}
{% include 2017/related-posts.html page=page %}
{% include 2017/foot.html %}
</body>
</html>

View File

@ -1 +0,0 @@
{{content}}

View File

@ -1,30 +0,0 @@
---
type: article
---
{% include head.html %}
{% include site-header.html %}
<div class='post-list -single -cheatsheet'>
<div class='post-item'>
{% include social-list.html page=page class="-collapse" %}
<div class='post-headline -cheatsheet'>
<p class='prelude'><span></span></p>
<h1><span>{{ page.title }}</span></h1>
</div>
{% if site.data.carbon.enabled %}
<div class='headline-pub'>
<script async src='{{ site.data.carbon.src }}' id="_carbonads_js"></script>
</div>
{% endif %}
<div class='post-content -cheatsheet'>
{{ content }}
</div>
{% include social-list.html page=page %}
</div>
</div>
{% include foot.html %}

View File

@ -1,28 +0,0 @@
---
type: article
---
{% include head.html %}
{% include site-header.html %}
{% include 2017/article-schema.html page=page %}
<div class='post-list -single -cheatsheet'>
<div class='post-item'>
<div class='post-headline -cheatsheet'>
<p class='prelude'><span></span></p>
<h1><span>{{ page.title }}</span></h1>
<div class='pubbox'>
{% include 2017/headline-pub.html %}
</div>
</div>
<div class='post-content -cheatsheet'>
{{ content }}
</div>
{% include social-list.html page=page %}
</div>
</div>
{% include foot.html %}

View File

@ -1,30 +0,0 @@
---
html_class: home
type: home
---
{% include head.html %}
{% include site-header.html %}
{% for category in site.data.categories.names %}
<div class='pages-header'>
<h2 id='{{ category | downcase | replace: " ", "-" }}'>{{ category }}</h2>
</div>
<div class='pages-list'>
{% for page in site.pages %}
{% if page.category == category %}
<a href="{{base}}{{ page.url }}">
<span class='title'>{{ page.title }}</span>
<span class='date'>{{ page.url | remove: '.html' | remove: '/' }}</span>
</a>
{% endif %}
{% endfor %}
</div>
{% endfor %}
{% if site.data.carbon.enabled %}
<div class='side-ad'>
<script async src='{{ site.data.carbon.src }}' id="_carbonads_js"></script>
</div>
{% endif %}
{% include foot.html %}

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
{% assign target = page.redirect.to | remove: '.html' | replace: 'cheatsheets/cheatsheets', 'cheatsheets' %}
<html lang="en-US">
<meta charset="utf-8">
<title>Redirecting…</title>
<link rel="canonical" href="{{ target }}">
<meta http-equiv="refresh" content="0; url={{ target }}">
<h1>Redirecting...</h1>
<a href="{{ target }}">Click here if you are not redirected.</a>
<script>location="{{ target }}"</script>
</html>

View File

@ -1,3 +0,0 @@
// Base
@import '../_sass/2017/base/base.scss';
@import '../_sass/2017/base/fade.scss';

View File

@ -1,30 +0,0 @@
// Prismjs
import 'prismjs'
import 'prismjs/plugins/line-highlight/prism-line-highlight.min.js'
import 'prismjs/components/prism-jsx.min.js'
import 'prismjs/components/prism-bash.min.js'
import 'prismjs/components/prism-scss.min.js'
import 'prismjs/components/prism-css.min.js'
import 'prismjs/components/prism-elixir.min.js'
import 'prismjs/components/prism-ruby.min.js'
// Initializers
import './initializers/prism'
import './initializers/onmount'
// Behaviors
import './behaviors/anchors'
import './behaviors/dismissable'
import './behaviors/dismiss'
import './behaviors/disqus'
import './behaviors/h3-section-list'
import './behaviors/main-body'
import './behaviors/no-preview'
import './behaviors/searchable-header'
import './behaviors/searchable-item'
import './behaviors/search-form'
import './behaviors/search-input'
// CSS
import 'prismjs/plugins/line-highlight/prism-line-highlight.css'
import 'hint.css/hint.min.css'

View File

@ -1,46 +0,0 @@
import onmount from 'onmount'
import prepend from 'dom101/prepend'
const DEFAULTS = {
// select elements to put anchor on
rule: 'h2[id]',
// class name for anchor
className: 'local-anchor anchor',
// text of anchor
text: '#',
// append before or after innerText?
shouldAppend: false
}
/*
* Behavior: Add local anchors
*/
onmount('[data-js-anchors]', function () {
const data = JSON.parse(this.getAttribute('data-js-anchors') || '{}')
const rules = Array.isArray(data)
? data.length
? data
: [DEFAULTS]
: [Object.assign({}, DEFAULTS, data)]
for (const { rule, className, text, shouldAppend } of rules) {
for (const el of this.querySelectorAll(rule)) {
if (!el.hasAttribute('id')) {
continue
}
const id = el.getAttribute('id')
const anchor = document.createElement('a')
anchor.setAttribute('href', `#${id}`)
anchor.setAttribute('class', className)
anchor.innerText = String(text || DEFAULTS.text)
if (shouldAppend) {
el.appendChild(anchor)
} else {
prepend(el, anchor)
}
}
}
})

View File

@ -1,22 +0,0 @@
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

@ -1,17 +0,0 @@
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')
}
})

View File

@ -1,32 +0,0 @@
import onmount from 'onmount'
import injectDisqus from '../helpers/inject_disqus'
/**
* Delay disqus by some time. It's at the bottom of the page, there's no need
* for it to load fast. This will give more time to load more critical assets.
*/
const DISQUS_DELAY = 100
/**
* Injects Disqus onto the page.
*/
onmount('[data-js-disqus]', function () {
const data = JSON.parse(this.getAttribute('data-js-disqus'))
const $parent = this.parentNode
$parent.setAttribute('hidden', true)
window.disqus_config = function () {
this.page.url = data.url
this.page.identifier = data.identifier
}
// Disqus takes a while to load, don't do it so eagerly.
window.addEventListener('load', () => {
setTimeout(() => {
injectDisqus(data.host)
$parent.removeAttribute('hidden')
}, DISQUS_DELAY)
})
})

View File

@ -1,32 +0,0 @@
/* eslint-disable no-new */
import Isotope from 'isotope-layout'
import onmount from 'onmount'
import on from 'dom101/on'
import qsa from 'dom101/query-selector-all'
/*
* Behavior: Isotope
*/
onmount('[data-js-h3-section-list]', function () {
const iso = new Isotope(this, {
itemSelector: '.h3-section',
transitionDuration: 0
})
const images = qsa('img', this)
images.forEach((image) => {
on(image, 'load', () => {
iso.layout()
})
})
// Insurance against weirdness on pages like devhints.io/vim, where the
// critical path CSS may look different from the final CSS (because of the
// tables).
on(window, 'load', () => {
iso.layout()
})
})

View File

@ -1,16 +0,0 @@
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

@ -1,17 +0,0 @@
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]:not([hidden])')
const href = link && link.getAttribute('href')
if (href) window.location = href
})
})

View File

@ -1,24 +0,0 @@
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

@ -1,23 +0,0 @@
import onmount from 'onmount'
import { nextUntil } from '../helpers/dom'
import matches from 'dom101/matches'
// Ensure that search-index is set first
import './searchable-item'
/**
* Propagate item search indices to headers
*/
onmount('[data-js-searchable-header]', function () {
const els = nextUntil(this, '[data-js-searchable-header]').filter((el) =>
matches(el, '[data-search-index]')
)
const keywords = els
.map((n) => n.getAttribute('data-search-index'))
.join(' ')
.split(' ')
this.setAttribute('data-search-index', keywords.join(' '))
})

View File

@ -1,13 +0,0 @@
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

@ -1,2 +0,0 @@
import 'sanitize.css'
import './critical-home.scss'

View File

@ -1,16 +0,0 @@
@import './_utils.scss';
@import './_base.scss';
// Components
@import '../_sass/2017/components/attribute-peg.scss';
@import '../_sass/2017/components/announcements-item.scss';
@import '../_sass/2017/components/announcements-list.scss';
@import '../_sass/2017/components/back-button.scss';
@import '../_sass/2017/components/body-area.scss';
@import '../_sass/2017/components/headline-pub.scss';
@import '../_sass/2017/components/page-actions.scss';
@import '../_sass/2017/components/pages-list.scss';
@import '../_sass/2017/components/search-box.scss';
@import '../_sass/2017/components/site-header.scss';
@import '../_sass/2017/components/top-nav.scss';
@import '../_sass/2017/components/top-sheet.scss';

View File

@ -1,2 +0,0 @@
import 'sanitize.css'
import './critical-sheet.scss'

View File

@ -1,21 +0,0 @@
@import './_utils.scss';
@import './_base.scss';
// Markdown
@import '../_sass/2017/markdown/a-em.scss';
@import '../_sass/2017/markdown/code.scss';
@import '../_sass/2017/markdown/headings.scss';
@import '../_sass/2017/markdown/local-anchor.scss';
@import '../_sass/2017/markdown/p.scss';
@import '../_sass/2017/markdown/table.scss';
@import '../_sass/2017/markdown/ul.scss';
// Components
@import '../_sass/2017/components/back-button.scss';
@import '../_sass/2017/components/body-area.scss';
@import '../_sass/2017/components/h3-section.scss';
@import '../_sass/2017/components/h3-section-list.scss';
@import '../_sass/2017/components/headline-pub.scss';
@import '../_sass/2017/components/main-heading.scss';
@import '../_sass/2017/components/page-actions.scss';
@import '../_sass/2017/components/top-nav.scss';

View File

@ -1,27 +0,0 @@
/*
* This is the "critical path" JavaScript that will be included INLINE on every
* page. Keep this as small as possible!
*/
import wrapify from './wrapify'
import addClass from 'dom101/add-class'
import on from 'dom101/on'
// Transform the main body markup to make it readable.
const body = document.querySelector('[data-js-main-body]')
if (body) {
wrapify(body)
addClass(body, '-wrapified')
}
// Be "done" when we're done, or after a certain timeout.
on(window, 'load', done)
setTimeout(done, 5000)
let isDone
function done() {
if (isDone) return
addClass(document.documentElement, 'LoadDone')
isDone = true
}

View File

@ -1,62 +0,0 @@
import matches from 'dom101/matches'
/*
* Just like jQuery.append
*/
export function appendMany(el, children) {
children.forEach((child) => {
el.appendChild(child)
})
}
/*
* Just like jQuery.nextUntil
*/
export function nextUntil(el, selector) {
const nextEl = el.nextSibling
return nextUntilTick(nextEl, selector, [])
}
function nextUntilTick(el, selector, acc) {
if (!el) return acc
const isMatch = matches(el, selector)
if (isMatch) return acc
return nextUntilTick(el.nextSibling, selector, [...acc, el])
}
/*
* Just like jQuery.before
*/
export function before(reference, newNode) {
reference.parentNode.insertBefore(newNode, reference)
}
/*
* Like jQuery.children('selector')
*/
export function findChildren(el, selector) {
return [].slice.call(el.children).filter((child) => matches(child, selector))
}
/**
* Creates a div
* @private
*
* @example
*
* createDiv({ class: 'foo' })
*/
export function createDiv(props) {
const d = document.createElement('div')
Object.keys(props).forEach((key) => {
d.setAttribute(key, props[key])
})
return d
}

View File

@ -1 +0,0 @@
/* blank */

View File

@ -1,72 +0,0 @@
/**
* 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
}

View File

@ -1,43 +0,0 @@
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('hidden')
el.style.removeProperty('display')
})
}
/**
* 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('hidden', true)
el.style.setProperty('display', "none")
})
qsa(selectors).forEach((el) => {
el.removeAttribute('hidden')
el.style.removeProperty('display')
})
}

View File

@ -1,12 +0,0 @@
import ready from 'dom101/ready'
import onmount from 'onmount'
/**
* Behavior: Wrapping
*/
ready(() => {
setTimeout(() => {
onmount()
})
})

View File

@ -1 +0,0 @@
window.Prism = require('prismjs')

View File

@ -1,274 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`h2 + pre 1`] = `
<div>
<div
class="h2-section"
>
<h2>
heading
</h2>
<div
class="body h3-section-list"
data-js-h3-section-list=""
>
<div
class="h3-section language-markdown"
>
<div
class="body language-markdown"
>
<pre
class="language-markdown"
>
(code)
</pre>
</div>
</div>
</div>
</div>
</div>
`;
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

@ -1,88 +0,0 @@
/* eslint-env jest */
import wrapify from '../index'
it(
'simple usage',
run(
`
<div>
<h2>simple usage<h2>
<h3>install</h3>
<p>(install)</p>
<h3>usage</h3>
<p>(usage)</p>
</div>
`,
(root) => {
expect(
root.querySelectorAll('.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>
`,
(root) => {
expect(root.querySelectorAll('div.h3-section.-hello').length).toEqual(1)
expect(
root.querySelectorAll('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 = document.createElement('div')
div.innerHTML = input
const root = div.children[0]
wrapify(root)
expect(root).toMatchSnapshot()
if (fn) fn(root)
}
}
it(
'h2 + pre',
run(`
<div>
<h2>heading</h2>
<pre class='language-markdown'>(code)</pre>
</div>
`)
)

View File

@ -1,119 +0,0 @@
import matches from 'dom101/matches'
import addClass from 'dom101/add-class'
import {
appendMany,
nextUntil,
before,
findChildren,
createDiv
} from '../helpers/dom'
/**
* Wraps h2 sections into h2-section.
* Wraps h3 sections into h3-section.
*
* @private
*/
export default function wrapify(root) {
// These are your H2 sections. Returns a list of .h2-section nodes.
const sections = wrapifyH2(root)
// For each h2 section, wrap the H3's in them
sections.forEach((section) => {
const bodies = findChildren(section, '[data-js-h3-section-list]')
bodies.forEach((body) => {
wrapifyH3(body)
})
})
}
/**
* Wraps h2 sections into h2-section.
* Creates and HTML structure like so:
*
* .h2-section
* h2.
* (title)
* .body.h3-section-list.
* (body goes here)
*
* @private
*/
function wrapifyH2(root) {
return groupify(root, {
tag: 'h2',
wrapperFn: () => createDiv({ class: 'h2-section' }),
bodyFn: () =>
createDiv({
class: 'body h3-section-list',
'data-js-h3-section-list': ''
})
})
}
/**
* Wraps h3 sections into h3-section.
* Creates and HTML structure like so:
*
* .h3-section
* h3.
* (title)
* .body.
* (body goes here)
*
* @private
*/
function wrapifyH3(root) {
return groupify(root, {
tag: 'h3',
wrapperFn: () => createDiv({ class: 'h3-section' }),
bodyFn: () => createDiv({ class: 'body' })
})
}
/**
* Groups all headings (a `tag` selector) under wrappers like `.h2-section`
* (build by `wrapperFn()`).
* @private
*/
export function groupify(el, { tag, wrapperFn, bodyFn }) {
const first = el.children[0]
let result = []
// Handle the markup before the first h2
if (first && !matches(first, tag)) {
const sibs = nextUntil(first, tag)
result.push(wrap(first, null, [first, ...sibs]))
}
// Find all h3's inside it
const children = findChildren(el, tag)
children.forEach((child) => {
const sibs = nextUntil(child, tag)
result.push(wrap(child, child, sibs))
})
return result
function wrap(pivot, first, sibs) {
const wrap = wrapperFn()
const pivotClass = pivot.className
if (pivotClass) addClass(wrap, pivotClass)
before(pivot, wrap)
const body = bodyFn()
if (pivotClass) addClass(body, pivotClass)
appendMany(body, sibs)
if (first) wrap.appendChild(first)
wrap.appendChild(body)
return wrap
}
}

0
_sass/.gitignore vendored
View File

View File

@ -1,427 +0,0 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

View File

@ -1,58 +0,0 @@
@import url('//brick.a.ssl.fastly.net/Roboto:400,400i,700')
@import url('//fonts.googleapis.com/css?family=Raleway:800')
@import url('//fonts.googleapis.com/css?family=Fira+Mono:400,400i')
@import url('//brick.a.ssl.fastly.net/EB+Garamond:400i')
$body-font-size: 17px
$body-line-height: 1.7
@mixin font-size($multiplier, $lhmultiplier, $size, $line-height)
font-size: $size * $multiplier
@if $line-height != none
line-height: $line-height * $lhmultiplier
@mixin body-font
font-family: 'Roboto', sans-serif
font-weight: 400
@mixin italic-font
font-family: 'eb garamond', serif
font-weight: 400
font-style: italic
+kernliga
@mixin headline-font
font-family: 'eb garamond', serif
font-weight: 400
font-style: italic
+kernliga
@mixin caps-font
text-transform: uppercase
letter-spacing: 2px
@mixin mono-font
font-family: 'fira mono', monospace
font-weight: 400
letter-spacing: -0.5px
+no-antialias
@mixin bold-font
font-family: 'raleway', sans-serif
font-weight: 800
/*
* sizes
*/
@mixin italic-font-size($size, $line-height: none)
+font-size(1.0, 1.0, $size, $line-height)
@mixin headline-font-size($size, $line-height: none)
+font-size(1.0, 1.0, $size, $line-height)
@mixin bold-font-size($size, $line-height: none)
+font-size(1.0, 1.0, $size, $line-height)
@mixin mono-font-size($size, $line-height: none)
+font-size(1.0, 1.0, $size, $line-height)

View File

@ -1,41 +0,0 @@
@mixin kernliga
font-size-adjust: none
// don't display digraphs in languages that don't support it
-webkit-font-language-override: normal
font-language-override: normal
// use font-defined kerning info
-webkit-font-kerning: auto
font-kerning: auto
// opentype options: kerning, ligatures, horiz ligatures, discretionary ligatures, contextual swash
// https://en.wikipedia.org/wiki/List_of_typographic_features
-webkit-font-feature-settings: "kern", "liga", "dlig", "hlig", "cswh"
font-feature-settings: "kern", "liga", "dlig", "hlig", "cswh"
// allow browser to auto-infer missing glyphs
font-synthesis: weight style
// swashes on first letters
// &:first-letter
// font-feature-settings: "kern", "swsh"
// -webkit-font-feature-settings: "kern", "swsh"
@mixin antialias
text-rendering: optimizeLegibility
-webkit-font-smoothing: antialiased
-moz-osx-font-smoothing: grayscale
@mixin no-antialias
text-rendering: auto
-webkit-font-smoothing: subpixel-antialiased
-moz-osx-font-smoothing: auto
@mixin clearfix
&:after
display: table
content: ''
clear: both
height: 0
zoom: 1

View File

@ -1,105 +0,0 @@
/*
* .about-the-site
*/
.about-the-site
&
position: relative
.container
text-align: center
padding: 3em 80px
margin: 0 auto
+clearfix
@media (max-width: 768px)
padding-left: 40px
padding-right: 40px
&:before
content: ''
position: absolute
display: block
left: 20px
right: 20px
top: 0
border-top: solid 1px $hairline
&
+body-font
font-size: 0.85em
color: lighten($gray, 20%)
a, a:visited
color: lighten($gray, 10%)
box-shadow: inset 0 -1px rgba(black, 0.05)
padding-bottom: 2px
a:hover, a:focus
color: $black
box-shadow: inset 0 -2px $accent
strong
+bold-font
strong a, strong a:visited
color: $black
box-shadow: none
strong a:hover, strong a:focus
color: $black
box-shadow: inset 0 -2px $accent
.identity
margin: 0
margin-top: 0.2em
float: right
+italic-font
+italic-font-size(2.5em)
.identity a,
.identity a:visited
color: $black
box-shadow: none
.identity a:hover,
.identity a:focus
color: $accent
.blurb
margin: 0
max-width: 500px
text-align: left
float: left
line-height: 1.55
.back
float: right
margin-top: 0.4em
margin-right: 2em
.fleuron:before
content: '\f492'
font-family: Ionicons
font-size: 16px
font-weight: normal
font-style: normal
display: inline-block
vertical-align: middle
color: $black
margin: 0 7px
@media (max-width: 768px)
display: none
@media (max-width: 768px)
.identity
float: left
clear: both
.blurb
float: none
margin-bottom: 1em
width: auto
.back
float: right
margin-right: 0

View File

@ -1,66 +0,0 @@
.big-button,
a.big-button
display: inline-block
width: 180px
height: 50px
line-height: 50px - 2px
padding: 0
border-radius: 30px
font-size: 0.85em
box-shadow: none
background: transparent
@media (max-width: 768px)
width: 140px
height: 40px
line-height: 40px - 2px
&, &:visited
border: solid 2px $accent
color: $accent
&.-back,
&.-back:visited
border: solid 1px rgba($gray, 0.2)
color: $gray
&.-back:before,
&.-next:after
font-family: Ionicons
font-size: 20px
display: inline-block
vertical-align: middle
position: relative
top: -1px
&.-back:before
content: '\f38f'
&.-next:after
content: '\f3d1'
margin-left: 16px
top: 0
&.-slim
width: 60px
border-width: 2px
height: 60px
line-height: 60px
border-radius: 50%
@media (max-width: 768px)
width: 40px
height: 40px
line-height: 40px
&:hover, &:focus
background: $accent
border-color: transparent
color: white
box-shadow: none
&.-back:hover,
&.-back:focus
background: $accent
color: white

View File

@ -1,8 +0,0 @@
/*
* .brief-intro -- Brief introduction
*/
.brief-intro
font-size: 1.1em
color: $gray

View File

@ -1,38 +0,0 @@
/*
* .full-image -- full width image containers
*/
.full-image
&
overflow: hidden
text-align: center
position: relative
img
background: #fcfcfc
&.cropped img,
&.cropped img:first-child:last-child
margin-bottom: -50px
display: block
background: transparent
&.cropped:after
content: ''
display: block
position: absolute
bottom: 0
left: 20px
right: 20px
border-bottom: solid 1px $lightgray
&.stretched img
width: 100%
@media (max-width: 768px)
margin-left: -20px
margin-right: -20px
@media (min-width: 769px)
width: 100vw
margin-left: calc(-50vw + #{$page-width} / 2)

View File

@ -1,14 +0,0 @@
.hint--top, .hint--bottom
&:before
margin-top: -14px
margin-left: -8px
border-radius: 2px
&:before, &:after
transition-duration: 10ms
&:after
box-shadow: none
border-radius: 2px
text-shadow: none
margin-left: -55px

View File

@ -1,78 +0,0 @@
.hljs-literal,
.hljs-number,
.hljs-string,
.hljs-symbol,
.hljs-value
color: $accent
.hljs-key,
.hljs-attribute
color: darken($accent, 20%)
.hljs-keyword,
.hljs-constant
color: $black
.hljs-comment
color: $gray
font-style: italic
//
// Prism
//
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata
color: $gray
font-style: italic
.token.punctuation
color: #999
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted
color: #905
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted
color: $accent
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string
color: #a67f59
.token.atrule,
.token.attr-value,
.token.keyword
color: #07a
.token.function
color: #DD4A68
.token.regex,
.token.important,
.token.variable
color: #e90
.token.important,
.token.bold
font-weight: bold
.token.italic
font-style: italic
.token.entity
cursor: help

View File

@ -1,142 +0,0 @@
/*
* .next-article -- lead into the next article
*/
.next-article
$bg: darken(#8e44ad, 15%)
$textcolor: saturate(mix(white, $bg, 85%), 90%)
&
display: block
padding: 0
margin-left: 40px
margin-right: 40px
position: relative
&, &:hover, &:focus
box-shadow: none
&:after
content: ''
display: block
position: absolute
left: -20px
right: -20px
bottom: 0
border-bottom: solid 1px $hairline
// suppress its hairline
& + .about-the-site:before
display: none
@media (max-width: 768px)
margin-left: 0
margin-right: 0
// remove horizontal line
& + .about-the-site:before
display: none
.container
display: block
text-align: center
padding: 10em 20px
@media (min-width: 769px)
margin-top: 8em
padding: 8em 20px
.h3
display: block
margin: 0 auto auto
padding: 0
font-size: 2.2em
line-height: 1.3em
+bold-font
color: white
transition: all 250ms linear
// &:hover > span
// box-shadow: inset 0 -2px $accent
// text-shadow: 0 0 4px $bg, 0 0 4px $bg, 0 0 4px $bg, 0 0 4px $bg, 0 0 4px $bg, 0 0 4px $bg, 0 0 4px $bg, 0 0 4px $bg, 0 0 4px $bg
@media (max-width: 768px)
font-size: 1.5em
.h3, .excerpt
max-width: $page-width * 0.9
.h3 + .excerpt
margin-top: 0.5em
.excerpt
display: block
margin-left: auto
margin-right: auto
font-size: 1em
line-height: 1.6em
.big-button
margin-top: 2em
h3 a:hover,
h3 a:focus
color: $accent
.heading
display: block
+caps-font
font-size: 0.7em
margin-bottom: 1em
.heading:before
font-family: Ionicons
content: '\f4a8'
margin-right: 15px
font-size: 16px
display: inline-block
vertical-align: middle
color: $accent
.big-button,
a.big-button
background: transparent
&, &:visited
border-color: $accent
color: white
&:hover, &:focus
background: $accent
border-color: transparent
@mixin recolor-article($bg, $bg2, $url: '', $a: 0.9, $angle: 177deg)
$w: 1500
$h: 10
$textcolor: mix(white, $bg, 75%)
$notch: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='#{$w}' height='#{$h}' version='1.1'><polyline fill='white' points='#{$w},0 0,0 0,#{$h}'/></svg>"
&
background: url($notch) -50px top / 110% auto no-repeat, linear-gradient(to right, rgba(adjust-color($bg, $lightness: 0%), $a), rgba(adjust-color($bg2, $lightness: 0%), $a)), linear-gradient($angle, rgba($bg, 0.0), rgba($bg, 0.9)), url($url) center center / cover, $bg
&:hover, &:focus
background: url($notch) -50px top / 110% auto no-repeat, linear-gradient(to right, rgba(adjust-color($bg, $lightness: 1%), $a), rgba(adjust-color($bg2, $lightness: 1%), $a)), linear-gradient($angle, rgba($bg, 0.0), rgba($bg, 0.9)), url($url) center center / cover, $bg
.excerpt, .heading
color: $textcolor
.next-article
&
+recolor-article(#612e76, #5867cc)
&.-v2
text-shadow: 0 1px 1px rgba(black, 0.5)
+recolor-article(#027d65, #00536b, "bg/pebbles.jpg", 0.7, $angle: 35deg)
&.-v3
text-shadow: 0 1px 1px rgba(black, 0.5)
+recolor-article(#1d2434, #202a3e, "bg/roughwall.jpg", 0.9, $angle: 1deg)
&.-v4
text-shadow: 0 1px 1px rgba(black, 0.5)
+recolor-article(#902014, #c77e0a, "bg/woodfloor.jpg", 0.45, $angle: 1deg)
&.-v5
text-shadow: 0 1px 1px rgba(black, 0.5)
+recolor-article(#17283a, #25295e, "bg/stairs.jpg", 0.85, $angle: 1deg)
// &.next-article

View File

@ -1,64 +0,0 @@
/*
* .post-headline -- H1's of posts
*/
.post-headline
&
margin: 1.5em auto 3em auto
text-align: center
.post-icon
margin-bottom: 2px
h1
text-align: center
margin-bottom: 0
+headline-font
+headline-font-size(2.8em, 1.2)
width: 80%
margin-left: auto
margin-right: auto
@media (max-width: 768px)
+headline-font-size(2em)
.meta
display: block
text-align: center
margin: 0
margin-top: 1em
font-weight: normal
+caps-font
font-size: 0.8em
.meta .author,
.meta .date
margin: 0 5px
padding-bottom: 2px
.meta a,
.meta a:visited
color: $gray
.meta a:hover,
.meta a:focus
&, span
color: $gray
time
color: $black
box-shadow: inset 0 -2px $accent
a, a:visited, a:focus, a:hover
color: $black
text-decoration: none
box-shadow: none
.pubbox
margin-top: 32px
font-size: 16px
line-height: 1.5
.carbon-img
margin-top: 4px

View File

@ -1,58 +0,0 @@
/*
* .post-icon -- category icons
*/
$icon-size: 56px
@mixin iconify($color, $text)
&
background: $color
color: lighten($color, 50%)
box-shadow: -2px 2px #e67e22, 2px -2px #f1c40f, 1px -2px rgba($color, 0.2), 1px 3px rgba($color, 0.3)
span:after
content: $text
.post-icon,
abbr.post-icon
border: 0
margin: 0
display: inline-block
width: $icon-size
height: $icon-size
line-height: $icon-size + 2px
text-align: center
border-radius: 50%
color: #aaa
background: #eee
+body-font
font-size: 16px
letter-spacing: 1px
span:after
content: attr(data-label)
font-size: 0.9em
position: relative
top: -1px
@media (max-width: 480px)
transform: scale(0.75)
&.-icon-css
+iconify(#3498db, 'CSS')
font-size: 14px
line-height: $icon-size + 2px
&.-icon-development
+iconify(#34495e, 'DEV')
font-size: 14px
line-height: $icon-size + 2px
&.-icon-ruby
+iconify(#e74c3c, 'RB')
text-indent: 2px
&.-icon-javascript
+iconify(#2ecc71, 'JS')
text-indent: 1px
&.-icon-productivity
+iconify(#2ecc71, 'PROD')
text-indent: 1px

View File

@ -1,73 +0,0 @@
/*
* .post-index (et al) -- utility classes
*/
.post-index
&
margin: 0 auto 4em auto
position: relative
padding-top: 4em
font-size: 0.9em
.container
overflow: hidden
max-width: $page-width
margin: 0 auto
h3
+caps-font
color: $gray
font-size: 1em
.post-index-item
display: block
overflow: hidden
padding: 6px 20px
border-top: solid 1px $hairline
&, &:hover, &:focus
box-shadow: none
&:hover, &:focus
.article
transition: none
.date
display: block
.date
width: 100px
font-size: 0.8em
color: $gray
@media (min-width: 768px)
padding-left: 0
padding-right: 0
.date, .tag
margin-top: 0.2em
.date, article
float: left
.tag
float: right
.article
color: $text
margin-right: 3px
transition: all 100ms linear
&:hover .article
color: $accent
&:nth-of-type(1) .article,
&:nth-of-type(2) .article,
&:nth-of-type(3) .article,
&:nth-of-type(4) .article
+bold-font
.tag
color: $gray
font-weight: normal
font-size: 0.8em

View File

@ -1,23 +0,0 @@
.post-item
max-width: $page-width
margin: 0 auto
.post-list
margin: 40px auto
padding: 20px
@media (max-width: 768px)
margin: 0 auto
.post-list > .post-item:not(:first-child)
&:before
content: ''
display: block
width: 150px
height: 1px
background: $hairline
margin: 7em auto
@media (max-width: 768px)
margin: 4em auto

View File

@ -1,17 +0,0 @@
.site-header
text-align: center
padding: 0 20px
.container
padding: 15px 20px
font-size: 0.9em
color: rgba($gray, 0.6)
border-bottom: solid 1px $hairline
em
+italic-font
+italic-font-size(1.1em)
font-size: 1.1em
color: $text

View File

@ -1,73 +0,0 @@
/*
* .social-list -- social share icons
*/
.social-list
&, li
margin: 0
padding: 0
&
display: block
text-align: center
width: 100%
margin-top: 2em
@media (min-width: 768px)
margin-top: 4em
&.-top
margin-top: -2em
@media (min-width: 768px)
margin-top: -1em
li
display: inline-block
a
display: inline-block
padding: 6px
text-align: center
box-shadow: none
a:before, a:after
transition: all 100ms linear
.text
display: none
a:before
font-family: 'Ionicons'
font-weight: normal
font-style: normal
font-size: 18px
width: 40px
height: 40px
line-height: 40px
display: inline-block
text-align: center
border: solid 2px transparent
border-radius: 50%
@mixin socialiconify($color, $content, $filled)
&
color: darken($lightgray, 10%)
&:hover,
&:focus
color: $color
&:before
content: $filled
&:hover:before,
&:focus:before
border-color: $color
content: $filled
.facebook a
+socialiconify(#4c66a4, '\f230', '\f231')
.twitter a
+socialiconify(dodgerblue, '\f242', '\f243')
.googleplus a
+socialiconify(#f53, '\f234', '\f235')

View File

@ -1,157 +0,0 @@
/*
* html base (html, body)
*/
*
+antialias
html
font-size: $body-font-size
line-height: $body-line-height
background: $bg
color: $text
@media (max-width: 768px)
font-size: floor($body-font-size * 14/17)
html
transition: opacity 200ms linear
opacity: 1
html, input, textarea, td, th
+body-font
html, body
overflow-x: hidden
/*
* fouc prevention
*/
body
transition: opacity 100ms linear
html.no-js *
opacity: 0
/*
* basic styles (a, p, img...)
*/
a, a:visited
color: $text
text-decoration: none
box-shadow: inset 0 -1px rgba(#888, 0.3)
transition: all 100ms linear
a:focus, a:hover
box-shadow: inset 0 -2px $accent
color: $black
strong, b
&, a, a:visited
color: $black
h3, p, ul, ol
margin: 1.5em 0
/*
* iframes
*/
p > iframe
margin: 0 auto
display: block
/*
* lists
*/
ul, ol
padding-left: 1.5em
ul
&
list-style: none
> li
position: relative
> li:before
content: ''
display: block
position: absolute
left: -1.4em
top: 0
margin-top: 0.7em
width: 4px
height: 4px
border-radius: 50%
border: solid 2px $lightgray
@media (max-width: 768px)
width: 3px
height: 3px
/*
* headings
*/
h2
&, a, a:visited
color: $black
h2
text-align: center
+headline-font
+headline-font-size(2em, 1.4)
margin: 2em auto 0 auto
@media (max-width: 768px)
+headline-font-size(1.6em)
@media (min-width: 769px)
h2:before,
h2:after
content: ''
display: inline-block
vertical-align: middle
width: 46px
height: 1px
background: $lightgray
margin: 0 30px
h3
+bold-font
+bold-font-size(1.1em)
margin-top: 2em
&, a, a:visited
color: $black
@media (max-width: 768px)
margin-top: 1.5em
h3 + p
margin-top: -1.7em
/*
* images
*/
p > img:first-child:last-child
display: block
margin: 0 auto
max-width: 100%
code
+mono-font
+mono-font-size(0.9em)
hr
width: 200px
height: 1px
border: 0
background: $lightgray
margin: 3em auto
display: block

View File

@ -1,119 +0,0 @@
$term-border: mix($accent, white, 50%)
/*
* pre > code -- code blocks
*/
pre > code
+mono-font
+mono-font-size(0.82em)
color: $text
padding-right: 20px
// box-shadow: inset 1px 1px rgba(black, 0.06)
pre
padding: 20px 50px
border-radius: 4px
background: $wash
margin: 2.2em -50px
line-height: 1.2em
@media (min-width: 768px)
border-top: solid 1px #eef3fa
border-bottom: solid 1px #c7d7ee
border-radius: 4px
@media (max-width: 768px)
padding: 20px
margin: 2em -20px
background: darken($wash, 3%)
@media (max-width: 660px)
border-radius: 0
pre + pre
margin-top: -1.5em
pre.medium
> code
font-size: 1em
@media (min-width: 768px)
padding-top: 30px
padding-bottom: 30px
pre.large
> code
font-size: 1.1em
line-height: 1.4em
@media (min-width: 768px)
padding-top: 30px
padding-bottom: 30px
pre.terminal,
pre.light
&
background: white
border: solid 1px $term-border
position: relative
pre.light
background: #fdfdff
pre.terminal
&
padding-top: 56px
&.large
padding-top: 65px
&:before
content: ''
display: block
height: 34px
line-height: 34px
background: white
border-bottom: solid 1px $term-border
border-top-left-radius: 3px
border-top-right-radius: 3px
position: absolute
top: 0
left: 0
right: 0
+bold-font
&:after
content: ''
position: absolute
display: block
width: 4px
height: 4px
border-radius: 50%
left: 15px
top: 15px
font-size: 20px
background-color: $term-border
box-shadow: 20px 0 $term-border, 40px 0 $term-border
@media (max-width: 768px)
margin-left: 0
margin-right: 0
pre + pre
margin-top: -1.7em
@media (min-width: 769px)
pre.cursor > code > :last-child:after
content: ''
display: inline-block
width: 3px
height: 1em
transform: scaleY(1.5) translateY(0.1em)
margin-left: 0.4em
background-color: $accent
-webkit-animation: blink 700ms steps(2,end) infinite
-moz-animation: blink 700ms steps(2,end) infinite
animation: blink 700ms steps(2,end) infinite
h3 + pre,
h3 + table
margin-top: -1em

View File

@ -1,73 +0,0 @@
/*
* table
*/
table
min-width: 100%
margin-top: 2em
margin-bottom: 2em
font-size: 0.9em
border-bottom: solid 1px $gray
thead > tr:first-child > th,
thead > tr:first-child > td,
tbody > tr:first-child > th,
tbody > tr:first-child > td
border-top: solid 1px $gray
td, th
text-align: left
border-top: solid 1px $hairline
padding: 8px 10px
th
+bold-font
color: $black
td:first-child,
th:first-child
text-align: left
padding-left: 0
td:last-child,
th:last-child
padding-right: 0
table.no-head
thead
display: none
table.shortcuts
&
table-layout: fixed
thead
display: none
th:nth-child(1), td:nth-child(1)
width: 25%
th:nth-child(2), td:nth-child(2)
width: 75%
td:first-child > code
background: #fcfcfc
padding: 5px 10px
border-radius: 2px
table.lite-headings
&
border-bottom: solid 1px mix($gray, $lightgray, 50%)
th
color: mix($gray, $lightgray, 50%)
font-size: 0.9em
thead > tr:first-child > td,
thead > tr:first-child > th
border-top: solid 1px mix($gray, $lightgray, 50%)
tbody > tr:first-child > td,
tbody > tr:first-child > th
border-top: solid 1px $hairline

View File

@ -1,11 +0,0 @@
@keyframes blink
0%
opacity: 0
100%
opacity: 1
@-webkit-keyframes blink
0%
opacity: 0
100%
opacity: 1

View File

@ -1,27 +0,0 @@
/*
* .center (et al) -- utility classes
*/
.center
text-align: center
.spaced
@media (min-width: 769px)
margin-top: 4em
margin-bottom: 4em
.spaced-more
@media (min-width: 769px)
margin-top: 6em
margin-bottom: 6em
.spaced-less
@media (min-width: 769px)
margin-top: 2em
margin-bottom: 2em
.wide
@media (min-width: 920px)
width: 140%
margin-left: -25%

View File

@ -1,43 +0,0 @@
/*
* .top/.bottom -- margin helpers
*/
.top-collapse-0
margin-top: 0
.top-collapse-1
margin-top: -1em
.top-collapse-2
margin-top: -2em
.top-collapse-4
margin-top: -4em
.top-space-0
margin-top: 0
.top-space-1
margin-top: 1em
.top-space-2
margin-top: 2em
.top-space-4
margin-top: 2em
@media (min-width: 769px)
margin-top: 4em
.bottom-collapse-0
margin-bottom: 0
.bottom-collapse-1
margin-bottom: -1em
.bottom-collapse-2
margin-bottom: -2em
.bottom-collapse-4
margin-bottom: -4em
.bottom-space-0
margin-bottom: 0
.bottom-space-1
margin-bottom: 1em
.bottom-space-2
margin-bottom: 2em
.bottom-space-4
margin-bottom: 2em
@media (min-width: 769px)
margin-bottom: 4em

View File

@ -1,57 +0,0 @@
// base colors
$text: #555
$black: #151515
$bg: #fff
$wash: #f4f6f8
// grays
$gray: #939aa1
$lightgray: #c0d3da
$hairline: #eef3fa
// accents
$accent: #1ea3ff
$accent2: #f53
// metrics
$page-width: 620px
@import url('//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css')
@import url('//cdn.jsdelivr.net/hint.css/1.3.2/hint.min.css')
@import 'base/utils'
@import 'base/typography'
@import 'base/normalize'
@import 'elements/body'
@import 'elements/table'
@import 'elements/code'
@import 'components/about-the-site'
@import 'components/big-button'
@import 'components/brief-intro'
@import 'components/full-image'
@import 'components/hint'
@import 'components/hljs'
@import 'components/next-article'
@import 'components/post-headline'
@import 'components/post-icon'
@import 'components/post-index'
@import 'components/post-list'
@import 'components/site-header'
@import 'components/social-list'
@import 'helpers/general'
@import 'helpers/margins'
@import 'helpers/blink'
// Shim for headline-pub
$base-mute: #678
$base-b: #333
$base-b3: #678
$base-text: #333
$shadow3: 0 6px 8px rgba($base-mute, 0.03), 0 1px 2px rgba($base-mute, 0.30)
$shadow6: 0 6px 8px rgba($base-mute, 0.03), 0 1px 2px rgba($base-mute, 0.30), 0 8px 12px rgba($base-b3, 0.1)
$gray-text: $base-mute
@import '../2017/components/headline-pub.scss'

View File

@ -1,30 +0,0 @@
$bounce: cubic-bezier(0.75, -0.5, 0, 1.75);
/*
/* "Preloader":
* This makes the content semi-transparent before the page ad loads.
*/
.post-content {
html.WithJs & {
opacity: 0;
}
}
/*
* Defer "loading" until page's onload event fires.
* (The page actually already loaded, we just pretend like it hasn't)
*/
.pages-list,
.post-content.-wrapified,
.intro-content {
html.WithJs & {
opacity: 0.98;
}
html.WithJs.LoadDone & {
opacity: 1;
transition: opacity 100ms linear 100ms;
}
}

View File

@ -1,3 +0,0 @@
.push-button {
@extend %push-button;
}

View File

@ -1,51 +0,0 @@
@import './variables';
@import '../vendor/modularscale/modularscale';
@import '../vendor/ionicons-inline/ionicons';
@import './utils/font-size';
@import './utils/gutter';
@import './utils/heading-style';
@import './utils/section-gutter';
@import './utils/section-with-container';
@import './placeholders/push-button';
@import './base/base';
@import './base/fade';
@import './markdown/a-em';
@import './markdown/code';
@import './markdown/headings';
@import './markdown/local-anchor';
@import './markdown/p';
@import './markdown/table';
@import './markdown/ul';
@import './components/attribute-peg';
@import './components/announcements-item';
@import './components/announcements-list';
@import './components/back-button';
@import './components/body-area';
@import './components/comments-area';
@import './components/comments-details';
@import './components/comments-section';
@import './components/h2-section';
@import './components/h3-section';
@import './components/h3-section-list';
@import './components/headline-pub';
@import './components/hint-mark';
@import './components/home-button';
@import './components/intro-content';
@import './components/main-heading';
@import './components/missing-message';
@import './components/notice-box';
@import './components/page-actions';
@import './components/pages-list';
@import './components/pre-footer';
@import './components/push-button';
@import './components/related-posts-area';
@import './components/related-posts-callout';
@import './components/related-posts-group';
@import './components/related-posts-section';
@import './components/related-post-list';
@import './components/related-post-item';
@import './components/search-box';
@import './components/search-footer';
@import './components/site-header';
@import './components/top-nav';
@import './components/top-sheet';

View File

@ -1,16 +0,0 @@
#!/usr/bin/env bash
(
echo https://devhints.io/
(
git ls-files \
| grep -E '\.md$' \
| grep -v -E 'CONTRIBUTING|README|Readme' \
| grep -v -E '^_' \
| sort \
| uniq \
| sed 's/\.md$//g'
) \
| sed 's#^#https://devhints.io/#g'
) \
| xargs curl >/dev/null

View File

@ -1,23 +0,0 @@
#!/usr/bin/env bash
# Helper to copy the latest cheatsheets to the clipboard for CloudFlare
# purging. This ensures visitors will see new versions.
(
git log "master@{3 days ago}..HEAD" --pretty="" --name-only \
| grep -E '\.md$' \
| grep -v -E 'CONTRIBUTING|README|Readme' \
| grep -v -E '^_' \
| sort \
| uniq \
| sed 's/\.md$//g'
) \
| sed 's#^#https://devhints.io/#g' \
| xargs echo https://devhints.io/ \
| pbcopy
echo "Copied to clipboard."
echo "Purge it here:"
echo ""
echo " https://www.cloudflare.com/a/caching/devhints.io"
echo ""
echo "Then click 'Purge Individual Files'"

View File

@ -1,32 +0,0 @@
#!/usr/bin/env bash
set -eou pipefail
exit_failure() {
echo 'Failed :('
echo ''
echo ' If your build failed at this point, it means'
echo ' the site failed to generate. Check the project'
echo ' out locally and try to find out why.'
}
trap exit_failure ERR
files=(
_site/vim.html
_site/react.html
_site/index.html
)
for fn in "${files[@]}"; do
echo ''
echo -n "→ Checking: $fn... "
test -f "$fn"
grep -q '<script src' "$fn"
grep -q 'assets/packed/app.js' "$fn"
grep -q 'doctype html' "$fn"
grep -q 'link rel="canonical"' "$fn"
done
echo ''
echo ''
echo "✓ Smoke tests good :)"

View File

@ -1,4 +0,0 @@
---
type: other
---
@import '2015/style.sass'

View File

@ -1,8 +0,0 @@
---
type: other
---
// Generated by parcel (relative to _sass)
@import '../../assets/packed/app.css';
// From _sass
@import '2017/style.scss';

View File

@ -1,48 +0,0 @@
.site-header, .social-list, .about-the-site,
#see-also, #see-also+ul {
display: none;
}
/*.post-headline.-cheatsheet .prelude span:before {
content: 'cheatsheet for'
}*/
.post-list {
margin: 0;
padding-top: 0;
padding-bottom: 0;
}
.post-item {
max-width: 100%;
}
.post-headline.-cheatsheet .prelude {
max-width: 300px;
}
.post-headline, p.prelude {
margin-top: 0;
}
.post-headline.-cheatsheet .prelude {
font-size: 0.6em;
letter-spacing: 1px;
}
.post-headline {
margin-bottom: 1.5em;
}
.post-headline.-cheatsheet .prelude span {
padding: 0.75em 20px;
border-bottom: solid 1.5px #111;
}
.post-headline.-cheatsheet h1 {
font-size: 1.75em;
text-shadow: none;
}
pre {
border-top: solid 1px #ddd;
border-bottom: solid 1px #ddd;
background: none;
}
h3 {
margin: 1em 0;
}
pre {
margin-top: 1.1em;
margin-bottom: 1.1em;
}

View File

@ -1,43 +0,0 @@
(function () {
var tags = document.querySelectorAll('h1,h2,h3,h4,h5,h6,li,p,span');
for (var i=0, len=tags.length; i<len; i++) {
var tag = tags[i];
if ((~tag.innerHTML.indexOf('>') ||
(!tag.innerHTML.match(/https?:\/\//))))
continue;
tag.innerHTML = tag.innerHTML.replace(/https?:\/\/[^ ]*/g, function (url) {
url = url.replace(/[\.\),!]*$/, '');
var label = url;
label = label.replace(/^https?:\/\//, '')
.replace(/\/$/, '');
return "<a href='"+url+"'>"+label+"</a>";
});
}
})();
/* unorphan */
(function () {
var els = document.querySelectorAll('h1 a, h1, h2, p.brief-intro, .pull-quote');
for (var i = 0, len = els.length; i < len; i++) {
var el = els[i];
var last = el.lastChild;
if (last && last.nodeType === 3) {
console.log('=>', last.nodeValue, last.nodeValue.replace(/\s+([^\s]+\s*)$/g, '\xA0$1'));
last.nodeValue = last.nodeValue.replace(/\s+([^\s]+\s*)$/g, '\xA0$1');
}
}
})();
/* loaded */
document.documentElement.className += ' loaded';
/* hljs */
(function () {
var codes = document.querySelectorAll('pre > code');
for (var i = 0, len = codes.length; i < len; i++) {
var block = codes[i];
hljs.highlightBlock(block);
}
})();

View File

@ -1,201 +0,0 @@
/*
* pages list
*/
.pages-list {
font-size: 0.9em;
max-width: 800px;
margin: 60px auto;
overflow: hidden;
}
.pages-list a {
display: block;
padding: 6px 0;
text-align: left;
float: left;
width: 44%;
margin: 0 3%;
box-shadow: none;
transition: all 100ms linear;
}
.pages-list a .title,
.pages-list a .date {
display: block;
}
.pages-list a .title {
font-weight: bold;
color: #111;
float: left;
}
.pages-list a .date {
color: #aaa;
font-size: 0.9em;
float: right;
}
.pages-list a:hover .title,
.pages-list a:focus .title {
color: dodgerblue;
}
/*
* post headline
*/
.post-headline.-cheatsheet .prelude {
color: #111;
font-size: 0.85em;
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 0;
font-weight: bold;
}
.post-headline.-cheatsheet .prelude span:before {
content: 'Cheatsheet for';
}
@media (min-width: 769px) {
.post-headline.-cheatsheet .prelude {
max-width: 230px;
margin-left: auto;
margin-right: auto;
}
.post-headline.-cheatsheet .prelude span {
display: inline-block;
}
}
.post-headline.-cheatsheet h1 {
color: #111;
font-size: 3.5em;
text-shadow: 2px 2px 0 white, 3px 3px 0 #ddd;
margin-top: 20px;
}
/*
* about the site
*/
.about-the-site {
margin-top: 8em;
}
.about-the-site .back {
margin-right: 0;
}
/*
* markdown
*/
@media (min-width: 768px) {
h2 {
margin-top: 3em;
}
}
/*
* grey code
*/
.greycode td:first-child code,
.greycode th:first-child code {
background: white;
padding: 6px 8px 5px 8px;
border-radius: 3px;
}
.greycode td:first-child code + em,
.greycode th:first-child code + em {
color: #808890;
font-size: 0.9em;
margin: 0 5px;
}
.greycode a {
margin: 0 5px;
}
@media (min-width: 768px) {
table.greycode {
background: #fcfcfc;
border-radius: 4px;
border-top: 0;
border-bottom: solid 1px #c7d7ee;
}
table.greycode:not(.wide) {
width: calc(620px + 100px);
margin-left: -50px;
}
table.greycode thead:first-child > tr:first-child > th,
table.greycode thead:first-child > tr:first-child > td,
table.greycode tbody:first-child > tr:first-child > th,
table.greycode tbody:first-child > tr:first-child > td,
table.greycode.no-head thead:nth-child(2) > tr:first-child > th,
table.greycode.no-head thead:nth-child(2) > tr:first-child > td,
table.greycode.no-head tbody:nth-child(2) > tr:first-child > th,
table.greycode.no-head tbody:nth-child(2) > tr:first-child > td {
border-top: 0;
}
table.greycode thead > tr:first-child > th,
table.greycode thead > tr:first-child > td,
table.greycode tbody > tr:first-child > th,
table.greycode tbody > tr:first-child > td {
border-top: solid 1px #c7d7ee;
}
table.greycode td:first-child,
table.greycode th:first-child {
padding-left: 50px;
}
table.greycode td:last-child,
table.greycode th:last-child {
padding-right: 50px;
}
}
.hljs-comment {
font-style: normal;
}
.key-codes code {
background: #fdfdff;
padding: 3px 8px 3px 8px;
border-radius: 3px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
margin: 0 5px;
}
.key-codes code + code {
margin-left: 0;
}
.key-codes pre code {
background: transparent;
padding: 0;
box-shadow: none;
margin: 0;
}
.social-list.-collapse {
margin-top: 0;
position: absolute;
top: 40vh;
left: 30px;
width: 32px;
}
@media (max-width: 480px) {
.social-list.-collapse {
display: none;
}
}

24
astro.config.mjs Normal file
View File

@ -0,0 +1,24 @@
import { defineConfig } from 'astro/config'
import partytown from '@astrojs/partytown'
/*
* https://astro.build/config
*/
export default defineConfig({
site: 'https://devhints.io',
build: {
format: 'file' /* generate /my-post.html instead of /my-post/index.html */
},
prefetch: {
prefetchAll: true
},
server: {
host: true
} /* access from https://192.168.x.x/ */,
integrations: [partytown({ config: { forward: ['dataLayer.push'] } })],
markdown: {
// Syntax highlighting is handled by render()
syntaxHighlight: false
}
})

View File

@ -1,21 +0,0 @@
---
layout: blank
type: other
---
{% assign pages = site.pages
| where_exp: "page", 'page.type == "article"'
%}[
{% for page in pages
%}{% if forloop.index0 != 0 %},{% endif %}{
"id": {{ page.url | replace: ".html", "" | slice: 1, 9999 | jsonify }},
"title": {{ page.title | jsonify }},
"url": {{ page.url | replace: ".html", "" | jsonify }},
"category": {{ page.category | jsonify }},
"keywords": {{ page.keywords | jsonify }},
"content_html": {{ page.content | markdownify | strip | jsonify }},
"intro_html": {{ page.intro | markdownify | strip | jsonify }},
"description_html": {{ page.description | markdownify | strip | jsonify }},
"tags": {{ page.tags | jsonify }},
"updated": {{ page.updated | jsonify }}
}{% endfor %}
]

View File

@ -1,19 +0,0 @@
version: '3.8'
services:
web:
build: .
volumes:
- .:/app
- rubygems:/usr/local/bundle
- ./node_modules:/app/node_modules
- yarn_cache:/root/.cache/yarn
ports:
- '4001:4001'
- '35729:35729'
command: >
bash -c 'yarn; bundle; env PORT=4001 HOST=0.0.0.0 yarn run dev'
volumes:
rubygems:
node_modules:
yarn_cache:

View File

@ -1,139 +1,121 @@
# Note: Please don't edit redirects by hand.
# redirects are managed via src/redirects.ts.
[[redirects]]
force = true
from = "/brew"
to = "/homebrew"
[[redirects]]
force = true
from = "/commander-js"
to = "/commander.js"
[[redirects]]
force = true
from = "/css-animation"
to = "/css#animation"
[[redirects]]
force = true
from = "/css-background"
to = "/css#background"
[[redirects]]
force = true
from = "/css-font"
to = "/css#fonts"
[[redirects]]
force = true
from = "/css-selectors"
to = "/css#selectors"
[[redirects]]
force = true
from = "/date"
to = "/datetime"
[[redirects]]
force = true
from = "/es2015"
to = "/es6"
[[redirects]]
force = true
from = "/es2016"
to = "/es6"
[[redirects]]
force = true
from = "/es2017"
to = "/es6"
[[redirects]]
force = true
from = "/es2018"
to = "/es6"
[[redirects]]
force = true
from = "/expect.js"
to = "/expectjs"
[[redirects]]
force = true
from = "/factory_girl"
to = "/factory_bot"
[[redirects]]
force = true
from = "/fetch"
to = "/js-fetch"
[[redirects]]
force = true
from = "/flexbox"
to = "/css-flexbox"
[[redirects]]
force = true
from = "/flowtype"
to = "/flow"
[[redirects]]
force = true
from = "/gpgconf"
to = "/gnupg"
[[redirects]]
force = true
from = "/gpg"
to = "/gnupg"
[[redirects]]
force = true
from = "/gutom"
to = "/ph-food-delivery"
[[redirects]]
force = true
from = "/handlebars-js"
to = "/handlebars.js"
[[redirects]]
force = true
from = "/harvey-js"
to = "/harvey.js"
[[redirects]]
force = true
from = "/immutable-js"
to = "/immutable.js"
[[redirects]]
force = true
from = "/jade"
to = "/pug"
[[redirects]]
force = true
from = "/jinja2"
to = "/jinja"
[[redirects]]
force = true
from = "/package.json"
to = "/package-json"
[[redirects]]
force = true
from = "/package"
to = "/package-json"
[[redirects]]
force = true
from = "/phoenix-ecto@1.3"
to = "/phoenix-ecto"
[[redirects]]
force = true
from = "/sh"
to = "/bash"
[build]
command = "yarn build"
publish = "_site/"
environment = { NODE_VERSION = "20.9.0", RUBY_VERSION = "3.2.3", PYTHON_VERSION = "3.8" }
[[redirects]]
force = true
from = "/flexbox"
to = "/css-flexbox"
[[redirects]]
force = true
from = "/flowtype"
to = "/flow"
[[redirects]]
force = true
from = "/gpgconf"
to = "/gnupg"
[[redirects]]
force = true
from = "/gpg"
to = "/gnupg"
[[redirects]]
force = true
from = "/gutom"
to = "/ph-food-delivery"
[[redirects]]
force = true
from = "/handlebars-js"
to = "/handlebars.js"
[[redirects]]
force = true
from = "/harvey-js"
to = "/harvey.js"
[[redirects]]
force = true
from = "/immutable-js"
to = "/immutable.js"
[[redirects]]
force = true
from = "/jade"
to = "/pug"
[[redirects]]
force = true
from = "/package"
to = "/package-json"
[[redirects]]
force = true
from = "/package.json"
to = "/package-json"
[[redirects]]
force = true
from = "/phoenix-ecto@1.3"
to = "/phoenix-ecto"
[[redirects]]
force = true
from = "/sh"
to = "/bash"
[[redirects]]
force = true
from = "/commander-js"
to = "/commander.js"
[[redirects]]
force = true
from = "/es2015"
to = "/es6"
[[redirects]]
force = true
from = "/es2016"
to = "/es6"
[[redirects]]
force = true
from = "/es2017"
to = "/es6"
[[redirects]]
force = true
from = "/es2018"
to = "/es6"
[[redirects]]
force = true
from = "/expect.js"
to = "/expectjs"
[[redirects]]
force = true
from = "/factory_girl"
to = "/factory_bot"
[[redirects]]
force = true
from = "/css-animation"
to = "/css#animation"
[[redirects]]
force = true
from = "/css-background"
to = "/css#background"
[[redirects]]
force = true
from = "/css-font"
to = "/css#fonts"
[[redirects]]
force = true
from = "/css-selectors"
to = "/css#selectors"
[[redirects]]
force = true
from = "/brew"
to = "/homebrew"
[[redirects]]
force = true
from = "/date"
to = "/datetime"
[[redirects]]
force = true
from = "/fetch"
to = "/js-fetch"
command = "npm run build"
publish = "_site/"
[build.environment]
NODE_VERSION = "18.12.0"
PYTHON_VERSION = "3.8"
RUBY_VERSION = "2.7.6"

View File

@ -1,64 +1,61 @@
{
"name": "cheatsheets",
"description": "Devhints.io",
"version": "1.0.0",
"author": "Rico Sta. Cruz <rstacruz@users.noreply.github.com>",
"dependencies": {
"autoprefixer": "^9.8.2",
"dom101": "^2.2.1",
"hint.css": "^2.6.0",
"isotope-layout": "^3.0.6",
"lodash.noop": "^3.0.1",
"modularscale-sass": "^3.0.10",
"onmount": "^1.3.0",
"postcss-modules": "^2.0.0",
"prismjs": "^1.20.0",
"sanitize.css": "^11.0.1",
"sass": "^1.26.8"
},
"devDependencies": {
"@babel/core": "^7.10.2",
"@babel/preset-env": "^7.10.2",
"@rstacruz/prettier-plugin-markdown-code-fences": "^1.0.0",
"jest": "26.0.1",
"jest-html": "1.5.0",
"netlify-plugin-minify-html": "^0.2.3",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.4",
"prettier": "^2.0.5",
"wait-on": "^5.0.1"
},
"homepage": "https://devhints.io/",
"jest": {
"snapshotSerializers": [
"<rootDir>/node_modules/jest-html"
]
},
"license": "MIT",
"main": "index.js",
"name": "devhints-astro",
"version": "0.0.1",
"private": true,
"repository": "https://github.com/rstacruz/cheatsheets.git",
"type": "module",
"scripts": {
"build": "run-s -s 'parcel:*:build' jekyll:build",
"dev": "run-p -sl jekyll:watch 'parcel:*:watch'",
"jekyll:build": "bundle exec jekyll build",
"jekyll:watch": "wait-on assets/packed/app.js && wait-on _includes/2017/critical/critical-sheet.css && bundle exec jekyll serve --safe --trace --drafts --watch --incremental --host ${HOST:-0.0.0.0} --port ${PORT:-3000}",
"jest-html": "jest-html",
"parcel:app:build": "parcel build '_parcel/app.js' -d assets/packed --no-source-maps --no-autoinstall",
"parcel:app:watch": "parcel watch '_parcel/app.js' -d assets/packed --no-source-maps --no-autoinstall",
"parcel:build": "run-s -s 'parcel:*:build'",
"parcel:critical:build": "parcel build '_parcel/critical*.js' -d _includes/2017/critical --no-source-maps --no-autoinstall",
"parcel:critical:watch": "parcel watch '_parcel/critical*.js' -d _includes/2017/critical --no-source-maps --no-autoinstall",
"predev": "rm -rf assets/packed _includes/2017/critical",
"prejekyll:build": "bundle",
"prejekyll:watch": "bundle",
"prettier:format": "prettier --write '_parcel/**/*.{js,scss}'",
"test": "jest",
"test:smoke": "bash _support/smoke_test.sh"
"astro": "astro",
"build": "concurrently -m 1 \"npm:cache_markdown\" \"astro build --silent\"",
"cache_markdown": "bundle exec ruby src/ruby/cache_kramdown.rb *.md */*.md",
"ci": "concurrently -g \"npm:*:check\" \"vitest run\" \"npm:test:*\" \"npm:build\"",
"dev": "astro dev",
"eslint:check": "eslint .",
"eslint:format": "eslint --fix .",
"format": "concurrently -m 1 \"npm:*:format\"",
"prettier:check": "prettier --cache --list-different .",
"prettier:format": "prettier --cache --write .",
"preview": "astro preview",
"start": "astro dev",
"test": "vitest",
"test:playwright": "playwright test",
"test:ruby": "bundle exec ruby src/ruby/*.test.rb"
},
"volta": {
"node": "18.19.1",
"yarn": "1.22.22"
"dependencies": {
"@astrojs/partytown": "^2.0.2",
"@astrojs/ts-plugin": "^1.3.1",
"@fontsource/cousine": "^5.0.15",
"@mapbox/rehype-prism": "^0.8.0",
"@playwright/test": "^1.38.1",
"@rstacruz/rehype-sectionize": "^0.7.0",
"@types/mapbox__rehype-prism": "^0.8.1",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"astro": "^4.0.3",
"autocompleter": "^9.1.0",
"concurrently": "^8.2.1",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-astro": "^0.31.4",
"eslint-plugin-jsx-a11y": "^6.7.1",
"fuse.js": "^6.6.2",
"gray-matter": "^4.0.3",
"happy-dom": "^12.9.1",
"hint.css": "^2.7.0",
"html-inline-external": "^1.0.10",
"playwright": "^1.38.1",
"prettier": "^3.0.3",
"prettier-plugin-astro": "^0.13.0",
"prettier-plugin-organize-imports": "^3.2.3",
"prismjs": "1.29.0",
"rehype-parse": "^9.0.0",
"rehype-stringify": "^10.0.0",
"sanitize.css": "13.0.0",
"sass": "^1.69.1",
"snarkdown": "^2.0.0",
"tsx": "^4.7.1",
"unified": "^11.0.3",
"vitest": "^1.4.0",
"zod": "^3.22.4"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
"packageManager": "pnpm@8.15.4+sha256.cea6d0bdf2de3a0549582da3983c70c92ffc577ff4410cbf190817ddc35137c2"
}

20
playwright.config.ts Normal file
View File

@ -0,0 +1,20 @@
import { defineConfig } from '@playwright/test'
const port = process.env.PORT ?? 4321
const isCI = Boolean(process.env.CI)
export default defineConfig({
testMatch: /.*\.e2e\.[^.]*/,
webServer: {
command: `npm run dev -- --port ${port}`,
url: `http://localhost:${port}/`,
reuseExistingServer: !isCI /* Spawn dev server on CI */
},
expect: {
timeout: isCI ? 5000 : 2500 /* default: 5000 */
},
use: {
baseURL: `http://localhost:${port}/`
}
})

7826
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
Powerline:
┌─┐
└─

2
public/_headers Normal file
View File

@ -0,0 +1,2 @@
/_astro/*
Cache-Control: public, max-age=604800, immutable

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,10 +0,0 @@
---
layout: blank
type: other
---
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>{{ site.url }}/</loc></url>
{% for page in site.pages %}{% if page.type == 'article' %}<url><loc>{{ site.url }}{{ page.url | remove: '.html' }}</loc></url>
{% endif %}{% endfor %}
</urlset>

View File

@ -0,0 +1,11 @@
---
type Props = { token: string }
const props = Astro.props as Props
const token = { props }
---
<script
defer
type="text/partytown"
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon={`{"token": "${token}"}`}></script>

View File

@ -0,0 +1,25 @@
---
type Props = { measurementId: string }
const props = Astro.props as Props
const measurementId = props.measurementId
---
<script
type="text/partytown"
src={`https://www.googletagmanager.com/gtag/js?id=${measurementId}`}></script>
{/* prettier-ignore */}
<script
type="text/partytown"
data-ga-measurement-id={measurementId}
id="ga-init"
>
const measurementId = document
.getElementById('ga-init')
.getAttribute('data-ga-measurement-id')
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments) // eslint-disable-line
}
gtag('js', new Date())
gtag('config', measurementId)
</script>

View File

@ -0,0 +1,42 @@
---
import '../sass/full.scss'
import '@fontsource/cousine/400.css'
import '@fontsource/cousine/700.css'
import GoogleAnalytics from '~/analytics/GoogleAnalytics.astro'
import CloudflareAnalytics from '~/analytics/CloudflareAnalytics.astro'
import { cloudflareAnalytics, googleAnalytics } from '~/config'
export type Props = {
title?: string
bodyClass?: string
}
const props = Astro.props as Props
const analyticsEnabled = import.meta.env.PROD || true
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<link rel="shortcut icon" type="image/png" href="/assets/favicon.png" />
{/* Title */}
{props.title ? <title>{props.title}</title> : null}
{/* Google tag */}
{
analyticsEnabled && googleAnalytics.measurementId ? (
<GoogleAnalytics measurementId={googleAnalytics.measurementId} />
) : null
}
{
analyticsEnabled && cloudflareAnalytics.token ? (
<CloudflareAnalytics token={cloudflareAnalytics.token} />
) : null
}
<slot name="head" />
</head>
<body class={props.bodyClass ?? ''}>
<slot />
</body>
</html>

View File

@ -0,0 +1,46 @@
---
/*
* Simplified replacement for astro-seo. Is less opinionated
*/
export type Props = {
title?: string
meta?: Record<string, string | string[]>
metaProperties?: Record<string, string | string[]>
links?: Record<string, string>
}
const props = Astro.props as Props
function toArray(input: string | string[]): string[] {
return (Array.isArray(input) ? input : [input]).filter(Boolean)
}
---
{props.title ? <title>{props.title}</title> : null}
{
props.meta
? Object.entries(props.meta).flatMap(([name, contents]) =>
toArray(contents).map((content) => (
<meta name={name} content={content} />
))
)
: null
}
{
props.metaProperties
? Object.entries(props.metaProperties).flatMap(([property, contents]) =>
toArray(contents).map((content) => (
<meta property={property} content={content} />
))
)
: null
}
{
props.links
? Object.entries(props.links).map(([rel, href]) => (
<link rel={rel} href={href} />
))
: null
}

View File

@ -0,0 +1,52 @@
---
import { getUrlFromPage } from '~/lib/links'
import type { SheetPage } from '~/lib/page'
export type Props = {
class?: string
page?: SheetPage
}
const props = Astro.props as Props
const page = props.page
const url = getUrlFromPage(page, Astro.site)
const t = {
facebookShare: 'Share on Facebook',
twitterShare: 'Share on Twitter',
sheetDescription: 'The ultimate cheatsheet for {title}',
defaultDescription: 'Ridiculous collection of web development cheatsheets'
}
const title = page?.frontmatter?.title
const description = title
? t.sheetDescription.replace('{title}', title)
: t.defaultDescription
const tweet = `${description} ${url}`
// prettier-ignore
const facebookURL = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`
// prettier-ignore
const twitterURL = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweet)}`
---
<ul class={`social-list ${props.class ?? ''}`}>
<li class="facebook link hint--bottom" data-hint={t.facebookShare}>
<a
href={facebookURL}
target="share"
aria-label={t.facebookShare}
role="button"><span class="text"></span></a
>
</li>
{' '}
<li class="twitter link hint--bottom" data-hint={t.twitterShare}>
<a
href={twitterURL}
target="share"
aria-label={t.twitterShare}
role="button"><span class="text"></span></a
>
</li>
</ul>

View File

@ -0,0 +1,67 @@
---
import type { SheetPage } from '~/lib/page'
import SocialList from './SocialList.astro'
import { getEditLink } from '~/lib/links'
export type Props = {
noBack?: boolean
noShare?: boolean
noEdit?: boolean
page?: SheetPage
}
const props = Astro.props as Props
const t = {
title: 'Devhints.io',
editOnGitHub: 'Edit on GitHub',
edit: 'Edit',
backToHome: 'Back to home'
}
const editLink = getEditLink(props.page)
---
<nav class="top-nav" data-js-no-preview role="navigation">
<div class="container">
{
props.noBack !== true ? (
<div class="left">
<a class="home back-button" href="/" aria-label={t.backToHome} />
</div>
) : null
}
<a class="brand" href="/">
{t.title}
</a>
{
props.noShare !== true ? (
<div class="actions">
{/* Social share links */}
<SocialList class="social page-actions" page={props.page} />
{props.noEdit !== true ? (
<ul class="page-actions">
<li
class="link github -button hint--bottom"
data-hint={t.editOnGitHub}
>
<a href={editLink}>
<span class="text -visible">{t.edit}</span>
</a>
</li>
</ul>
) : null}
</div>
) : null
}
</div>
</nav>
<style lang="scss" is:global>
@import '../sass/2017/utils';
@import '../sass/2017/components/back-button';
@import '../sass/2017/components/page-actions';
@import '../sass/2017/components/top-nav';
</style>

View File

@ -0,0 +1,17 @@
---
import { carbon } from '~/config'
const useCarbon = import.meta.env.PROD
---
<div class="HeadlinePub" role="complementary">
{useCarbon ? <script async src={carbon.src} id="_carbonads_js" /> : null}
<span class="placeholder -one"></span>
<span class="placeholder -two"></span>
<span class="placeholder -three"></span>
<span class="placeholder -four"></span>
</div>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/headline-pub';
</style>

View File

@ -0,0 +1,16 @@
---
export type Props = {
class?: string
}
const props = Astro.props as Props
---
<div class={`push-button ${props.class ?? ''}`}>
<slot />
</div>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/placeholders/push-button';
@import '../../sass/2017/components/push-button';
</style>

View File

@ -0,0 +1,34 @@
---
import { announcement } from '~/config'
import snarkdown from 'snarkdown'
const body = snarkdown(announcement.body)
.split('<br />')
.map((text) => `<p>${text}</p>`)
.join('')
---
<div class="announcements-list">
<div
class="announcements-item item -hide"
data-js-dismissable={`{"id":"${announcement.id}"}`}
>
<h3 class="title">{announcement.title}</h3>
<div class="body" set:html={body} />
<button data-js-dismiss class="close"></button>
</div>
</div>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/announcements-item';
@import '../../sass/2017/components/announcements-list';
</style>
<script>
import { setupDismiss } from '~/scripts/v2017/behaviors_2/dismiss'
import { setupDismissable } from '~/scripts/v2017/behaviors_2/dismissable'
setupDismiss()
setupDismissable()
</script>

View File

@ -0,0 +1,17 @@
---
import type { SheetPage } from '~/lib/page'
import PageListItem from './PageListItem.astro'
export type Props = {
pages: SheetPage[]
}
const props = Astro.props as Props
---
{props.pages.map((page) => <PageListItem page={page} class="top-sheet" />)}
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/top-sheet';
</style>

View File

@ -0,0 +1,22 @@
---
import type { SheetPage } from '../../lib/page'
export type Props = {
page: SheetPage
class?: string
}
const props = Astro.props as Props
const page = props.page
const url = `/${page.slug}`
---
<a href={url} class={`article item -item-${page.slug} ${props.class ?? ''}`}>
<span class="info">
<code class="slug">{page.slug}</code>
<span class="title">
{page.frontmatter.title ?? page.slug}
</span>
{/* TODO attribute-peg */}
</span>
</a>

View File

@ -0,0 +1,124 @@
---
// import "../sass/critical-sheet.scss";
import snarkdown from 'snarkdown'
import { render } from '../lib/render'
import type { SheetPage } from '../lib/page'
import BaseLayout from './BaseLayout.astro'
import TopNav from './TopNav.astro'
import CommentsArea from './V2017Sheet/CommentsArea.astro'
import SearchFooter from './V2017Sheet/SearchFooter.astro'
import RelatedPosts from './V2017Sheet/RelatedPosts.astro'
import CarbonBox from './V2017/CarbonBox.astro'
import { getSEOPropsForPage } from '~/lib/seo/seo'
import { getJSONLDsForPage } from '~/lib/seo/jsonLd'
import SEO from '~/components/SEO/SEO.astro'
import NoticeBox from './V2017Sheet/NoticeBox.astro'
import { getEditLink } from '~/lib/links'
export type Props = {
page: SheetPage
}
const props = Astro.props as Props
const page = props.page
const mkdn = await render(page.markdown)
const seoProps = getSEOPropsForPage(page)
const jsonLdSchemas = getJSONLDsForPage(page)
const tags = page.frontmatter.tags ?? []
const deprecatedBy = page.frontmatter.deprecated_by
const title: string = page.frontmatter.title ?? page.slug
const editUrl = getEditLink(page)
const intro: string | null = page.frontmatter.intro
? `<p>${snarkdown(page.frontmatter.intro)}</p>`
: null
---
<BaseLayout bodyClass="UseCompactHeader HighlightPubFirstLine">
<Fragment slot="head">
<SEO {...seoProps} />
{
jsonLdSchemas.map((schema) => (
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
))
}
</Fragment>
<TopNav page={page} />
<div class="body-area">
<header class="main-heading -center" role="banner">
<h1 class="h1">{title}{' '}<em>cheatsheet</em></h1>
{/* Publicite */}
<div class="pubbox" data-js-no-preview><CarbonBox /></div>
</header>
{/* WIP */}
{
tags.includes('WIP') ? (
<NoticeBox>
This page is a work in progress. You can help by{' '}
<a href={editUrl}>suggesting edits</a>!
</NoticeBox>
) : null
}
{/* Deprecated */}
{
deprecatedBy ? (
<NoticeBox>
<strong>Deprecated:</strong> This guide covers an older version.
<a href={deprecatedBy}>A newer version is available here.</a>
</NoticeBox>
) : null
}
{
intro ? (
<div class="intro-content MarkdownBody">
<Fragment set:html={intro} />
</div>
) : null
}
<main
class="post-content MarkdownBody"
data-js-main-body
data-js-anchors
role="main"
>
<Fragment set:html={mkdn.html} />
</main>
</div>
<div class="pre-footer" data-js-no-preview><i class="icon"></i></div>
<CommentsArea identifier={page.slug} />
<SearchFooter />
<RelatedPosts page={page} />
</BaseLayout>
<style lang="scss" is:global>
@import '../sass/2017/utils';
@import '../sass/2017/markdown/a-em';
@import '../sass/2017/markdown/code';
@import '../sass/2017/markdown/headings';
@import '../sass/2017/markdown/local-anchor';
@import '../sass/2017/markdown/p';
@import '../sass/2017/markdown/table';
@import '../sass/2017/markdown/ul';
@import '../sass/2017/components/body-area';
@import '../sass/2017/components/h2-section';
@import '../sass/2017/components/h3-section';
@import '../sass/2017/components/h3-section-list';
@import '../sass/2017/components/hint-mark';
@import '../sass/2017/components/intro-content';
@import '../sass/2017/components/main-heading';
@import '../sass/2017/components/pre-footer';
</style>
<script>
import { setupNoPreview } from '~/scripts/v2017/behaviors_2/no-preview'
import { setupAnchors } from '~/scripts/v2017/behaviors_2/anchors'
setupNoPreview()
setupAnchors()
</script>

View File

@ -0,0 +1,60 @@
---
import { disqus, site } from '../../config'
const t = {
suffix: 'for this cheatsheet.',
link: 'Write yours!'
}
export type Props = {
identifier: string
}
const props = Astro.props as Props
const url = `${site.url}/${props.identifier}`
const { identifier } = props
---
<section class="comments-area" id="comments" data-js-no-preview>
<div class="container">
<details class="comments-details">
<summary>
<strong class="count">
<span
class="disqus-comment-count"
data-disqus-identifier={identifier}
data-disqus-url={url}>0 Comments</span
>
</strong>
{' '}
<span class="suffix">{t.suffix}</span>
{' '}
<span class="fauxlink">{t.link}</span>
</summary>
<div class="comments-section">
<div class="comments">
<div id="disqus_thread"></div>
</div>
</div>
</details>
</div>
<noscript
data-js-disqus={JSON.stringify({
host: disqus.host,
url,
identifier
})}></noscript>
</section>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/comments-area';
@import '../../sass/2017/components/comments-details';
@import '../../sass/2017/components/comments-section';
</style>
<script>
import { setupDisqus } from '~/scripts/v2017/behaviors_2/disqus'
setupDisqus()
</script>

View File

@ -0,0 +1,12 @@
---
---
<aside class="notice-box MarkdownBody">
<slot />
</aside>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/notice-box';
</style>

View File

@ -0,0 +1,35 @@
---
import type { SheetPage } from '~/lib/page'
export type Props = {
class?: string
page: SheetPage
}
const props = Astro.props as Props
const page = props.page
const url = `/${page.slug}`
const t = {
suffix: 'cheatsheet'
}
---
<li class={`related-post-item ${props.class ?? ''}`}>
<a href={url}>
<strong>{page.frontmatter.title}</strong>
{' '}
<span>
{t.suffix}
{/* TODO attribute-peg */}
<!-- {% if include.page.layout == '2017/sheet' %} -->
<!-- <abbr class='attribute-peg -new-layout hint--bottom' data-hint='New layout!'><span></span></abbr> -->
<!-- {% endif %} -->
</span>
</a>
</li>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/related-post-item';
</style>

View File

@ -0,0 +1,93 @@
---
import { getPages, type SheetPage } from '~/lib/page'
import { getTopPages, getRelatedPages } from '~/lib/page/queries'
import { etc } from '~/config'
import RelatedPostItem from '~/components/V2017Sheet/RelatedPostItem.astro'
import PushButton from '~/components/V2017/PushButton.astro'
export type Props = {
page: SheetPage
}
const props = Astro.props as Props
const page = props.page
const t = {
callout: {
description:
'Over {size} curated cheatsheets, by developers for developers.',
link: 'Devhints home'
},
group: {
top: 'Top cheatsheets',
other: 'Other cheatsheets',
category: 'Other {category} cheatsheets'
}
}
const calloutDescription = t.callout.description.replace(
'{size}',
etc.advertisedSheetCount.toString()
)
const category = page.frontmatter.category
const categoryHeading = category
? t.group.category.replace('{category}', category)
: t.group.other
const pages = await getPages()
const relatedPages = getRelatedPages(pages, page, { maxCount: 6 })
const topPages = getTopPages(pages, page, { maxCount: 6 })
---
<footer class="related-posts-area" id="related" data-js-no-preview>
<div class="container">
<div class="related-posts-section">
{/* Callout */}
<div class="callout">
<a class="related-posts-callout" href="/">
<div class="text">
<i class="icon"></i>
<span class="description">{calloutDescription}</span>
<PushButton class="-dark">{t.callout.link}</PushButton>
</div>
</a>
</div>
{/* Posts in the same category */}
<div class="group">
<div class="related-posts-group">
<h3>{categoryHeading}</h3>
</div>
<ul class="related-post-list">
{
relatedPages.map((page) => (
<RelatedPostItem page={page} class="item" />
))
}
</ul>
</div>
{/* Top pages */}
<div class="group">
<div class="related-posts-group">
<h3>{t.group.top}</h3>
</div>
<ul class="related-post-list">
{topPages.map((page) => <RelatedPostItem page={page} class="item" />)}
</ul>
</div>
</div>
</div>
</footer>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/related-posts-area';
@import '../../sass/2017/components/related-posts-callout';
@import '../../sass/2017/components/related-posts-group';
@import '../../sass/2017/components/related-posts-section';
@import '../../sass/2017/components/related-post-list';
</style>

View File

@ -0,0 +1,22 @@
---
import SearchForm from './SearchForm.astro'
---
<footer class="search-footer" data-js-no-preview>
<div class="container">
<div class="search-footer-section">
<div class="search">
<SearchForm class="-small" />
</div>
<div class="links">
<a class="home-button" href="/"><i></i></a>
</div>
</div>
</div>
</footer>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/home-button';
@import '../../sass/2017/components/search-footer';
</style>

View File

@ -0,0 +1,69 @@
---
import 'autocompleter/autocomplete.css'
import { etc } from '../../config'
export type Props = {
class?: string
/** True for the homepage */
isLive: boolean
}
const t = {
prefix: 'devhints.io',
defaultPlaceholder: 'Search {size}+ cheatsheets',
homePlaceholder: 'Search...'
}
const props = Astro.props as Props
let placeholder = props.isLive ? t.homePlaceholder : t.defaultPlaceholder
placeholder = placeholder.replace('{size}', etc.advertisedSheetCount.toString())
---
<form class="search" action="/" method="get" data-js-search-form>
<label class={`search-box ${props.class ?? ''}`}>
<span class="prefix">{t.prefix}</span>
<span class="sep">/</span>
{
(
<input
name="q"
type="text"
class="input"
{...(props.isLive ? { autofocus: true } : {})}
data-js-search-input
placeholder={placeholder}
/>
)
}
</label>
</form>
<script>
import { onScrollVisible } from '~/lib/domutils/onScrollVisible'
// Prevent <enter> from submitting the form
document
.querySelectorAll<HTMLFormElement>('[data-js-search-form]')
.forEach((form) => {
form.addEventListener('submit', (event) => {
event.preventDefault()
})
})
document
.querySelectorAll<HTMLInputElement>('[data-js-search-input]')
.forEach((input) => {
onScrollVisible(input, async () => {
const { setup } = await import('./SearchForm.script')
setup(input)
})
})
</script>
<style lang="scss" is:global>
@import '../../sass/2017/utils';
@import '../../sass/2017/components/search-box';
@import '../../sass/2017/components/autocomplete';
</style>

View File

@ -0,0 +1,25 @@
import autocomplete from 'autocompleter'
import { fetchFuse, parseFuse } from '~/lib/fuseSearch/fuseSearch'
export async function setup(input: HTMLInputElement) {
let fuse
autocomplete<{ label: string; value: string }>({
input,
fetch: async (text: string, update) => {
fuse ??= parseFuse(await fetchFuse())
const rows: Array<{ item: { title: string; slug: string } }> =
fuse.search(text, { limit: 10 })
update(
rows.map((row) => ({ label: row.item.title, value: row.item.slug }))
)
},
onSelect: (item) => {
// ^ { label, value }
const slug = item.value
window.location.href = `/${slug}`
}
})
}

75
src/config.ts Normal file
View File

@ -0,0 +1,75 @@
export const site = {
url: 'https://devhints.io',
title: 'Devhints.io cheatsheets'
} as const
export const etc = {
advertisedSheetCount: 357
} as const
export const disqus = {
enabled: true,
host: 'devhints.disqus.com'
} as const
export const cloudflareAnalytics = {
token: '93ebff376c05423d8e6c1dfbe406a172'
} as const
export const googleAnalytics = {
measurementId: 'G-N7TC6B227L'
} as const
export const github = {
repositoryUrl: 'https://github.com/rstacruz/cheatsheets',
branch: 'master'
} as const
export const urls = {
newCheatsheetUrl: 'https://github.com/rstacruz/cheatsheets/issues/907'
} as const
export const carbon = {
src: 'https://cdn.carbonads.com/carbon.js?serve=CE7IK5QM&placement=devhintsio'
} as const
export const categories = [
'Analytics',
'Ansible',
'Apps',
'C-like',
'CLI',
'CSS',
'Databases',
'Devops',
'Elixir',
'Git',
'HTML',
'Java & JVM',
'JavaScript',
'JavaScript libraries',
'Jekyll',
'Ledger',
'Markup',
'macOS',
'Node.js',
'PHP',
'Python',
'Rails',
'React',
'Ruby',
'Ruby libraries',
'Vim',
'Fitness',
'Others'
]
export const announcement = {
id: '2023-12-14',
title: `We're on Twitter ♥️`,
body: [
`Follow [@devhints](https://twitter.com/devhints) on X/Twitter for daily "today I learned" snippets.`,
``,
`Also: I've started a new blog with some insights on web development. Have a look! [**ricostacruz.com/posts**](https://ricostacruz.com/posts?utm_source=devhints)`
].join('\n')
}

1
src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="astro/client" />

View File

@ -0,0 +1,20 @@
/**
* Trigger a `callback` when an `element` is made visible.
*/
export function onScrollVisible(element: HTMLElement, callback: () => void) {
if (typeof IntersectionObserver !== 'function') {
callback()
return
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
callback()
observer.unobserve(element)
}
})
})
observer.observe(element)
}

View File

@ -0,0 +1,34 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Simple pages scenario > multi-result search 1`] = `
[
{
"item": {
"slug": "react",
"title": "React",
},
"refIndex": 0,
},
{
"item": {
"slug": "react-router",
"title": "React Router",
},
"refIndex": 2,
},
]
`;
exports[`Simple pages scenario > no-result search 1`] = `[]`;
exports[`Simple pages scenario > one-result search 1`] = `
[
{
"item": {
"slug": "vim",
"title": "Vim",
},
"refIndex": 1,
},
]
`;

View File

@ -0,0 +1,37 @@
import { buildFuseIndex, parseFuse } from './fuseSearch'
describe('Simple pages scenario', () => {
// prettier-ignore
const pages = {
"react": { slug: "react", frontmatter: { title: "React" } },
"vim": { slug: "vim", frontmatter: { title: "Vim" } },
"react-router": { slug: "react-router", frontmatter: { title: "React Router" } },
}
let fuse: ReturnType<typeof parseFuse>
runTest({
label: 'one-result search',
query: 'vim'
})
runTest({
label: 'multi-result search',
query: 'react'
})
runTest({
label: 'no-result search',
query: 'skinamarink'
})
beforeEach(() => {
const externalData = buildFuseIndex(pages)
fuse = parseFuse(externalData)
})
function runTest({ label, query }: { label: string; query: string }) {
test(label, () => {
const results = fuse.search(query)
expect(results).toMatchSnapshot()
})
}
})

View File

@ -0,0 +1,48 @@
import Fuse from 'fuse.js'
import type { SheetPage } from '~/lib/page'
/**
* This is what gets served in searchindex.json
*/
export type ExternalSearchData = {
index: ReturnType<FuseIndex['toJSON']>
rows: Array<{ title: string; slug: string }>
}
export type Row = { title: string; slug: string }
export type FuseIndex = Fuse.FuseIndex<Row>
/** A subset of `SheetPage` needed for search indexing */
type PartialSheetPage = {
slug: SheetPage['slug']
frontmatter: Pick<SheetPage['frontmatter'], 'title'>
}
/**
* Get `pages` and turn them into a fuse index json.
*/
export function buildFuseIndex(pages: Record<string, PartialSheetPage>) {
const rows = Object.values(pages).map((page): Row => {
return { title: (page.frontmatter.title ?? '') as string, slug: page.slug }
})
const myIndex: FuseIndex = Fuse.createIndex(['title', 'slug'], rows)
const indexJSON = myIndex.toJSON()
const result = { index: indexJSON, rows }
return result
}
export async function fetchFuse() {
const res = await fetch('/searchindex.json')
if (res.status > 400) throw new Error('Failed to fetch searchindex.json')
return res.json()
}
export function parseFuse(data: ExternalSearchData) {
const index = Fuse.parseIndex(data.index)
const fuse = new Fuse<Row>(data.rows, {}, index)
return fuse
}

58
src/lib/kramdown.ts Normal file
View File

@ -0,0 +1,58 @@
import { spawn } from 'node:child_process'
import crypto from 'node:crypto'
import { readFile } from 'node:fs/promises'
export type KramdownResult = {
html: string
}
/**
* Renders via Ruby Kramdown
*/
export async function renderKramdown(input: string): Promise<KramdownResult> {
return (await renderKramdownFromCache(input)) ?? renderKramdownJIT(input)
}
/**
* Tries to get from .cache/ (left by `npm run cache_markdown`).
*/
async function renderKramdownFromCache(
input: string
): Promise<KramdownResult | undefined> {
const digest = crypto.createHash('sha256').update(input.trim()).digest('hex')
const cachePath = `.cache/${digest}.html`
try {
const result = await readFile(cachePath, 'utf-8')
return { html: result }
} catch (err) {
if (import.meta.env.PROD) console.log(`Cache MISS (${cachePath})`)
}
}
/**
* Renders via Kramdown by invoking Ruby.
*/
function renderKramdownJIT(input: string): Promise<KramdownResult> {
return new Promise((resolve, reject) => {
let output = ''
const child = spawn('bundle', ['exec', 'ruby', './src/ruby/kramdown.rb'])
child.stdin.write(input)
child.stdin.end()
child.stdout.on('data', (data: string) => {
output += data
})
child.on('exit', (code: number) => {
if (code !== 0) {
reject(new Error(`Exited with code ${code}`))
} else {
resolve({ html: output })
}
})
})
}

17
src/lib/links.ts Normal file
View File

@ -0,0 +1,17 @@
import { github } from '~/config'
import type { SheetPage } from './page'
export function getEditLink(page: { slug: string } | null | undefined) {
if (!page) return null
return `${github.repositoryUrl}/blob/${github.branch}/${page.slug}.md`
}
export function getUrlFromPage(
page?: SheetPage | undefined,
siteUrl?: URL | undefined
) {
if (!siteUrl) throw new Error('No site URL found')
if (!page) return siteUrl.toString()
if (page.slug) return `${siteUrl}${page.slug}`
throw new Error("Can't get URL from page")
}

89
src/lib/page.test.ts Normal file
View File

@ -0,0 +1,89 @@
import { getPages, mapGlobToPages } from './page'
let pages: Awaited<ReturnType<typeof getPages>>
let keys: string[]
beforeEach(async () => {
pages ??= await getPages()
keys ??= Object.keys(pages)
})
test('return pages', () => {
expect(keys).toContain('react')
expect(keys).toContain('bash')
expect(keys).toContain('tests/basic')
})
test('frontmatter', () => {
const page = pages.bash
expect(page.slug).toEqual('bash')
expect(typeof page.markdown).toEqual('string')
expect(typeof page.frontmatter.title).toEqual('string')
expect(typeof page.frontmatter.keywords).toEqual('object')
})
describe('mapGlobToPages()', () => {
test('basic scenario', () => {
const result = mapGlobToPages({
bash: ['---', 'title: Bash', '---', '# hi'].join('\n')
})
expect(result).toMatchInlineSnapshot(`
{
"bash": {
"frontmatter": {
"title": "Bash",
},
"markdown": "# hi",
"slug": "bash",
},
}
`)
})
test('parsing numbers', () => {
const result = mapGlobToPages({
'101': ['---', 'title: 101', '---', '# hi'].join('\n')
})
expect(result).toMatchInlineSnapshot(`
{
"101": {
"frontmatter": {
"title": "101",
},
"markdown": "# hi",
"slug": "101",
},
}
`)
})
test('invalid type', () => {
expect(() => {
mapGlobToPages({
'101': ['---', 'title: false', '---', '# hi'].join('\n')
})
}).toThrowErrorMatchingInlineSnapshot(
`[FrontmatterValidationError: Zod validation error: '101' {"title":["Invalid input"]}]`
)
})
test('missing frontmatter', () => {
const result = mapGlobToPages({
'101': ['---', 'title: 101', '---', '# hi'].join('\n')
})
expect(result).toMatchInlineSnapshot(`
{
"101": {
"frontmatter": {
"title": "101",
},
"markdown": "# hi",
"slug": "101",
},
}
`)
})
})

78
src/lib/page.ts Normal file
View File

@ -0,0 +1,78 @@
import grayMatter from 'gray-matter'
import { ZodError } from 'zod'
import {
SheetFrontmatterSchema,
type SheetFrontmatter
} from '~/types/SheetFrontmatter'
export type SheetPage = {
slug: string
markdown: string
frontmatter: SheetFrontmatter
}
class FrontmatterValidationError extends Error {
filePath: string
constructor(message: string, options: ErrorOptions & { filePath: string }) {
super(message, options)
this.name = 'FrontmatterValidationError'
this.filePath = options.filePath
}
}
/**
* Returns pages
*/
export async function getPages(): Promise<Record<string, SheetPage>> {
const files = import.meta.glob(
[
'../../*.md',
'../../*/*.md',
'!../../README.md',
'!../../CONTRIBUTING.md',
'!../../404.md',
'!../../index.md',
'!../../index@2016.md'
],
{
eager: true,
query: '?raw',
import: 'default'
}
) as Record<string, string>
return mapGlobToPages(files)
}
export function mapGlobToPages(
files: Record<string, string>
): Record<string, SheetPage> {
const result: Record<string, SheetPage> = {}
for (const [filePath, rawContent] of Object.entries(files)) {
const slug = filePath.replace(/^\.\.\/\.\.\//, '').replace(/\.md$/, '')
// ^ filePath == "../../README.md"
const res = grayMatter(rawContent)
// ^ res == { content: '...', data: {} }
const frontmatter = (() => {
try {
return SheetFrontmatterSchema.parse(res.data)
} catch (err) {
if (!(err instanceof ZodError)) throw err
const debugInfo = JSON.stringify(err.flatten().fieldErrors)
const newErr = new FrontmatterValidationError(
`Zod validation error: '${filePath}' ${debugInfo}`,
{ cause: err, filePath }
)
throw newErr
}
})()
result[slug] = { slug, markdown: res.content, frontmatter }
}
return result
}

24
src/lib/page/accessors.ts Normal file
View File

@ -0,0 +1,24 @@
/*
* Accessors: things that get stuff from a specific record. Usually in the form
* of `get(page, ...) -> any`
*/
import type { SheetPage } from '../page'
/**
* Check if a page has a tag
*/
export function hasTag(page: SheetPage, tagName: string): boolean {
return (
(page.frontmatter.tags && page.frontmatter.tags.includes(tagName)) || false
)
}
/**
* Checks if something should appear on the homepage
*/
export function isListed(page: SheetPage): boolean {
return page.frontmatter.category !== 'Hidden'
}

122
src/lib/page/queries.ts Normal file
View File

@ -0,0 +1,122 @@
import { categories } from '../../config'
import type { SheetPage } from '../page'
import { hasTag, isListed } from './accessors'
export type Category = {
pages: SheetPage[]
title: string
id: string
}
/**
* Returns categories and their corresponding pages.
*/
export function getPagesByCategory(pages: Record<string, SheetPage>) {
const pageCategories: Record<string, Category> = {}
for (const category of categories) {
pageCategories[category] = { pages: [], title: category, id: category }
}
for (const page of Object.values(pages)) {
if (!isListed(page)) continue
const categoryName = page.frontmatter.category ?? 'Others'
if (!categoryName) continue
let cat = pageCategories[categoryName]
if (!cat) cat = pageCategories['Others']
cat.pages.push(page)
}
return pageCategories
}
/**
* Returns categories and their corresponding pages.
*/
export function getRecentPages(
pages: Record<string, SheetPage>,
options?: { maxCount?: number }
): SheetPage[] {
const { maxCount = 8 } = options ?? {}
return Object.values(pages)
.filter((page) => isListed(page) && page.frontmatter.updated)
.sort(compare('desc', (page: SheetPage) => page.frontmatter.updated ?? ''))
.slice(0, maxCount)
}
/**
* Return featured pages for the home page
*/
export function getFeaturedPages(
pages: Record<string, SheetPage>,
options?: { maxCount?: number }
): SheetPage[] {
const { maxCount = 8 } = options ?? {}
return Object.values(pages)
.filter((page) => isListed(page) && hasTag(page, 'Featured'))
.sort(compare('asc', (page: SheetPage) => page.slug ?? ''))
.slice(0, maxCount)
}
/**
* Top pages (highest weight)
*/
export function getTopPages(
pages: Record<string, SheetPage>,
referencePage: SheetPage,
options?: { maxCount?: number }
): SheetPage[] {
const { maxCount = 6 } = options ?? {}
return Object.values(pages)
.filter((page) => isListed(page) && 'weight' in page.frontmatter)
.filter((page) => page.slug !== referencePage.slug)
.sort(compare('asc', (page: SheetPage) => page.slug ?? ''))
.sort(compare('asc', (page: SheetPage) => page.frontmatter.weight ?? 0))
.slice(0, maxCount)
}
/**
* Top pages (highest weight)
*/
export function getRelatedPages(
pages: Record<string, SheetPage>,
referencePage: SheetPage,
options?: { maxCount?: number }
): SheetPage[] {
const { maxCount = 6 } = options ?? {}
const category = referencePage.frontmatter.category
return Object.values(pages)
.filter((page) => isListed(page) && page.frontmatter.category === category)
.filter((page) => page.slug !== referencePage.slug)
.sort(compare('asc', (page: SheetPage) => page.slug ?? ''))
.sort(compare('asc', (page: SheetPage) => page.frontmatter.weight ?? 0))
.slice(0, maxCount)
}
/**
* Helper: Create a comparator function
*/
function compare<T>(
direction: 'asc' | 'desc',
accessor: (input: T) => number | string
) {
const k = direction === 'desc' ? -1 : 1
return (a: T, b: T): number => {
const va = accessor(a)
const vb = accessor(b)
return k * (va === vb ? 0 : va > vb ? 1 : -1)
}
}

89
src/lib/render.test.ts Normal file
View File

@ -0,0 +1,89 @@
import { render } from './render'
it('h3 only', async () => {
const input = ['### H3', '', 'This is some h3'].join('\n')
const { html } = await render(input)
expect(html).toMatchInlineSnapshot(`
"<section class="h2-section"><div class="body h3-section-list">
<section class="h3-section"><h3 id="h3">H3</h3><div class="body">
<p>This is some h3</p>
</div></section></div></section>"
`)
})
it('multiple h3s', async () => {
const input = ['### One', 'x', '### Two', 'y'].join('\n')
const { html } = await render(input)
expect(html).toMatchInlineSnapshot(`
"<section class="h2-section"><div class="body h3-section-list">
<section class="h3-section"><h3 id="one">One</h3><div class="body">
<p>x</p>
</div></section><section class="h3-section"><h3 id="two">Two</h3><div class="body">
<p>y</p>
</div></section></div></section>"
`)
})
it('multiple h2s and h3s', async () => {
const input = [
'## Sun',
'### One',
'x',
'### Two',
'y',
'## Moon',
'### Three',
'x',
'### Four',
'y'
].join('\n')
const { html } = await render(input)
expect(html).toMatchInlineSnapshot(`
"<section class="h2-section"><h2 id="sun">Sun</h2><div class="body h3-section-list">
<section class="h3-section"><h3 id="one">One</h3><div class="body">
<p>x</p>
</div></section><section class="h3-section"><h3 id="two">Two</h3><div class="body">
<p>y</p>
</div></section></div></section><section class="h2-section"><h2 id="moon">Moon</h2><div class="body h3-section-list">
<section class="h3-section"><h3 id="three">Three</h3><div class="body">
<p>x</p>
</div></section><section class="h3-section"><h3 id="four">Four</h3><div class="body">
<p>y</p>
</div></section></div></section>"
`)
})
it('nothing', async () => {
const input = ['Nothing'].join('\n')
const { html } = await render(input)
expect(html).toMatchInlineSnapshot(`
"<section class="h2-section"><div class="body h3-section-list">
<section class="h3-section"><div class="body"><p>Nothing</p>
</div></section></div></section>"
`)
})
it('h3s with a class', async () => {
const input = ['### One', 'x', '### Two', '{: .-prime}', 'y'].join('\n')
const { html } = await render(input)
expect(html).toMatchInlineSnapshot(`
"<section class="h2-section"><div class="body h3-section-list">
<section class="h3-section"><h3 id="one">One</h3><div class="body">
<p>x</p>
</div></section><section class="h3-section -prime"><h3 class="-prime" id="two">Two</h3><div class="body -prime">
<p>y</p>
</div></section></div></section>"
`)
})
it('h2 class', async () => {
const input = ['## Intro', '{: .-three-column}', '### One', 'x'].join('\n')
const { html } = await render(input)
expect(html).toMatchInlineSnapshot(`
"<section class="h2-section -three-column"><h2 class="-three-column" id="intro">Intro</h2><div class="body h3-section-list -three-column">
<section class="h3-section"><h3 id="one">One</h3><div class="body">
<p>x</p>
</div></section></div></section>"
`)
})

119
src/lib/render.ts Normal file
View File

@ -0,0 +1,119 @@
import rehypePrism from '@mapbox/rehype-prism'
import rehypeParse from 'rehype-parse'
import rehypeStringify from 'rehype-stringify'
import { unified } from 'unified'
import { renderKramdown } from './kramdown'
import { plugin as rehypeSectionize } from '@rstacruz/rehype-sectionize'
const PRISM_CONFIG = {
// For a list of languages Prism supports:
// https://github.com/PrismJS/prism/tree/master/components
alias: {
bash: ['sh', 'fish'],
ini: ['dosini'],
pug: ['jade'],
// "ignore" actually is for gitignore files, but it's closest to a
// neutral highlighting that I can find
ignore: [
// "nohighlight" was a Jekyll thing to prevent syntax highlighting
'nohighlight',
'org'
]
}
}
const REHYPE_SECTIONIZE_CONFIG = [
{
level: 'h2',
prelude: {
enabled: true,
tagName: 'section',
properties: { className: 'h3-section-list' }
},
section: {
addHeadingClass: true,
tagName: 'section',
properties: { className: 'h2-section' }
},
body: {
addHeadingClass: true,
enabled: true,
tagName: 'div',
properties: { className: 'body h3-section-list' }
}
},
{
level: 'h3',
prelude: {
enabled: false
},
section: {
addHeadingClass: true,
properties: { className: 'h3-section' },
tagName: 'section'
},
body: {
addHeadingClass: true,
enabled: true,
properties: { className: 'body' },
tagName: 'div'
}
}
]
/**
* Renders Markdown to HTML via Kramdown and applies some post-processing.
*/
export async function render(input: string): Promise<{ html: string }> {
let { html } = await renderKramdown(input)
html = addInitialH2(html)
html = addH3s(html)
html = await processRehype(html)
html = removeBlankHeadings(html)
return { html }
}
// Inject extra H2 if needed. This fixes layout issues
function addInitialH2(html: string): string {
if (html.trim().startsWith('<h2')) return html
return `<h2></h2>\n${html}`
}
// Add extra H3's in cases of `h2 + (ul|p|ol)`
// Fixes layout issues in "Also see" sections (eg, /awscli)
// The blank H3's will be removed later
function addH3s(html: string): string {
html = html.replace(
/(\/h2>[\s\r\n]*)(<(?:ul|p|ol))/g,
(_, closing, opening) => {
return `${closing}<h3></h3>${opening}`
}
)
return html
}
function removeBlankHeadings(html: string): string {
return html.replace(/<h3><\/h3>/g, '').replace(/<h2><\/h2>/g, '')
}
/**
* Runs through Rehype to add syntax highlighting and more.
*/
async function processRehype(inputHtml: string): Promise<string> {
const processResult = await unified()
.use(rehypeParse)
// @ts-expect-error dunno how to fix this
.use(rehypePrism, PRISM_CONFIG)
.use(rehypeStringify)
// @ts-expect-error dunno how to fix this
.use(rehypeSectionize, REHYPE_SECTIONIZE_CONFIG)
.process(inputHtml)
let html = String(processResult)
html = html
.replace('<html><head></head><body>', '')
.replace('</body></html>', '')
return html
}

View File

@ -0,0 +1,136 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`getSEOPropsForPage() > title only 1`] = `
{
"links": {
"canonical": "https://devhints.io/react",
},
"meta": {
"app:pageurl": "https://devhints.io/react",
"description": "The one-page guide to React: usage, examples, links, snippets, and more.",
},
"metaProperties": {
"article:tag": [],
"og:description": "The one-page guide to React: usage, examples, links, snippets, and more.",
"og:image": "https://assets.devhints.io/previews/react.jpg",
"og:image:height": "471",
"og:image:width": "900",
"og:site_name": "Devhints.io cheatsheets",
"og:title": "React cheatsheet",
"og:type": "article",
"og:url": "https://devhints.io/react",
"twitter:description": "The one-page guide to React: usage, examples, links, snippets, and more.",
"twitter:image": "https://assets.devhints.io/previews/react.jpg",
"twitter:title": "React cheatsheet",
},
"title": "React cheatsheet",
}
`;
exports[`getSEOPropsForPage() > with Markdown description 1`] = `
{
"links": {
"canonical": "https://devhints.io/react",
},
"meta": {
"app:pageurl": "https://devhints.io/react",
"description": "A React cheatsheet (Markdown text) · One-page guide to React",
},
"metaProperties": {
"article:tag": [],
"og:description": "A React cheatsheet (Markdown text) · One-page guide to React",
"og:image": "https://assets.devhints.io/previews/react.jpg",
"og:image:height": "471",
"og:image:width": "900",
"og:site_name": "Devhints.io cheatsheets",
"og:title": "React cheatsheet",
"og:type": "article",
"og:url": "https://devhints.io/react",
"twitter:description": "A React cheatsheet (Markdown text) · One-page guide to React",
"twitter:image": "https://assets.devhints.io/previews/react.jpg",
"twitter:title": "React cheatsheet",
},
"title": "React cheatsheet",
}
`;
exports[`getSEOPropsForPage() > with description 1`] = `
{
"links": {
"canonical": "https://devhints.io/react",
},
"meta": {
"app:pageurl": "https://devhints.io/react",
"description": "A React cheatsheet · One-page guide to React",
},
"metaProperties": {
"article:tag": [],
"og:description": "A React cheatsheet · One-page guide to React",
"og:image": "https://assets.devhints.io/previews/react.jpg",
"og:image:height": "471",
"og:image:width": "900",
"og:site_name": "Devhints.io cheatsheets",
"og:title": "React cheatsheet",
"og:type": "article",
"og:url": "https://devhints.io/react",
"twitter:description": "A React cheatsheet · One-page guide to React",
"twitter:image": "https://assets.devhints.io/previews/react.jpg",
"twitter:title": "React cheatsheet",
},
"title": "React cheatsheet",
}
`;
exports[`getSEOPropsForPage() > with description, keywords 1`] = `
{
"links": {
"canonical": "https://devhints.io/react",
},
"meta": {
"app:pageurl": "https://devhints.io/react",
"description": "A React cheatsheet · One-page guide to React",
},
"metaProperties": {
"article:tag": [],
"og:description": "A React cheatsheet · One-page guide to React",
"og:image": "https://assets.devhints.io/previews/react.jpg",
"og:image:height": "471",
"og:image:width": "900",
"og:site_name": "Devhints.io cheatsheets",
"og:title": "React cheatsheet",
"og:type": "article",
"og:url": "https://devhints.io/react",
"twitter:description": "A React cheatsheet · One-page guide to React",
"twitter:image": "https://assets.devhints.io/previews/react.jpg",
"twitter:title": "React cheatsheet",
},
"title": "React cheatsheet",
}
`;
exports[`getSEOPropsForPage() > with keywords 1`] = `
{
"links": {
"canonical": "https://devhints.io/react",
},
"meta": {
"app:pageurl": "https://devhints.io/react",
"description": "hooks · components · props · One-page guide to React",
},
"metaProperties": {
"article:tag": [],
"og:description": "hooks · components · props · One-page guide to React",
"og:image": "https://assets.devhints.io/previews/react.jpg",
"og:image:height": "471",
"og:image:width": "900",
"og:site_name": "Devhints.io cheatsheets",
"og:title": "React cheatsheet",
"og:type": "article",
"og:url": "https://devhints.io/react",
"twitter:description": "hooks · components · props · One-page guide to React",
"twitter:image": "https://assets.devhints.io/previews/react.jpg",
"twitter:title": "React cheatsheet",
},
"title": "React cheatsheet",
}
`;

View File

@ -0,0 +1,53 @@
import { getJSONLDsForPage } from './jsonLd'
it('works', () => {
const page = {
slug: 'react',
frontmatter: {
title: 'React',
description: 'A React cheatsheet',
category: 'JavaScript'
}
}
const output = getJSONLDsForPage(page)
expect(output).toMatchInlineSnapshot(`
[
{
"@context": "http://schema.org",
"@type": "NewsArticle",
"description": "A React cheatsheet · One-page guide to React",
"headline": "React cheatsheet",
"image": [
"https://assets.devhints.io/previews/react.jpg",
],
"mainEntityOfPage": {
"@id": "https://google.com/article",
"@type": "WebPage",
},
},
{
"@context": "http://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"item": {
"@id": "https://devhints.io/#javascript",
"name": "JavaScript",
},
"position": 1,
},
{
"@type": "ListItem",
"item": {
"@id": "https://devhints.io/react",
"name": "React cheatsheet",
},
"position": 2,
},
],
},
]
`)
})

59
src/lib/seo/jsonLd.ts Normal file
View File

@ -0,0 +1,59 @@
import type { JsonLdDocument } from '~/types/JsonLdDocument'
import { site } from '../../config'
import { getDescription, getPageImage, getPageURL } from './seo'
export function getJSONLDsForPage(page: {
// TODO: rename getJsonLdSchemasForPage
slug: string
frontmatter: {
title?: string
description?: string
category?: string
}
}): Array<JsonLdDocument> {
const description = getDescription(page)
const image = getPageImage(page)
const url = getPageURL(page)
const category = page.frontmatter.category ?? 'Others'
const categoryAnchor = category.toLowerCase().replace(/ /g, '-')
const headline = page.frontmatter.title
? `${page.frontmatter.title} cheatsheet`
: ''
const newsArticle: JsonLdDocument = {
'@context': 'http://schema.org',
'@type': 'NewsArticle',
mainEntityOfPage: {
'@type': 'WebPage',
'@id': 'https://google.com/article'
},
headline,
image: [image],
description: description
}
const breadcrumb: JsonLdDocument = {
'@context': 'http://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
item: {
'@id': `${site.url}/#${categoryAnchor}`,
name: category
}
},
{
'@type': 'ListItem',
position: 2,
item: {
'@id': url,
name: headline
}
}
]
}
return [newsArticle, breadcrumb]
}

58
src/lib/seo/seo.test.ts Normal file
View File

@ -0,0 +1,58 @@
import { getSEOPropsForPage, toPlainText } from './seo'
const slug = 'react'
const title = 'React'
const description = 'A React cheatsheet'
const markdownDescription =
'A [React](https://react.dev) cheatsheet (*Markdown text*)'
const keywords = ['hooks', 'components', 'props']
describe('getSEOPropsForPage()', () => {
runTest({
title: 'title only',
input: { slug, frontmatter: { title } }
})
runTest({
title: 'with description',
input: { slug, frontmatter: { title, description } }
})
runTest({
title: 'with Markdown description',
input: { slug, frontmatter: { title, description: markdownDescription } }
})
runTest({
title: 'with description, keywords',
input: { slug, frontmatter: { title, description, keywords } }
})
runTest({
title: 'with keywords',
input: { slug, frontmatter: { title, keywords } }
})
function runTest({ title, input }: { title: string; input: any }) {
test(title, () => {
const output = getSEOPropsForPage(input)
expect(output).toMatchSnapshot()
})
}
})
describe('toPlainText()', () => {
test('converts markdown to plain text', () => {
const input = 'This is a **bold** text'
const output = toPlainText(input)
expect(output).toEqual('This is a bold text')
})
test('removes HTML tags', () => {
const input = '<p>This is a <strong>bold</strong> text</p>'
const output = toPlainText(input)
expect(output).toEqual('This is a bold text')
})
test('handles empty input', () => {
const input = ''
const output = toPlainText(input)
expect(output).toEqual('')
})
})

151
src/lib/seo/seo.ts Normal file
View File

@ -0,0 +1,151 @@
import snarkdown from 'snarkdown'
import { site } from '../../config'
import type { SheetPage } from '../page'
export function getSEOPropsForHome() {
const t = {
title: 'Devhints — TL;DR for developer documentation',
description: 'A ridiculous collection of web development cheatsheets'
}
const url = site.url
const image = 'https://assets.devhints.io/previews/index.jpg'
return {
title: t.title,
links: {
canonical: url
},
meta: {
description: t.description,
'app:pageurl': url
},
metaProperties: denull({
'og:description': t.description,
'og:image:height': '471',
'og:image': image,
'og:image:width': '900',
'og:site_name': site.title,
'og:title': t.title,
'og:type': 'website',
'og:url': url,
// BUG: twitter card props should be metaNames
'twitter:title': image,
'twitter:image': image,
'twitter:description': t.description
})
}
}
export function getSEOPropsForPage(
page: Pick<SheetPage, 'slug' | 'frontmatter'>
) {
const title = `${page.frontmatter.title} cheatsheet`
const description = getDescription(page)
const image = getPageImage(page)
const url = getPageURL(page)
return {
title: title,
links: {
canonical: url
},
meta: {
description,
'app:pageurl': url
},
metaProperties: denull({
'og:description': description,
'og:image:height': '471',
'og:image': image,
'og:image:width': '900',
'og:site_name': site.title,
'og:title': title,
'og:type': 'article',
'og:url': url,
'article:tag': page.frontmatter.tags ?? [],
'article:section': page.frontmatter.category,
// BUG: twitter card props should be metaNames
'twitter:title': title,
'twitter:image': image,
'twitter:description': description
})
}
}
/**
* Return a description for a page
*/
export function getDescription(page: {
frontmatter: {
title?: string
intro?: string
description?: string
keywords?: string[]
}
}) {
const t = {
withDescriptionAndIntro: `{description} {intro}`,
withDescription: `{description} · One-page guide to {title}`,
withKeywordsAndIntro: `{keywords} · {intro}`,
withKeywords: `{keywords} · One-page guide to {title}`,
withIntro: `One-page guide to {title}: usage, examples, and more. {intro}`,
default: `The one-page guide to {title}: usage, examples, links, snippets, and more.`
}
let fmt: string = t.default
if (page.frontmatter.description && page.frontmatter.intro) {
fmt = t.withDescriptionAndIntro
} else if (page.frontmatter.description) {
fmt = t.withDescription
} else if (page.frontmatter.keywords && page.frontmatter.intro) {
fmt = t.withKeywordsAndIntro
} else if (page.frontmatter.keywords) {
fmt = t.withKeywords
} else if (page.frontmatter.intro) {
fmt = t.withIntro
} else {
fmt = t.default
}
return fmt
.replace('{title}', () => page.frontmatter.title ?? '')
.replace('{keywords}', () => (page.frontmatter.keywords ?? []).join(' · '))
.replace('{intro}', () => toPlainText(page.frontmatter.intro ?? ''))
.replace('{description}', () =>
toPlainText(page.frontmatter.description ?? '')
)
}
/**
* Remove falsy values from an object
*/
function denull(
record: Record<string, string | string[] | null | undefined>
): Record<string, string | string[]> {
return Object.fromEntries(
Object.entries(record).filter((entry): entry is [string, string] =>
Boolean(entry[1])
)
)
}
export function getPageImage({ slug }: { slug: string }) {
return `https://assets.devhints.io/previews/${slug}.jpg`
}
export function getPageURL({ slug }: { slug: string }) {
return new URL(`/${slug}`, site.url).toString()
}
/**
* Convert Markdown to plain text.
*/
export function toPlainText(input: string) {
const html = snarkdown(input)
const plainText = html.replace(/<[^>]*>/g, '').replace(/\n/g, ' ')
return plainText
}

33
src/pages/404.astro Normal file
View File

@ -0,0 +1,33 @@
---
import BaseLayout from '~/components/BaseLayout.astro'
import TopNav from '~/components/TopNav.astro'
import SearchForm from '~/components/V2017Sheet/SearchForm.astro'
const t = {
title: 'Not found',
description: "Sorry, we don't have a cheatsheet for this yet. Try searching!",
goHome: 'Back to home'
}
---
<BaseLayout title="Not found">
<TopNav noEdit noBack />
<div class="body-area -slim">
<div class="site-header">
<h1>{t.title}</h1>
<p>{t.description}</p>
<SearchForm isLive />
<p class="action">
<a class="push-button" href="/">{t.goHome}</a>
</p>
</div>
</div>
</BaseLayout>
<style lang="scss" is:global>
@import '../sass/2017/utils';
@import '../sass/2017/placeholders/push-button';
@import '../sass/2017/components/body-area';
@import '../sass/2017/components/push-button';
@import '../sass/2017/components/site-header';
</style>

23
src/pages/[...slug].astro Normal file
View File

@ -0,0 +1,23 @@
---
import V2017Sheet from '~/components/V2017Sheet.astro'
import { getPages } from '~/lib/page'
import type { SheetPage } from '~/lib/page'
export type Props = {
page: SheetPage
}
const props = Astro.props as Props
export async function getStaticPaths() {
const pages = await getPages()
return Promise.all(
Object.values(pages).map((page) => {
return { props: { page }, params: { slug: page.slug } }
})
)
}
---
<V2017Sheet page={props.page} />

View File

@ -0,0 +1,14 @@
import { GET } from '../searchindex.json'
test('has data', async () => {
const response = await GET()
const data = await response.json()
expect(typeof data.index).toEqual('object')
expect(Array.isArray(data.index.records)).toBeTruthy()
expect(Array.isArray(data.rows)).toBeTruthy()
for (const row of data.rows) {
expect(typeof row.title).toBeTruthy() // some titles are numbers?
expect(typeof row.slug).toEqual('string')
}
})

View File

@ -0,0 +1,21 @@
import { GET } from '../sitemap.xml'
let lines: string[]
beforeEach(async () => {
if (lines) return
const response = await GET()
const body = await response.text()
lines = body.split('\n')
})
test('has data', async () => {
expect(lines).toContain('<url><loc>https://devhints.io/react</loc></url>')
expect(lines).toContain('<url><loc>https://devhints.io/bash</loc></url>')
})
test('skip unlisted sheets', async () => {
expect(lines).not.toContain(
'<url><loc>https://devhints.io/tests/basic</loc></url>'
)
})

102
src/pages/index.astro Normal file
View File

@ -0,0 +1,102 @@
---
import { getPages } from '~/lib/page'
import { getSEOPropsForHome } from '~/lib/seo/seo'
import {
getPagesByCategory,
getFeaturedPages,
getRecentPages
} from '~/lib/page/queries'
import TopNav from '~/components/TopNav.astro'
import BaseLayout from '~/components/BaseLayout.astro'
import PageListItem from '~/components/V2017Home/PageListItem.astro'
import FeaturedPages from '~/components/V2017Home/FeaturedPages.astro'
import Announcements from '~/components/V2017Home/Announcements.astro'
import SearchForm from '~/components/V2017Sheet/SearchForm.astro'
import CarbonBox from '~/components/V2017/CarbonBox.astro'
import SEO from '~/components/SEO/SEO.astro'
import { urls } from '~/config'
const pages = await getPages()
const pageCategories = getPagesByCategory(pages)
const featuredPages = getFeaturedPages(pages, { maxCount: 8 })
const recentPages = getRecentPages(pages, { maxCount: 18 })
const seoProps = getSEOPropsForHome()
const t = {
title: "Rico's cheatsheets",
tagline: `Hey! I'm <a href='https://ricostacruz.com'>@rstacruz</a> and this is a modest collection of cheatsheets I've written.`,
recentlyUpdated: 'Recently updated',
seeSomethingMissing: 'See something missing?',
requestCheatsheet: 'Request cheatsheet'
}
---
<BaseLayout>
<Fragment slot="head">
<SEO {...seoProps} />
</Fragment>
<TopNav noEdit noBack />
<div class="body-area -slim">
<div class="site-header" role="banner">
<h1>{t.title}</h1>
<p set:html={t.tagline} />
{/* Search */}
<SearchForm isLive />
{/* Publicite */}
<div class="pubbox"><CarbonBox /></div>
{/* TODO: announcement */}
</div>
<div class="pages-list" role="main">
{/* Featured pages */}
<FeaturedPages pages={featuredPages} />
{/* Recent pages */}
<h2 class="category item" data-js-searchable-header>
<span>{t.recentlyUpdated}</span>
</h2>
{recentPages.map((page) => <PageListItem page={page} />)}
{
Object.values(pageCategories).map((category) => {
if (category.pages.length === 0) return
return (
<>
<h2 class="category item" data-js-searchable-header>
<span>{category.title}</span>
</h2>
{category.pages.map((page) => (
<PageListItem page={page} />
))}
</>
)
})
}
<div class="message item missing-message">
<h3>{t.seeSomethingMissing}</h3>
<p>
<a class="push-button" href={urls.newCheatsheetUrl}
>{t.requestCheatsheet}</a
>
</p>
</div>
</div>
</div>
<Announcements />
</BaseLayout>
<style lang="scss" is:global>
@import '../sass/2017/utils';
@import '../sass/2017/components/body-area';
@import '../sass/2017/components/missing-message';
@import '../sass/2017/components/pages-list';
@import '../sass/2017/components/push-button';
@import '../sass/2017/components/notice-box';
@import '../sass/2017/components/site-header';
</style>

View File

@ -0,0 +1,16 @@
import { buildFuseIndex } from '~/lib/fuseSearch/fuseSearch'
import { getPages } from '~/lib/page'
/*
* Returns a search index that can be hydrated later
*/
export async function GET() {
const result = buildFuseIndex(await getPages())
return new Response(JSON.stringify(result), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
}

26
src/pages/sitemap.xml.ts Normal file
View File

@ -0,0 +1,26 @@
import { site } from '~/config'
import { getPages } from '~/lib/page'
import { isListed } from '~/lib/page/accessors'
export async function GET() {
const lines = [
`<?xml version="1.0" encoding="UTF-8"?>`,
`<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`
]
const visiblePages = Object.values(await getPages()).filter(isListed)
for (const page of visiblePages) {
const url = `${site.url}/${page.slug}`
lines.push(`<url><loc>${url}</loc></url>`)
}
lines.push(`</urlset>`)
const data = lines.join('\n') + '\n'
return new Response(data, {
status: 200,
headers: { 'Content-Type': 'application/xml' }
})
}

View File

@ -0,0 +1,5 @@
---
import SEO from '~/components/SEO/SEO.astro'
---
<SEO metaProperties={{ 'article:tags': ['WIP', 'Featured'] }} />

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
# Caches Kramdown results in .cache/
# Normally, Kramdown is invoked as needed (kramdown.rb), but this is
# going to be slow down builds significantly. By doing all Kramdown
# rendering beforehand, this cuts down the time from 70s to 10s.
require 'fileutils'
require 'digest'
require_relative 'renderer'
# This seems to default to US-ASCII in some environments, which causes
# errors in some sheets
Encoding.default_external = Encoding::UTF_8
def remove_frontmatter(input_string)
input_string.sub(/\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m, '')
end
FileUtils.mkdir_p '.cache'
ARGV.each do |filepath|
input = File.read(filepath)
input = remove_frontmatter(input)
digest = Digest::SHA2.hexdigest(input.strip)
outfile = ".cache/#{digest}.html"
output = Renderer.render(input: input)
File.write(outfile, output)
end

4
src/ruby/kramdown.rb Normal file
View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
require_relative 'renderer'
puts Renderer.render(input: $stdin.read)

42
src/ruby/renderer.rb Normal file
View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
# Renders Markdown to HTML, emulating Jekyll 2 quirks.
module Renderer
module_function
KRAMDOWN_OPTIONS = {
input: 'GFM',
hard_wrap: false
}.freeze
def render(input:)
require 'kramdown'
new_input = input.to_s
.gsub(/^{% ?raw ?%}\n/, '') # raw on its own line
.gsub(/{% ?raw ?%}/, '') # inline in another line
.gsub(/{% include (common\/[^ ]+) title="([^"]+)" %}/) {
# a reduced subset of Jekyll includes
title = $2
file = $1
filepath = Pathname.new("_includes/#{file}").cleanpath.to_s
if filepath.start_with?("/") || filepath.start_with?(".")
raise Errno::ENOENT, "Invalid include - #{filepath}"
end
if !File.exist?(filepath)
raise Errno::ENOENT, "Cannot find include - #{filepath}"
end
data = File.read(filepath)
data = data.gsub(/{{ include.title }}/m, title)
data
}
.gsub(/^{% ?endraw ?%}\n/, '')
.gsub(/{% ?endraw ?%}/, '')
.gsub(/ {%- if 0 -%}{%- endif -%} /, '') # Used in jinja.md
.gsub(/{%- raw -%}\n?/, '') # Used in jinja.md
Kramdown::Document.new(new_input, KRAMDOWN_OPTIONS).to_html
end
end

71
src/ruby/renderer.test.rb Normal file
View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
require 'minitest/autorun'
require_relative 'renderer'
def run_test(input:, expected:, label:)
label ||= (input[0...20]).to_s
it(label) do
output = Renderer.render(input: input)
assert_equal(output.strip, expected.strip)
end
end
describe 'Renderer' do
it 'with react.md' do
input = File.read('./react.md').split("---")[2].strip
output = Renderer.render(input: input)
refute_includes(output, %(%raw%))
refute_includes(output, %(% raw %))
end
it 'With "{% include %}" tags' do
input = ['{% include common/moment_format.md title="Moment" %}', 'This is some text'].join("\n")
output = Renderer.render(input: input)
assert_includes(output, %(<h2 class="-three-column" id="moment">Moment</h2>))
assert_includes(output, %(<p>This is some text</p>))
end
run_test(
label: 'Basic test',
input: 'hola mundo',
expected: '<p>hola mundo</p>'
)
run_test(
label: 'H1',
input: '# hola mundo',
expected: %(<h1 id="hola-mundo">hola mundo</h1>)
)
run_test(
label: 'H1 with class',
input: "# hola mundo\n{: .heading}",
expected: %(<h1 class="heading" id="hola-mundo">hola mundo</h1>)
)
run_test(
label: 'With "{% raw %}" tags',
input: ['{% raw %}', 'This is some text', '{% endraw %}'].join("\n"),
expected: %(<p>This is some text</p>)
)
run_test(
label: 'With "{%raw%}" tags',
input: ['{%raw%}', 'This is some text', '{%endraw%}'].join("\n"),
expected: %(<p>This is some text</p>)
)
run_test(
label: 'With "{% raw %}" tags with new line',
input: ['{% raw %}', 'This is some text', '{% endraw %}', 'next line'].join("\n"),
expected: %(<p>This is some text\nnext line</p>)
)
run_test(
label: 'With "{% raw %}" tags with 2 new lines',
input: ['{% raw %}', 'This is some text', '{% endraw %}', '', 'next paragraph'].join("\n"),
expected: %(<p>This is some text</p>\n\n<p>next paragraph</p>)
)
end

11
src/sass/2017/_utils.scss Normal file
View File

@ -0,0 +1,11 @@
// Vendor
@import './variables';
@import '../vendor/ionicons-inline/ionicons';
@import '../vendor/modularscale/modularscale';
// Utilities
@import './utils/font-size';
@import './utils/gutter';
@import './utils/heading-style';
@import './utils/section-gutter';
@import './utils/section-with-container';

View File

@ -2,7 +2,8 @@
* Base
*/
html, body {
html,
body {
background: $base-body;
font-family: $body-font;
font-size: 14px;
@ -21,7 +22,8 @@ body {
* Code
*/
pre, code {
pre,
code {
font-family: $monospace-font;
letter-spacing: -0.03em;
}

View File

@ -7,7 +7,9 @@
background: white;
padding-right: 48px;
animation: announcements-item-flyin 500ms ease-out;
transition: opacity 500ms linear, transform 500ms ease-out;
transition:
opacity 500ms linear,
transform 500ms ease-out;
}
&.-hide {
@ -53,7 +55,7 @@
& > .close::before {
// https://stackoverflow.com/a/30421654
content: unquote("\"")+str-insert("00D7", "\\", 1)+unquote("\"");
content: unquote('"') + str-insert('00D7', '\\', 1) + unquote('"');
font-size: 14px;
}
}

View File

@ -0,0 +1,26 @@
// Overrides for npm:autocompleter
.autocomplete {
border: none;
padding: 0.5rem;
padding-top: 0;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
box-shadow: $shadow3;
transform: translate(-8px, -2px);
}
.autocomplete > div {
padding: 0.5rem;
border-radius: 4px;
}
.autocomplete > div:hover:not(.group) {
background: $base-body;
}
.autocomplete > div + div:not(.selected) {
box-shadow: inset 0 1px $line-color;
}
.autocomplete > div.selected,
.autocomplete > div.selected:hover {
background: $base-c;
color: #fff;
}

View File

@ -9,6 +9,7 @@
@include section-gutter(margin-right, $multiplier: -1);
margin-top: 0;
margin-bottom: 0;
column-gap: 0;
}
// Clearfix
@ -22,8 +23,8 @@
// Each section
& > .h3-section {
@include section-gutter(padding);
float: left;
width: 100%;
break-inside: avoid;
}
@media (min-width: 769px) {
@ -40,9 +41,7 @@
.h3-section-list,
.h3-section-list.-two-column {
@media (min-width: 769px) {
& > .h3-section {
width: 50%;
}
columns: 2;
}
}
@ -51,8 +50,8 @@
*/
.h3-section-list.-one-column {
& > .h3-section {
width: 100%;
@media (min-width: 769px) {
columns: 1;
}
& > .h3-section + .h3-section {
@ -66,36 +65,20 @@
.h3-section-list.-three-column {
@media (min-width: 769px) {
& > .h3-section {
width: 50%;
}
columns: 2;
}
@media (min-width: 961px) {
& > .h3-section {
width: 33.33%;
}
columns: 3;
}
}
/*
* Three column, left reference
*/
.h3-section-list.-left-reference {
@media (min-width: 769px) {
& > .h3-section {
width: 50%;
}
columns: 3;
}
@media (min-width: 961px) {
& > .h3-section {
width: 66.67%;
}
& > .h3-section:first-child {
width: 33.33%;
}
& > .h3-section + .h3-section {
width: 200%;
}
}

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