Compare commits
7 Commits
8ed5db5946
...
ada0463e75
Author | SHA1 | Date |
---|---|---|
Shadowfacts | ada0463e75 | |
Shadowfacts | 3fbd07aef9 | |
Shadowfacts | daa76bfba9 | |
Shadowfacts | 2edff0141e | |
Shadowfacts | e5f97c3a5e | |
Shadowfacts | afb844db87 | |
Shadowfacts | 04c43fa483 |
|
@ -0,0 +1,13 @@
|
||||||
|
import * as metadata from "../metadata";
|
||||||
|
import { Page } from "../metadata";
|
||||||
|
import * as util from "../util";
|
||||||
|
import layout from "../layout";
|
||||||
|
|
||||||
|
export default async function archive(posts: Page[]) {
|
||||||
|
const page = await metadata.get("site/archive.html.ejs");
|
||||||
|
page.text = util.render(page.text, {
|
||||||
|
posts,
|
||||||
|
}, "site/archive.html.ejs");
|
||||||
|
page.text = await layout(page.text, page.metadata, page.metadata.layout!);
|
||||||
|
util.write("archive/index.html", page.text);
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
import { Page, PostMetadata } from "../metadata";
|
|
||||||
import generatePaginated from "./paginated";
|
|
||||||
|
|
||||||
export default async function(posts: Page[]): Promise<Map<string, Page[]>> {
|
|
||||||
const categories = new Map<string, Page[]>();
|
|
||||||
|
|
||||||
for (const post of posts) {
|
|
||||||
const category = (<PostMetadata>post.metadata).category;
|
|
||||||
if (!categories.has(category)) {
|
|
||||||
categories.set(category, []);
|
|
||||||
}
|
|
||||||
categories.get(category)!.push(post);
|
|
||||||
}
|
|
||||||
|
|
||||||
categories.forEach((categoryPosts, category) => {
|
|
||||||
generatePaginated(categoryPosts, `/${category}/`, "site/category.html.ejs", {
|
|
||||||
category
|
|
||||||
}, {
|
|
||||||
title: `${category} posts`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return categories;
|
|
||||||
}
|
|
|
@ -1,21 +1,23 @@
|
||||||
import categories from "./categories";
|
import archive from "./archive";
|
||||||
import copy from "./copy";
|
import copy from "./copy";
|
||||||
import css from "./css";
|
import css from "./css";
|
||||||
import errors from "./errors";
|
import errors from "./errors";
|
||||||
import homepage from "./homepage";
|
import homepage from "./homepage";
|
||||||
import posts from "./posts";
|
import posts from "./posts";
|
||||||
import * as rss from "./rss";
|
import * as rss from "./rss";
|
||||||
|
import tags from "./tags";
|
||||||
import tutorials from "./tutorials";
|
import tutorials from "./tutorials";
|
||||||
import years from "./years";
|
import years from "./years";
|
||||||
|
|
||||||
export = {
|
export = {
|
||||||
categories,
|
archive,
|
||||||
copy,
|
copy,
|
||||||
css,
|
css,
|
||||||
errors,
|
errors,
|
||||||
homepage,
|
homepage,
|
||||||
posts,
|
posts,
|
||||||
rss,
|
rss,
|
||||||
|
tags,
|
||||||
tutorials,
|
tutorials,
|
||||||
years,
|
years,
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Page, PostMetadata } from "../metadata";
|
||||||
import * as util from "../util";
|
import * as util from "../util";
|
||||||
import { TutorialSeries } from "./tutorials";
|
import { TutorialSeries } from "./tutorials";
|
||||||
|
|
||||||
async function generateFeed(posts: Page[], permalink: string, category?: string) {
|
async function generateFeed(posts: Page[], permalink: string, tag?: string) {
|
||||||
posts = posts.sort((a, b) => {
|
posts = posts.sort((a, b) => {
|
||||||
const aDate = <Date>(<PostMetadata>a.metadata).date;
|
const aDate = <Date>(<PostMetadata>a.metadata).date;
|
||||||
const bDate = <Date>(<PostMetadata>b.metadata).date;
|
const bDate = <Date>(<PostMetadata>b.metadata).date;
|
||||||
|
@ -16,7 +16,7 @@ async function generateFeed(posts: Page[], permalink: string, category?: string)
|
||||||
let text = (await fs.readFile("site/feed.xml.ejs")).toString();
|
let text = (await fs.readFile("site/feed.xml.ejs")).toString();
|
||||||
text = util.render(text, {
|
text = util.render(text, {
|
||||||
posts,
|
posts,
|
||||||
category,
|
tag,
|
||||||
permalink,
|
permalink,
|
||||||
feedPath: dest
|
feedPath: dest
|
||||||
}, "site/feed.xml.ejs");
|
}, "site/feed.xml.ejs");
|
||||||
|
@ -28,9 +28,9 @@ export async function posts(posts: Page[]) {
|
||||||
generateFeed(posts, "/");
|
generateFeed(posts, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function categories(categories: Map<string, Page[]>) {
|
export async function tags(tags: Map<string, Page[]>) {
|
||||||
categories.forEach((posts, category) => {
|
tags.forEach((posts, tag) => {
|
||||||
generateFeed(posts, `/${category}/`, category);
|
generateFeed(posts, `/${tag}/`, tag);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Page, PostMetadata } from "../metadata";
|
||||||
|
import generatePaginated from "./paginated";
|
||||||
|
|
||||||
|
export default async function(posts: Page[]): Promise<Map<string, Page[]>> {
|
||||||
|
const taggedPosts = new Map<string, Page[]>();
|
||||||
|
|
||||||
|
for (const post of posts) {
|
||||||
|
const tags = (<PostMetadata>post.metadata).tags;
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (!taggedPosts.has(tag)) {
|
||||||
|
taggedPosts.set(tag, []);
|
||||||
|
}
|
||||||
|
taggedPosts.get(tag)!.push(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
taggedPosts.forEach((tagPosts, tag) => {
|
||||||
|
generatePaginated(tagPosts, `/${tag}/`, "site/tag.html.ejs", {
|
||||||
|
tag
|
||||||
|
}, {
|
||||||
|
title: `${tag} posts`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return taggedPosts;
|
||||||
|
}
|
|
@ -22,7 +22,8 @@ async function generate(): Promise<Page[]> {
|
||||||
generators.homepage(posts);
|
generators.homepage(posts);
|
||||||
generators.years(posts);
|
generators.years(posts);
|
||||||
generators.rss.posts(posts)
|
generators.rss.posts(posts)
|
||||||
generators.categories(posts).then(generators.rss.categories);
|
generators.tags(posts).then(generators.rss.tags);
|
||||||
|
generators.archive(posts);
|
||||||
|
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export interface Metadata {
|
||||||
export interface PostMetadata extends Metadata {
|
export interface PostMetadata extends Metadata {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
category: string;
|
tags: string[];
|
||||||
date: string | Date;
|
date: string | Date;
|
||||||
readingTime?: number;
|
readingTime?: number;
|
||||||
wordCount?: number;
|
wordCount?: number;
|
||||||
|
|
|
@ -71,7 +71,6 @@ export function video(metadata: Metadata, name: string, attributes: object): str
|
||||||
return `
|
return `
|
||||||
<video ${attributesStr}>
|
<video ${attributesStr}>
|
||||||
<source src="${metadata.permalink}/${name}.mp4" type="video/mp4">
|
<source src="${metadata.permalink}/${name}.mp4" type="video/mp4">
|
||||||
<source src="${metadata.permalink}/${name}.webm" type="video/webm">
|
|
||||||
</video>
|
</video>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
```
|
||||||
|
metadata.layout = "default.html.ejs"
|
||||||
|
metadata.title = "Archive"
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<% let currentYear %>
|
||||||
|
<% for (const post of posts) { %>
|
||||||
|
<% if (post.metadata.date.getFullYear() !== currentYear) { %>
|
||||||
|
<% if (currentYear) { %>
|
||||||
|
</ul>
|
||||||
|
<% } %>
|
||||||
|
<% currentYear = post.metadata.date.getFullYear() %>
|
||||||
|
<h2><%= currentYear %></h2>
|
||||||
|
<ul>
|
||||||
|
<% } %>
|
||||||
|
<li>
|
||||||
|
<a href="<%= post.metadata.permalink %>">
|
||||||
|
<%= post.metadata.title %>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
|
@ -7,6 +7,9 @@
|
||||||
--secondary-ui-text-color: var(--dark-secondary-ui-text-color);
|
--secondary-ui-text-color: var(--dark-secondary-ui-text-color);
|
||||||
--content-text-color: var(--dark-content-text-color);
|
--content-text-color: var(--dark-content-text-color);
|
||||||
|
|
||||||
|
--aside-background: var(--dark-aside-background);
|
||||||
|
--aside-border: var(--dark-aside-border);
|
||||||
|
|
||||||
// Syntax highdarking
|
// Syntax highdarking
|
||||||
--atom-base: var(--dark-atom-base);
|
--atom-base: var(--dark-atom-base);
|
||||||
--atom-mono-1: var(--dark-atom-mono-1);
|
--atom-mono-1: var(--dark-atom-mono-1);
|
||||||
|
@ -31,6 +34,9 @@
|
||||||
--secondary-ui-text-color: var(--light-secondary-ui-text-color);
|
--secondary-ui-text-color: var(--light-secondary-ui-text-color);
|
||||||
--content-text-color: var(--light-content-text-color);
|
--content-text-color: var(--light-content-text-color);
|
||||||
|
|
||||||
|
--aside-background: var(--light-aside-background);
|
||||||
|
--aside-border: var(--light-aside-border);
|
||||||
|
|
||||||
// Syntax highlighting
|
// Syntax highlighting
|
||||||
--atom-base: var(--light-atom-base);
|
--atom-base: var(--light-atom-base);
|
||||||
--atom-mono-1: var(--light-atom-mono-1);
|
--atom-mono-1: var(--light-atom-mono-1);
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
--secondary-ui-text-color: var(--light-secondary-ui-text-color);
|
--secondary-ui-text-color: var(--light-secondary-ui-text-color);
|
||||||
--content-text-color: var(--light-content-text-color);
|
--content-text-color: var(--light-content-text-color);
|
||||||
|
|
||||||
|
--aside-background: var(--light-aside-background);
|
||||||
|
--aside-border: var(--light-aside-border);
|
||||||
|
|
||||||
// Syntax highlighting
|
// Syntax highlighting
|
||||||
--atom-base: var(--light-atom-base);
|
--atom-base: var(--light-atom-base);
|
||||||
--atom-mono-1: var(--light-atom-mono-1);
|
--atom-mono-1: var(--light-atom-mono-1);
|
||||||
|
@ -31,6 +34,9 @@
|
||||||
--secondary-ui-text-color: var(--dark-secondary-ui-text-color);
|
--secondary-ui-text-color: var(--dark-secondary-ui-text-color);
|
||||||
--content-text-color: var(--dark-content-text-color);
|
--content-text-color: var(--dark-content-text-color);
|
||||||
|
|
||||||
|
--aside-background: var(--dark-aside-background);
|
||||||
|
--aside-border: var(--dark-aside-border);
|
||||||
|
|
||||||
// Syntax highdarking
|
// Syntax highdarking
|
||||||
--atom-base: var(--dark-atom-base);
|
--atom-base: var(--dark-atom-base);
|
||||||
--atom-mono-1: var(--dark-atom-mono-1);
|
--atom-mono-1: var(--dark-atom-mono-1);
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
<%- include("normalize.css") %>
|
<%- include("normalize.css") %>
|
||||||
<%- include("syntax-highlighting.css") %>
|
<%- include("syntax-highlighting.css") %>
|
||||||
|
|
||||||
|
$light-accent-color: #0638d0;
|
||||||
|
$dark-accent-color: #f9c72f;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--light-accent-color: #0638d0;
|
// Theme colors
|
||||||
--dark-accent-color: #f9c72f;
|
--light-accent-color: #{$light-accent-color};
|
||||||
|
--dark-accent-color: #{$dark-accent-color};
|
||||||
--light-content-background-color: white;
|
--light-content-background-color: white;
|
||||||
--dark-content-background-color: #111;
|
--dark-content-background-color: #111;
|
||||||
--light-shadow-color: #f7f7f7;
|
--light-shadow-color: #f7f7f7;
|
||||||
|
@ -17,6 +21,11 @@
|
||||||
--light-content-text-color: #222;
|
--light-content-text-color: #222;
|
||||||
--dark-content-text-color: #ddd;
|
--dark-content-text-color: #ddd;
|
||||||
|
|
||||||
|
--light-aside-background: #{lighten($light-accent-color, 50%)};
|
||||||
|
--dark-aside-background: #{darken($dark-accent-color, 50%)};
|
||||||
|
--light-aside-border: #{darken($light-accent-color, 10%)};
|
||||||
|
--dark-aside-border: #{darken($dark-accent-color, 10%)};
|
||||||
|
|
||||||
// Syntax highlighting
|
// Syntax highlighting
|
||||||
--light-atom-base: #fafafa;
|
--light-atom-base: #fafafa;
|
||||||
--dark-atom-base: #282c34;
|
--dark-atom-base: #282c34;
|
||||||
|
@ -42,6 +51,11 @@
|
||||||
--dark-atom-hue-6: #d19a66;
|
--dark-atom-hue-6: #d19a66;
|
||||||
--light-atom-hue-6-2: #c18401;
|
--light-atom-hue-6-2: #c18401;
|
||||||
--dark-atom-hue-6-2: #e6c07b;
|
--dark-atom-hue-6-2: #e6c07b;
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
--ui-font: Avenir, Lucida Grande, Arial, sans-serif;
|
||||||
|
--content-font: Charter, Georgia, serif;
|
||||||
|
--monospace-font: SF Mono, monospace;
|
||||||
}
|
}
|
||||||
.theme-light {
|
.theme-light {
|
||||||
--accent-color: var(--light-accent-color);
|
--accent-color: var(--light-accent-color);
|
||||||
|
@ -52,6 +66,9 @@
|
||||||
--secondary-ui-text-color: var(--light-secondary-ui-text-color);
|
--secondary-ui-text-color: var(--light-secondary-ui-text-color);
|
||||||
--content-text-color: var(--light-content-text-color);
|
--content-text-color: var(--light-content-text-color);
|
||||||
|
|
||||||
|
--aside-background: var(--light-aside-background);
|
||||||
|
--aside-border: var(--light-aside-border);
|
||||||
|
|
||||||
// Syntax highlighting
|
// Syntax highlighting
|
||||||
--atom-base: var(--light-atom-base);
|
--atom-base: var(--light-atom-base);
|
||||||
--atom-mono-1: var(--light-atom-mono-1);
|
--atom-mono-1: var(--light-atom-mono-1);
|
||||||
|
@ -75,6 +92,9 @@
|
||||||
--secondary-ui-text-color: var(--dark-secondary-ui-text-color);
|
--secondary-ui-text-color: var(--dark-secondary-ui-text-color);
|
||||||
--content-text-color: var(--dark-content-text-color);
|
--content-text-color: var(--dark-content-text-color);
|
||||||
|
|
||||||
|
--aside-background: var(--dark-aside-background);
|
||||||
|
--aside-border: var(--dark-aside-border);
|
||||||
|
|
||||||
// Syntax highdarking
|
// Syntax highdarking
|
||||||
--atom-base: var(--dark-atom-base);
|
--atom-base: var(--dark-atom-base);
|
||||||
--atom-mono-1: var(--dark-atom-mono-1);
|
--atom-mono-1: var(--dark-atom-mono-1);
|
||||||
|
@ -90,15 +110,10 @@
|
||||||
--atom-hue-6-2: var(--dark-atom-hue-6-2);
|
--atom-hue-6-2: var(--dark-atom-hue-6-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonts
|
|
||||||
$sansSerif: Avenir, Lucida Grande, Arial, sans-serif;
|
|
||||||
$serif: Charter, Georgia, serif;
|
|
||||||
$monospace: SF Mono, monospace;
|
|
||||||
|
|
||||||
// General
|
// General
|
||||||
html {
|
html {
|
||||||
background-color: var(--content-background-color);
|
background-color: var(--content-background-color);
|
||||||
font-family: $sansSerif;
|
font-family: var(--ui-font);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--ui-text-color);
|
color: var(--ui-text-color);
|
||||||
|
@ -145,7 +160,7 @@ article {
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "#";
|
content: "#";
|
||||||
font-family: $monospace;
|
font-family: var(--monospace-font);
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
@ -175,15 +190,16 @@ article {
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-content {
|
.article-content {
|
||||||
font-family: $serif;
|
position: relative;
|
||||||
|
font-family: var(--content-font);
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: $sansSerif;
|
font-family: var(--ui-font);
|
||||||
|
|
||||||
.header-anchor {
|
.header-anchor {
|
||||||
font-family: $monospace;
|
font-family: var(--monospace-font);
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -208,6 +224,22 @@ article {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
background-color: var(--aside-background);
|
||||||
|
border: 1px solid var(--aside-border);
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 50%;
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
margin-left: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
p:first-child { margin-top: 0; }
|
||||||
|
p:last-child { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
// Markdown decorations
|
// Markdown decorations
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
a {
|
a {
|
||||||
|
@ -216,7 +248,7 @@ article {
|
||||||
&::after { content: "](" attr(data-link) ")"; word-wrap: break-word; }
|
&::after { content: "](" attr(data-link) ")"; word-wrap: break-word; }
|
||||||
&::before, &::after {
|
&::before, &::after {
|
||||||
color: var(--secondary-ui-text-color);
|
color: var(--secondary-ui-text-color);
|
||||||
font-family: $monospace;
|
font-family: var(--monospace-font);
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,7 +265,7 @@ article {
|
||||||
}
|
}
|
||||||
code::before, code::after {
|
code::before, code::after {
|
||||||
content: "`";
|
content: "`";
|
||||||
font-family: $monospace;
|
font-family: var(--monospace-font);
|
||||||
color: var(--secondary-ui-text-color);
|
color: var(--secondary-ui-text-color);
|
||||||
}
|
}
|
||||||
pre code::before,
|
pre code::before,
|
||||||
|
@ -244,12 +276,12 @@ article {
|
||||||
}
|
}
|
||||||
strong::before, strong::after {
|
strong::before, strong::after {
|
||||||
content: "**";
|
content: "**";
|
||||||
font-family: $monospace;
|
font-family: var(--monospace-font);
|
||||||
color: var(--secondary-ui-text-color);
|
color: var(--secondary-ui-text-color);
|
||||||
}
|
}
|
||||||
em::before, em::after {
|
em::before, em::after {
|
||||||
content: "_";
|
content: "_";
|
||||||
font-family: $monospace;
|
font-family: var(--monospace-font);
|
||||||
color: var(--secondary-ui-text-color);
|
color: var(--secondary-ui-text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -394,7 +426,7 @@ a {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -0.15em;
|
top: -0.15em;
|
||||||
transition: 0.3s ease all;
|
transition: 0.3s ease all;
|
||||||
font-family: $monospace;
|
font-family: var(--monospace-font);
|
||||||
color: transparent;
|
color: transparent;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
|
@ -430,11 +462,13 @@ pre {
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
|
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
|
|
||||||
|
font-family: var(--monospace-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
pre, code {
|
code {
|
||||||
font-family: $monospace;
|
font-family: var(--monospace-font);
|
||||||
font-size: 1rem;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -447,7 +481,7 @@ figure {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
figcaption {
|
figcaption {
|
||||||
font-family: $sansSerif;
|
font-family: var(--ui-font);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: var(--secondary-ui-text-color);
|
color: var(--secondary-ui-text-color);
|
||||||
|
@ -671,3 +705,12 @@ figure {
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1455px) {
|
||||||
|
article .article-content aside {
|
||||||
|
position: initial;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
<title>Shadowfacts<% if (category) { %> (<%= category %>)<% } %></title>
|
<title>Shadowfacts<% if (tag) { %> (<%= tag %>)<% } %></title>
|
||||||
<subtitle>
|
<subtitle>
|
||||||
<% if (category) { %>
|
<% if (tag) { %>
|
||||||
Only <%= category %> posts.
|
Only <%= tag %> posts.
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
Just my various ramblings.
|
Just my various ramblings.
|
||||||
<% } %>
|
<% } %>
|
||||||
</subtitle>
|
</subtitle>
|
||||||
|
<% if (tag) { %>
|
||||||
|
<category term="<%= tag %>" />
|
||||||
|
<% } %>
|
||||||
<link rel="alternate" type="text/html" href="https://shadowfacts.net<%= permalink %>" />
|
<link rel="alternate" type="text/html" href="https://shadowfacts.net<%= permalink %>" />
|
||||||
<link rel="self" href="https://shadowfacts.net<%= feedPath %>" type="application/atom+xml" />
|
<link rel="self" href="https://shadowfacts.net<%= feedPath %>" type="application/atom+xml" />
|
||||||
<id>https://shadowfacts.net<%= feedPath %></id>
|
<id>https://shadowfacts.net<%= feedPath %></id>
|
||||||
|
@ -21,8 +24,10 @@
|
||||||
<title><%= post.metadata.title %></title>
|
<title><%= post.metadata.title %></title>
|
||||||
<updated><%= post.metadata.date.toISOString() %></updated>
|
<updated><%= post.metadata.date.toISOString() %></updated>
|
||||||
<link rel="alternate" type="text/html" href="https://shadowfacts.net<%= post.metadata.permalink %>" />
|
<link rel="alternate" type="text/html" href="https://shadowfacts.net<%= post.metadata.permalink %>" />
|
||||||
<% if (post.metadata.category) { %>
|
<% if (post.metadata.tags) { %>
|
||||||
<category term="<%= post.metadata.category %>" />
|
<% for (const tag of post.metadata.tags) { %>
|
||||||
|
<category term="<%= tag %>" />
|
||||||
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
<content type="html"><![CDATA[
|
<content type="html"><![CDATA[
|
||||||
<%- post.text %>
|
<%- post.text %>
|
||||||
|
|
|
@ -8,9 +8,11 @@
|
||||||
<% const fullFormatted = formatDate(metadata.date, "hh:mm:ss A MMM Do, YYYY") %>
|
<% const fullFormatted = formatDate(metadata.date, "hh:mm:ss A MMM Do, YYYY") %>
|
||||||
<time itemprop="datePublished" datetime="<%= metadata.date.toISOString() %>" title="<%= fullFormatted %>"><%= formatted %></time>
|
<time itemprop="datePublished" datetime="<%= metadata.date.toISOString() %>" title="<%= fullFormatted %>"><%= formatted %></time>
|
||||||
</span>
|
</span>
|
||||||
<% if (metadata.category) { %>
|
<% if (metadata.tags) { %>
|
||||||
in
|
in
|
||||||
<span itemprop="articleSection"><a href="/<%= metadata.category %>/" rel="category"><%= metadata.category %></a></span>
|
<% for (let i = 0; i < metadata.tags.length; i++) { %>
|
||||||
|
<span itemprop="articleSection"><a href="/<%= metadata.tags[i] %>/" rel="tag"><%= metadata.tags[i] %></a></span><%= i < metadata.tags.length - 1 ? ", " : "" %>
|
||||||
|
<% } %>
|
||||||
<% } else if (metadata.series) { %>
|
<% } else if (metadata.series) { %>
|
||||||
in
|
in
|
||||||
<span itemprop="articleSection"><a href="/tutorials/<%= metadata.series %>" rel="category"><%= metadata.seriesName %></a></span>
|
<span itemprop="articleSection"><a href="/tutorials/<%= metadata.series %>" rel="category"><%= metadata.seriesName %></a></span>
|
||||||
|
|
|
@ -48,14 +48,15 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav" role="navigation">
|
<nav class="site-nav" role="navigation">
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><%- fancyLink("Archive", "/archive/") %></li>
|
||||||
<li><%- fancyLink("Tutorials", "/tutorials/") %></li>
|
<li><%- fancyLink("Tutorials", "/tutorials/") %></li>
|
||||||
<li><%- fancyLink("RTFM", "https://rtfm.shadowfacts.net") %></li>
|
<li><%- fancyLink("RTFM", "https://rtfm.shadowfacts.net") %></li>
|
||||||
<li><%- fancyLink("Type", "https://type.shadowfacts.net") %></li>
|
|
||||||
<li>
|
<li>
|
||||||
<a href="#" class="dropdown-link" aria-haspopup="true">Other <span class="arrow arrow-down" aria-hidden="true"></span></a>
|
<a href="#" class="dropdown-link" aria-haspopup="true">Other <span class="arrow arrow-down" aria-hidden="true"></span></a>
|
||||||
<ul aria-label="other links">
|
<ul aria-label="other links">
|
||||||
<li><%- fancyLink("Gitea", "https://git.shadowfacts.net") %></li>
|
<li><%- fancyLink("Gitea", "https://git.shadowfacts.net") %></li>
|
||||||
<li><%- fancyLink("Maven", "https://maven.shadowfacts.net") %></li>
|
<li><%- fancyLink("Maven", "https://maven.shadowfacts.net") %></li>
|
||||||
|
<li><%- fancyLink("Type", "https://type.shadowfacts.net") %></li>
|
||||||
<li><%- fancyLink("Meme Machine", "https://mememachine.shadowfacts.net") %></li>
|
<li><%- fancyLink("Meme Machine", "https://mememachine.shadowfacts.net") %></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Hello, World!"
|
metadata.title = "Hello, World!"
|
||||||
metadata.category = "meta"
|
metadata.tags = ["meta"]
|
||||||
metadata.date = "2016-05-06 11:13:18 -0400"
|
metadata.date = "2016-05-06 11:13:18 -0400"
|
||||||
metadata.oldPermalink = ["/meta/2016/06/07/hello-world/", "/meta/2016/hello-world/"]
|
metadata.oldPermalink = ["/meta/2016/06/07/hello-world/", "/meta/2016/hello-world/"]
|
||||||
metadata.shortDesc = "Hello again, world! Welcome to the third iteration of my website."
|
metadata.shortDesc = "Hello again, world! Welcome to the third iteration of my website."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "1.9.4 Porting Spree"
|
metadata.title = "1.9.4 Porting Spree"
|
||||||
metadata.category = "minecraft"
|
metadata.tags = ["minecraft"]
|
||||||
metadata.date = "2016-05-21 17:47:18 -0400"
|
metadata.date = "2016-05-21 17:47:18 -0400"
|
||||||
metadata.oldPermalink = ["/mods/2016/05/21/194-porting-spree/", "/minecraft/2016/1-9-4-porting-spree/"]
|
metadata.oldPermalink = ["/mods/2016/05/21/194-porting-spree/", "/minecraft/2016/1-9-4-porting-spree/"]
|
||||||
metadata.shortDesc = "Now that Forge for 1.9.4 is out, I've begun the log and arduous process of porting my mods to 1.9.4 (if by long and arduous, you mean short and trivial)."
|
metadata.shortDesc = "Now that Forge for 1.9.4 is out, I've begun the log and arduous process of porting my mods to 1.9.4 (if by long and arduous, you mean short and trivial)."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Introducing RTFM"
|
metadata.title = "Introducing RTFM"
|
||||||
metadata.category = "minecraft"
|
metadata.tags = ["minecraft"]
|
||||||
metadata.date = "2016-06-29 12:00:00 -0400"
|
metadata.date = "2016-06-29 12:00:00 -0400"
|
||||||
metadata.oldPermalink = ["/meta/2016/06/29/introducing-rtfm/", "/minecraft/2016/introducing-rtfm/"]
|
metadata.oldPermalink = ["/meta/2016/06/29/introducing-rtfm/", "/minecraft/2016/introducing-rtfm/"]
|
||||||
metadata.shortDesc = "RTFM is the brand new website that will contain the documentation for all of my projects, currently it only contains documentation for MC mods."
|
metadata.shortDesc = "RTFM is the brand new website that will contain the documentation for all of my projects, currently it only contains documentation for MC mods."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Forge Modding Tutorials for 1.10.2"
|
metadata.title = "Forge Modding Tutorials for 1.10.2"
|
||||||
metadata.category = "minecraft"
|
metadata.tags = ["minecraft"]
|
||||||
metadata.date = "2016-06-30 10:35:00 -0400"
|
metadata.date = "2016-06-30 10:35:00 -0400"
|
||||||
metadata.oldPermalink = ["/meta/2016/06/30/forge-1102-tutorials/", "/minecraft/2016/forge-modding-tutorials-for-1-10-2"]
|
metadata.oldPermalink = ["/meta/2016/06/30/forge-1102-tutorials/", "/minecraft/2016/forge-modding-tutorials-for-1-10-2"]
|
||||||
metadata.shortDesc = "The Forge modding tutorials have all the been updated to MC 1.10.2 as has the GitHub repo."
|
metadata.shortDesc = "The Forge modding tutorials have all the been updated to MC 1.10.2 as has the GitHub repo."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Introducing Mirror"
|
metadata.title = "Introducing Mirror"
|
||||||
metadata.category = "java"
|
metadata.tags = ["java"]
|
||||||
metadata.date = "2016-07-28 16:45:00 -0400"
|
metadata.date = "2016-07-28 16:45:00 -0400"
|
||||||
metadata.oldPermalink = ["/java/2016/07/28/introducing-mirror/", "/java/2016/introducing-mirror/"]
|
metadata.oldPermalink = ["/java/2016/07/28/introducing-mirror/", "/java/2016/introducing-mirror/"]
|
||||||
metadata.shortDesc = "Allow me to introduce my latest project, Mirror. Mirror is a reflection library for Java designed to take advantage of the streams, lambdas, and optionals introduced in Java 8."
|
metadata.shortDesc = "Allow me to introduce my latest project, Mirror. Mirror is a reflection library for Java designed to take advantage of the streams, lambdas, and optionals introduced in Java 8."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Kotlin and Minecraft Forge"
|
metadata.title = "Kotlin and Minecraft Forge"
|
||||||
metadata.category = "minecraft"
|
metadata.tags = ["minecraft"]
|
||||||
metadata.date = "2016-08-06 16:45:30 -0400"
|
metadata.date = "2016-08-06 16:45:30 -0400"
|
||||||
metadata.oldPermalink = ["/forge/2016/08/06/kotlin-and-forge/", "/minecraft/2016/kotlin-and-minecraft-forge/"]
|
metadata.oldPermalink = ["/forge/2016/08/06/kotlin-and-forge/", "/minecraft/2016/kotlin-and-minecraft-forge/"]
|
||||||
metadata.shortDesc = "So, you wanna use Kotlin in your Forge mod? Well there's good news, I've just released Forgelin, a fork of Emberwalker's Forgelin, a library that provides utilities for using Kotlin with Minecraft/Forge. "
|
metadata.shortDesc = "So, you wanna use Kotlin in your Forge mod? Well there's good news, I've just released Forgelin, a fork of Emberwalker's Forgelin, a library that provides utilities for using Kotlin with Minecraft/Forge. "
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "The Great Redesign"
|
metadata.title = "The Great Redesign"
|
||||||
metadata.category = "meta"
|
metadata.tags = ["meta"]
|
||||||
metadata.date = "2016-08-07 15:39:48 -0400"
|
metadata.date = "2016-08-07 15:39:48 -0400"
|
||||||
metadata.oldPermalink = ["/meta/2016/08/07/the-great-redesign/", "/meta/2016/the-great-redesign/"]
|
metadata.oldPermalink = ["/meta/2016/08/07/the-great-redesign/", "/meta/2016/the-great-redesign/"]
|
||||||
metadata.shortDesc = "Welcome to the fourth iteration of my website."
|
metadata.shortDesc = "Welcome to the fourth iteration of my website."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Type: A FOSS clone of typing.io"
|
metadata.title = "Type: A FOSS clone of typing.io"
|
||||||
metadata.category = "misc"
|
metadata.tags = ["misc"]
|
||||||
metadata.date = "2016-10-08 17:29:42 -0400"
|
metadata.date = "2016-10-08 17:29:42 -0400"
|
||||||
metadata.oldPermalink = ["/misc/2016/10/08/type/", "/misc/2016/type/"]
|
metadata.oldPermalink = ["/misc/2016/10/08/type/", "/misc/2016/type/"]
|
||||||
metadata.shortDesc = "I made an awesome FOSS clone of typing.io that you can check out at type.shadowfacts.net."
|
metadata.shortDesc = "I made an awesome FOSS clone of typing.io that you can check out at type.shadowfacts.net."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "The Pretty Good Minor Update"
|
metadata.title = "The Pretty Good Minor Update"
|
||||||
metadata.category = "meta"
|
metadata.tags = ["meta"]
|
||||||
metadata.date = "2017-02-17 14:30:42 -0400"
|
metadata.date = "2017-02-17 14:30:42 -0400"
|
||||||
metadata.oldPermalink = ["/meta/2017/02/17/the-pretty-good-minor-update/", "/meta/2017/the-pretty-good-minor-update/"]
|
metadata.oldPermalink = ["/meta/2017/02/17/the-pretty-good-minor-update/", "/meta/2017/the-pretty-good-minor-update/"]
|
||||||
metadata.shortDesc = "It's been about six months since the last time I redesigned the site, and while I didn't want to redesign it yet again, I felt it could use a little update to make sure everything's still good."
|
metadata.shortDesc = "It's been about six months since the last time I redesigned the site, and while I didn't want to redesign it yet again, I felt it could use a little update to make sure everything's still good."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Comments Powered by GitHub"
|
metadata.title = "Comments Powered by GitHub"
|
||||||
metadata.category = "meta"
|
metadata.tags = ["meta"]
|
||||||
metadata.date = "2017-04-23 09:05:42 -0400"
|
metadata.date = "2017-04-23 09:05:42 -0400"
|
||||||
metadata.oldPermalink = ["/meta/2017/04/23/comments-powered-by-github/", "/meta/2017/comments-powered-by-git-hub/"]
|
metadata.oldPermalink = ["/meta/2017/04/23/comments-powered-by-github/", "/meta/2017/comments-powered-by-git-hub/"]
|
||||||
metadata.shortDesc = "I built a way of commenting on my static website using GitHub to store comments."
|
metadata.shortDesc = "I built a way of commenting on my static website using GitHub to store comments."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Reincarnation"
|
metadata.title = "Reincarnation"
|
||||||
metadata.category = "meta"
|
metadata.tags = ["meta"]
|
||||||
metadata.date = "2019-09-18 10:34:42 -0400"
|
metadata.date = "2019-09-18 10:34:42 -0400"
|
||||||
metadata.shortDesc = "Stand by for reincarnation."
|
metadata.shortDesc = "Stand by for reincarnation."
|
||||||
metadata.oldPermalink = "/meta/2019/reincarnation/"
|
metadata.oldPermalink = "/meta/2019/reincarnation/"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "ActivityPub Resources"
|
metadata.title = "ActivityPub Resources"
|
||||||
metadata.category = "activitypub"
|
metadata.tags = ["activitypub"]
|
||||||
metadata.date = "2019-09-22 17:50:42 -0400"
|
metadata.date = "2019-09-22 17:50:42 -0400"
|
||||||
metadata.shortDesc = "A compilation of resources I found useful in learning/implementing ActivityPub."
|
metadata.shortDesc = "A compilation of resources I found useful in learning/implementing ActivityPub."
|
||||||
metadata.oldPermalink = "/activitypub/2019/activity-pub-resources/"
|
metadata.oldPermalink = "/activitypub/2019/activity-pub-resources/"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Learning Elixir"
|
metadata.title = "Learning Elixir"
|
||||||
metadata.category = "elixir"
|
metadata.tags = ["elixir"]
|
||||||
metadata.date = "2019-10-10 12:29:42 -0400"
|
metadata.date = "2019-10-10 12:29:42 -0400"
|
||||||
metadata.shortDesc = "How I learned Elixir and why I love it."
|
metadata.shortDesc = "How I learned Elixir and why I love it."
|
||||||
metadata.oldPermalink = "/elixir/2019/learning-elixir/"
|
metadata.oldPermalink = "/elixir/2019/learning-elixir/"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Building a JavaScript-Free Slide-Over Menu"
|
metadata.title = "Building a JavaScript-Free Slide-Over Menu"
|
||||||
metadata.category = "web"
|
metadata.tags = ["web"]
|
||||||
metadata.date = "2019-11-11 21:08:42 -0400"
|
metadata.date = "2019-11-11 21:08:42 -0400"
|
||||||
metadata.shortDesc = "Building a slide-over hamburger menu without using JavaScript."
|
metadata.shortDesc = "Building a slide-over hamburger menu without using JavaScript."
|
||||||
metadata.oldPermalink = "/web/2019/js-free-hamburger-menu/"
|
metadata.oldPermalink = "/web/2019/js-free-hamburger-menu/"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Mocking HTTP Requests for iOS App UI Tests"
|
metadata.title = "Mocking HTTP Requests for iOS App UI Tests"
|
||||||
metadata.category = "swift"
|
metadata.tags = ["swift"]
|
||||||
metadata.date = "2019-12-22 19:12:42 -0400"
|
metadata.date = "2019-12-22 19:12:42 -0400"
|
||||||
metadata.shortDesc = "Integrating a tiny web server into your Xcode UI test target to mock HTTP requests."
|
metadata.shortDesc = "Integrating a tiny web server into your Xcode UI test target to mock HTTP requests."
|
||||||
metadata.oldPermalink = "/ios/2019/mock-http-ios-ui-testing/"
|
metadata.oldPermalink = "/ios/2019/mock-http-ios-ui-testing/"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Faking the Mongo Eval Command"
|
metadata.title = "Faking the Mongo Eval Command"
|
||||||
metadata.category = "swift"
|
metadata.tags = ["swift"]
|
||||||
metadata.date = "2020-01-28 19:33:42 -0400"
|
metadata.date = "2020-01-28 19:33:42 -0400"
|
||||||
metadata.shortDesc = "MongoDB 4.2 removed the eval command, which is a good security measure, but unfortunate for building database-viewing GUI."
|
metadata.shortDesc = "MongoDB 4.2 removed the eval command, which is a good security measure, but unfortunate for building database-viewing GUI."
|
||||||
metadata.slug = "faking-mongo-eval"
|
metadata.slug = "faking-mongo-eval"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Simple Swift Promises"
|
metadata.title = "Simple Swift Promises"
|
||||||
metadata.category = "swift"
|
metadata.tags = ["swift"]
|
||||||
metadata.date = "2020-02-18 22:10:42 -0400"
|
metadata.date = "2020-02-18 22:10:42 -0400"
|
||||||
metadata.shortDesc = "Building a rudimentary implementation of asynchronous promises in Swift."
|
metadata.shortDesc = "Building a rudimentary implementation of asynchronous promises in Swift."
|
||||||
metadata.slug = "simple-swift-promises"
|
metadata.slug = "simple-swift-promises"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Writing a JavaScript Syntax Highlighter in Swift"
|
metadata.title = "Writing a JavaScript Syntax Highlighter in Swift"
|
||||||
metadata.category = "swift"
|
metadata.tags = ["swift"]
|
||||||
metadata.date = "2020-04-09 11:48:42 -0400"
|
metadata.date = "2020-04-09 11:48:42 -0400"
|
||||||
metadata.shortDesc = "Things I learned while building a tiny syntax highlighter."
|
metadata.shortDesc = "Things I learned while building a tiny syntax highlighter."
|
||||||
metadata.slug = "syntax-highlighting-javascript"
|
metadata.slug = "syntax-highlighting-javascript"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "The Sorry State of Thunderbolt 3 Docks"
|
metadata.title = "The Sorry State of Thunderbolt 3 Docks"
|
||||||
metadata.category = "computers"
|
metadata.tags = ["computers"]
|
||||||
metadata.date = "2020-04-13 17:19:42 -0400"
|
metadata.date = "2020-04-13 17:19:42 -0400"
|
||||||
metadata.shortDesc = "On a quest to find a Thunderbolt dock that meets my needs."
|
metadata.shortDesc = "On a quest to find a Thunderbolt dock that meets my needs."
|
||||||
metadata.slug = "thunderbolt-3"
|
metadata.slug = "thunderbolt-3"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Switching to Vim"
|
metadata.title = "Switching to Vim"
|
||||||
metadata.category = "editors"
|
metadata.tags = ["editors"]
|
||||||
metadata.date = "2020-05-21 18:22:42 -0400"
|
metadata.date = "2020-05-21 18:22:42 -0400"
|
||||||
metadata.shortDesc = "How I went about joining the cult of Vim."
|
metadata.shortDesc = "How I went about joining the cult of Vim."
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Algorithmic Bias"
|
metadata.title = "Algorithmic Bias"
|
||||||
metadata.category = "misc"
|
metadata.tags = ["misc"]
|
||||||
metadata.date = "2020-06-05 09:55:42 -0400"
|
metadata.date = "2020-06-05 09:55:42 -0400"
|
||||||
metadata.shortDesc = ""
|
metadata.shortDesc = ""
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Replicating Safari's Link Preview Animation"
|
metadata.title = "Replicating Safari's Link Preview Animation"
|
||||||
metadata.category = "swift"
|
metadata.tags = ["swift"]
|
||||||
metadata.date = "2020-07-03 16:28:42 -0400"
|
metadata.date = "2020-07-03 16:28:42 -0400"
|
||||||
metadata.shortDesc = ""
|
metadata.shortDesc = ""
|
||||||
metadata.slug = "uipreviewparameters-textlinerects"
|
metadata.slug = "uipreviewparameters-textlinerects"
|
||||||
|
@ -122,7 +122,9 @@ return UITargetedPreview(view: snapshotContainer, parameters: parameters, target
|
||||||
|
|
||||||
And with that, only the link text is visible in the preview animation and it expands nicely into the full preview:
|
And with that, only the link text is visible in the preview animation and it expands nicely into the full preview:
|
||||||
|
|
||||||
|
<div>
|
||||||
<%- video(metadata, "masked", {style: "width: 50%; margin: 0 auto; display: block;", title: "Screen recording of a link being previewed and dismissed with the link text animating back to its starting position upon dismissal."}) %>
|
<%- video(metadata, "masked", {style: "width: 50%; margin: 0 auto; display: block;", title: "Screen recording of a link being previewed and dismissed with the link text animating back to its starting position upon dismissal."}) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
But, there's still one small detail as keen-eyed readers may have noticed. In Safari, when dismissing the full preview, it animates back into the preview view and springs back to the original position. With our implementation, however, it doesn't. The preview view controller does animate back into the preview view, however, instead of returning to the original position, it disappears off into the middle of the screen. This is because there's still one `UIContextMenuInteractionDelegate` method we need to implement: `contextMenuInteraction(_:previewForDismissingMenuWithConfiguration:)`. Similar to the `previewForHighlighting` method, this method takes the interaction and the context menu configuration, creating a `UITargetedPreview` that should be used during the dismissal animation. Since we want the preview to go back to the same location while dismissing as it came from while expanding, we can cache the targeted preview we've already constructed for the highlight method and return it from the dismissal method.
|
But, there's still one small detail as keen-eyed readers may have noticed. In Safari, when dismissing the full preview, it animates back into the preview view and springs back to the original position. With our implementation, however, it doesn't. The preview view controller does animate back into the preview view, however, instead of returning to the original position, it disappears off into the middle of the screen. This is because there's still one `UIContextMenuInteractionDelegate` method we need to implement: `contextMenuInteraction(_:previewForDismissingMenuWithConfiguration:)`. Similar to the `previewForHighlighting` method, this method takes the interaction and the context menu configuration, creating a `UITargetedPreview` that should be used during the dismissal animation. Since we want the preview to go back to the same location while dismissing as it came from while expanding, we can cache the targeted preview we've already constructed for the highlight method and return it from the dismissal method.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.title = "Implement a Gemini Protocol Client Using Network.framework"
|
metadata.title = "Implement a Gemini Protocol Client Using Network.framework"
|
||||||
metadata.category = "swift"
|
metadata.tags = ["swift", "gemini"]
|
||||||
metadata.date = "2020-07-22 21:57:42 -0400"
|
metadata.date = "2020-07-22 21:57:42 -0400"
|
||||||
metadata.shortDesc = ""
|
metadata.shortDesc = ""
|
||||||
metadata.slug = "gemini-network-framework"
|
metadata.slug = "gemini-network-framework"
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
```
|
||||||
|
metadata.title = "SwiftUI Auto-Expanding Text Views"
|
||||||
|
metadata.tags = ["swift"]
|
||||||
|
metadata.date = "2020-08-29 11:20:42 -0400"
|
||||||
|
metadata.shortDesc = "Building a non-scrolling UITextView for use in SwiftUI layouts."
|
||||||
|
metadata.slug = "swiftui-expanding-text-view"
|
||||||
|
```
|
||||||
|
|
||||||
|
I'm currently in the process of rewriting the Compose screen in Tusker to use SwiftUI. This has mostly been a smooth process, but there have been a few hiccups, the first of which was the main text box. The updates to SwiftUI introduced in iOS 14 included [`TextEditor`](https://developer.apple.com/documentation/swiftui/texteditor), the SwiftUI equivalent of `UITextView` to allow multi-line text editing. Unfortunately, there's no (straightforward) way of disabling scrolling, making it unsuitable for some designs where the text view is embedded in a separate scroll view. Additionally, the fact that it's not available at all on iOS 13 means layouts that require non-scrolling multi-line text editors must wrap `UITextView` themselves in order to be achievable with SwiftUI.
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
You'd think this would be pretty simple: just use `UIViewRepresentable` with `UITextView` and disable scrolling. But if you try that approach, you'll find a few issues that make things a bit more complex. While setting the `isScrollEnabled` property on the text view to `false` does indeed disable scrolling and make the text view expand itself as more lines are typed, the text view does not entirely respect SwiftUI's layout system. Typing text that is larger than the available space causes the text view to expand outwards from the centerpoint, screwing up the layout, instead of wrapping the text onto the next line.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%- video(metadata, "scroll-disabled", {style: "width: 50%; margin: 0 auto; display: block;", title: "Screen recording of a non-scrolling UITextView inside SwiftUI being edited. When the text view grows, it expands from the center point and ends up offscreen."}) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Enabling scrolling on the text view partially solves this, making the text wrap whenever the user types something longer than fits on a single line. Of course, this also reintroduces the problem that the text view now scrolls instead of expanding to fit the contents. The simplest solution I've come up with for this problem is to have the SwiftUI side of the layout automatically expand the text view's frame whenever the contents changes. So, even though the `UITextView` is allowed to scroll, it never will because the layout will ensure that the actual size of the view is always taller than the text's height. Additionally, with bouncing disabled, there's no indication from the user's perspective that this is anything other than a regular non-scrolling text view.
|
||||||
|
|
||||||
|
Actually implementing this is pretty simple. There's a `UIViewRepresentable` which wraps the `UITextView` and plumbs a `Binding<String>` up to the text view. It also stores a closure that receives the `UITextView` which is invoked whenever the text changes, using the `UITextViewDelegate` method. This will allow the actual SwiftUI view to listen for text view changes and update the frame of the wrapped view.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WrappedTextView: UIViewRepresentable {
|
||||||
|
typealias UIViewType = UITextView
|
||||||
|
|
||||||
|
@Binding var text: String
|
||||||
|
let textDidChange: (UITextView) -> Void
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UITextView {
|
||||||
|
let view = UITextView()
|
||||||
|
view.isEditable = true
|
||||||
|
view.delegate = context.coordinator
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
|
uiView.text = self.text
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
textDidChange(uiView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
return Coordinator(text: $text, textDidChange: textDidChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UITextViewDelegate {
|
||||||
|
@Binding var text: String
|
||||||
|
let textDidChange: (UITextView) -> Void
|
||||||
|
|
||||||
|
init(text: Binding<String>, textDidChange: @escaping (UITextView) -> Void) {
|
||||||
|
self._text = text
|
||||||
|
self.textDidChange = textDidChange
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
self.text = textView.text
|
||||||
|
self.textDidChange(textView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
One key line to note is that, in the `updateUIView` method, after the text is updated, the `textDidChange` closure is called. This is necessary because the `UITextView.text` setter does not call the delegate method automatically. So, if the text was changed programatically, the delegate method wouldn't be called and, in turn, the did-change callback wouldn't execute, preventing the height from updating. `DispatchQueue.main.async` is used to defer running updating the view height until the next runloop iteration for two reasons:
|
||||||
|
|
||||||
|
1. So that we're not modifying view state during view updates, as that's undefined behavior in SwiftUI.
|
||||||
|
2. Because the UITextView doesn't recalculate its content size immediately when the text is set.
|
||||||
|
|
||||||
|
Waiting until the next runloop iteration solves both of those issues: the SwiftUI view updates will have finished and the text view will have recalculated its size.
|
||||||
|
|
||||||
|
The wrapping SwiftUI view is pretty simple. It passes the string binding through to the wrapped view and it also stores its minimum height, as well as an internal `@State` variable for the current height of the text view. The text view height is an optional, because before the text view appears, there is no height.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct ExpandingTextView: View {
|
||||||
|
@Binding var text: String
|
||||||
|
let minHeight: CGFloat = 150
|
||||||
|
@State private var textViewHeight: CGFloat?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
WrappedTextView(text: $text, textDidChange: self.textDidChange)
|
||||||
|
.frame(height: height ?? minHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func textDidChange(_ textView: UITextView) {
|
||||||
|
self.height = max(textView.contentSize.height, minHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, everything works correctly. The text view wraps text and expands to fit user input as expected, as well as updating its height when the content is altered in code.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%- video(metadata, "custom-wrapper", {style: "width: 50%; margin: 0 auto; display: block;", title: "Screen recording of a custom text view inside SwiftUI. When the text changes, the scroll view does not overflow and the height expands to fit the content."}) %>
|
||||||
|
</div>
|
BIN
site/static/2020/swiftui-expanding-text-view/custom-wrapper.mp4 (Stored with Git LFS)
Normal file
BIN
site/static/2020/swiftui-expanding-text-view/custom-wrapper.mp4 (Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
BIN
site/static/2020/swiftui-expanding-text-view/scroll-disabled.mp4 (Stored with Git LFS)
Normal file
BIN
site/static/2020/swiftui-expanding-text-view/scroll-disabled.mp4 (Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
BIN
site/static/2020/uipreviewparameters-textlinerects/dismiss.mp4 (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/dismiss.mp4 (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/dismiss.webm (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/dismiss.webm (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/masked.mp4 (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/masked.mp4 (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/masked.webm (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/masked.webm (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/safari-multiline.mp4 (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/safari-multiline.mp4 (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/safari-multiline.webm (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/safari-multiline.webm (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/safari.mp4 (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/safari.mp4 (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/safari.webm (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/safari.webm (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/unmasked-broken.mp4 (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/unmasked-broken.mp4 (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/unmasked-broken.webm (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/unmasked-broken.webm (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/unmasked.mp4 (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/unmasked.mp4 (Stored with Git LFS)
Binary file not shown.
BIN
site/static/2020/uipreviewparameters-textlinerects/unmasked.webm (Stored with Git LFS)
BIN
site/static/2020/uipreviewparameters-textlinerects/unmasked.webm (Stored with Git LFS)
Binary file not shown.
|
@ -3,9 +3,9 @@ metadata.layout = "default.html.ejs"
|
||||||
```
|
```
|
||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<h1 class="page-heading"><%= category %> posts</h1>
|
<h1 class="page-heading"><%= tag %> posts</h1>
|
||||||
<p class="rss">
|
<p class="rss">
|
||||||
Subscribe to just <%= category %> posts via <a href="/<%= category %>/feed.xml">RSS</a>.
|
Subscribe to just <%= tag %> posts via <a href="/<%= tag %>/feed.xml">RSS</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<% for (const post of posts) { %>
|
<% for (const post of posts) { %>
|
|
@ -1,5 +1,6 @@
|
||||||
```
|
```
|
||||||
metadata.layout = "default.html.ejs"
|
metadata.layout = "default.html.ejs"
|
||||||
|
metadata.title = "Tutorials"
|
||||||
```
|
```
|
||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|
Loading…
Reference in New Issue