The fifth version of chriskrycho.com, built in Eleventy.

Semicolons are good, actually (at least in JS).

[skip netlify]

+2 -2
.eleventy.js
··· 1 1 require('ts-node').register({ 2 2 project: 'tsconfig.json', 3 - }) 3 + }); 4 4 5 - module.exports = require('./eleventy/config') 5 + module.exports = require('./eleventy/config');
+5 -5
eleventy/absolute-url.ts
··· 1 - import { Result } from 'true-myth' 2 - import { URL } from 'url' 3 - import { logErr, toString } from './utils' 1 + import { Result } from 'true-myth'; 2 + import { URL } from 'url'; 3 + import { logErr, toString } from './utils'; 4 4 5 5 export const absoluteUrl = (path: string, baseUrl: string): string => 6 6 Result.tryOrElse(logErr, () => new URL(path, baseUrl)) 7 7 .map(toString) 8 - .unwrapOr(path) 8 + .unwrapOr(path); 9 9 10 - export default absoluteUrl 10 + export default absoluteUrl;
+48 -48
eleventy/archive-by-year.ts
··· 1 - import { DateTime } from 'luxon' 2 - import { Item } from '../types/eleventy' 3 - import { fromDateOrString, canParseDate } from './date-time' 1 + import { DateTime } from 'luxon'; 2 + import { Item } from '../types/eleventy'; 3 + import { fromDateOrString, canParseDate } from './date-time'; 4 4 5 5 export interface Year { 6 - name: string 7 - months: Month[] 6 + name: string; 7 + months: Month[]; 8 8 } 9 9 10 10 export interface Month { 11 - name: string 12 - days: Day[] 11 + name: string; 12 + days: Day[]; 13 13 } 14 14 15 15 export interface Day { 16 - name: string 17 - items: Item[] 16 + name: string; 17 + items: Item[]; 18 18 } 19 19 20 - type Archive = Year[] 20 + type Archive = Year[]; 21 21 22 22 export const enum Order { 23 23 OldFirst = 'OLD_FIRST', 24 24 NewFirst = 'NEW_FIRST', 25 25 } 26 26 27 - const YEAR_FORMAT = 'yyyy' 28 - const MONTH_FORMAT = 'MMM' 29 - const DAY_FORMAT = 'dd' 27 + const YEAR_FORMAT = 'yyyy'; 28 + const MONTH_FORMAT = 'MMM'; 29 + const DAY_FORMAT = 'dd'; 30 30 31 - export const TZ_OPTIONS: Intl.DateTimeFormatOptions = { timeZoneName: 'America/Denver' } 31 + export const TZ_OPTIONS: Intl.DateTimeFormatOptions = { timeZoneName: 'America/Denver' }; 32 32 33 - type DayMap = Map<number, Item[]> 34 - type MonthMap = Map<number, [string, DayMap]> 35 - type YearMap = Map<number, [string, MonthMap]> 33 + type DayMap = Map<number, Item[]>; 34 + type MonthMap = Map<number, [string, DayMap]>; 35 + type YearMap = Map<number, [string, MonthMap]>; 36 36 37 37 export const byDate = (order: Order) => (a: Item, b: Item): number => { 38 38 // Sort order is meaningless if either doesn't have the relevant comparison key. 39 39 if (!canParseDate(a.date) || !canParseDate(b.date)) { 40 - return 0 40 + return 0; 41 41 } 42 42 43 - const aDate = fromDateOrString(a.date).toSeconds() 44 - const bDate = fromDateOrString(b.date).toSeconds() 45 - return order === Order.OldFirst ? aDate - bDate : bDate - aDate 46 - } 43 + const aDate = fromDateOrString(a.date).toSeconds(); 44 + const bDate = fromDateOrString(b.date).toSeconds(); 45 + return order === Order.OldFirst ? aDate - bDate : bDate - aDate; 46 + }; 47 47 48 48 export const byUpdated = (order: Order) => (a: Item, b: Item): number => { 49 49 // Sort order is meaningless if either doesn't have the relevant comparison key. 50 50 if (!a.data || !b.data) { 51 - return 0 51 + return 0; 52 52 } 53 53 54 54 // Likewise if the value isn't parseable for either. 55 55 if (!canParseDate(a.data.updated) || !canParseDate(b.data.updated)) { 56 - return 0 56 + return 0; 57 57 } 58 58 59 - const aUpdated = fromDateOrString(a.data.updated).toSeconds() 60 - const bUpdated = fromDateOrString(b.data.updated).toSeconds() 61 - return order === Order.OldFirst ? aUpdated - bUpdated : bUpdated - aUpdated 62 - } 59 + const aUpdated = fromDateOrString(a.data.updated).toSeconds(); 60 + const bUpdated = fromDateOrString(b.data.updated).toSeconds(); 61 + return order === Order.OldFirst ? aUpdated - bUpdated : bUpdated - aUpdated; 62 + }; 63 63 64 - const dateTimeFromItem = ({ date }: Item): DateTime => fromDateOrString(date) 64 + const dateTimeFromItem = ({ date }: Item): DateTime => fromDateOrString(date); 65 65 66 66 const daysFromDayMap = (dayMap: DayMap, byEntries: SortByEntries, order: Order): Day[] => 67 67 [...dayMap.entries()].sort(byEntries).map(([, items]) => ({ 68 68 name: dateTimeFromItem(items[0]).toFormat(DAY_FORMAT, TZ_OPTIONS), 69 69 items: items.slice().sort(byDate(order)), 70 - })) 70 + })); 71 71 72 72 const monthsFromMonthMap = ( 73 73 monthMap: MonthMap, ··· 77 77 [...monthMap.entries()].sort(byEntries).map(([, [name, dayMap]]) => ({ 78 78 name, 79 79 days: daysFromDayMap(dayMap, byEntries, order), 80 - })) 80 + })); 81 81 82 82 const dayMapFromItem = (item: Item, dateTime: DateTime): DayMap => 83 - new Map([[dateTime.day, [item]]]) 83 + new Map([[dateTime.day, [item]]]); 84 84 85 85 const monthMapFromItem = (item: Item, dateTime: DateTime): MonthMap => 86 86 new Map([ ··· 88 88 dateTime.month, 89 89 [dateTime.toFormat(MONTH_FORMAT, TZ_OPTIONS), dayMapFromItem(item, dateTime)], 90 90 ], 91 - ]) 91 + ]); 92 92 93 93 function toYearMap(yearMap: YearMap, item: Item): YearMap { 94 - const itemDateTime = dateTimeFromItem(item) 95 - const { year, month, day } = itemDateTime 94 + const itemDateTime = dateTimeFromItem(item); 95 + const { year, month, day } = itemDateTime; 96 96 97 - const existingMonthMap = yearMap.get(year) 97 + const existingMonthMap = yearMap.get(year); 98 98 if (existingMonthMap) { 99 - const existingDayMap = existingMonthMap[1].get(month) 99 + const existingDayMap = existingMonthMap[1].get(month); 100 100 if (existingDayMap) { 101 - const existingDay = existingDayMap[1].get(day) 101 + const existingDay = existingDayMap[1].get(day); 102 102 if (existingDay) { 103 - existingDay.push(item) 103 + existingDay.push(item); 104 104 } else { 105 - existingDayMap[1].set(day, [item]) 105 + existingDayMap[1].set(day, [item]); 106 106 } 107 107 } else { 108 108 existingMonthMap[1].set(month, [ 109 109 itemDateTime.toFormat(MONTH_FORMAT, TZ_OPTIONS), 110 110 dayMapFromItem(item, itemDateTime), 111 - ]) 111 + ]); 112 112 } 113 113 } else { 114 114 yearMap.set(year, [ 115 115 itemDateTime.toFormat(YEAR_FORMAT, TZ_OPTIONS), 116 116 monthMapFromItem(item, itemDateTime), 117 - ]) 117 + ]); 118 118 } 119 - return yearMap 119 + return yearMap; 120 120 } 121 121 122 - type SortByEntries = ([a]: [number, unknown], [b]: [number, unknown]) => number 122 + type SortByEntries = ([a]: [number, unknown], [b]: [number, unknown]) => number; 123 123 124 124 const sortBy = (order: Order): SortByEntries => ([a], [b]): number => 125 - order === Order.OldFirst ? a - b : b - a 125 + order === Order.OldFirst ? a - b : b - a; 126 126 127 127 const intoYear = (byEntries: SortByEntries, order: Order) => ([, [name, monthMap]]: [ 128 128 number, ··· 130 130 ]): Year => ({ 131 131 name, 132 132 months: monthsFromMonthMap(monthMap, byEntries, order), 133 - }) 133 + }); 134 134 135 135 /** 136 136 Given a collection of items, generate a yearly-and-monthly-and-daily grouping. ··· 138 138 @param items The collection to produce an archive for 139 139 */ 140 140 export default function archiveByYear(items: Item[], order = Order.NewFirst): Archive { 141 - const byOrder = sortBy(order) 141 + const byOrder = sortBy(order); 142 142 143 143 return [...items.reduce(toYearMap, new Map()).entries()] 144 144 .sort(byOrder) 145 - .map(intoYear(byOrder, order)) 145 + .map(intoYear(byOrder, order)); 146 146 }
+74 -74
eleventy/config.ts
··· 1 - import { env } from 'process' 1 + import { env } from 'process'; 2 2 3 - import { DateTime } from 'luxon' 3 + import { DateTime } from 'luxon'; 4 4 5 - import { Config, Item, UserConfig, Collection } from '../types/eleventy' 6 - import absoluteUrl from './absolute-url' 7 - import archiveByYear, { byDate, byUpdated, Order } from './archive-by-year' 8 - import copyright from './copyright' 9 - import currentPage from './current-page' 10 - import toDateTime, { canParseDate, fromDateOrString, TZ } from './date-time' 11 - import isoDate from './iso-date' 12 - import localeDate from './locale-date' 13 - import markdown from './markdown' 14 - import * as PageLinks from './page-links' 15 - import spacewell from './plugin-spacewell' 16 - import typeset from './plugin-typeset' 17 - import siteTitle from './site-title' 18 - import excludingCollection from './excluding-collection' 19 - import toCollection from './to-collection' 5 + import { Config, Item, UserConfig, Collection } from '../types/eleventy'; 6 + import absoluteUrl from './absolute-url'; 7 + import archiveByYear, { byDate, byUpdated, Order } from './archive-by-year'; 8 + import copyright from './copyright'; 9 + import currentPage from './current-page'; 10 + import toDateTime, { canParseDate, fromDateOrString, TZ } from './date-time'; 11 + import isoDate from './iso-date'; 12 + import localeDate from './locale-date'; 13 + import markdown from './markdown'; 14 + import * as PageLinks from './page-links'; 15 + import spacewell from './plugin-spacewell'; 16 + import typeset from './plugin-typeset'; 17 + import siteTitle from './site-title'; 18 + import excludingCollection from './excluding-collection'; 19 + import toCollection from './to-collection'; 20 20 21 - import './feed' // for extension of types -- TODO: move those types elsewhere! 21 + import './feed'; // for extension of types -- TODO: move those types elsewhere! 22 22 23 - const BUILD_TIME = DateTime.fromJSDate(new Date(), TZ).toSeconds() 23 + const BUILD_TIME = DateTime.fromJSDate(new Date(), TZ).toSeconds(); 24 24 25 25 // Hack around the fact that in dev I want this to work on *every run*, but in prod builds 26 26 // I just want one time for the whole run. 27 27 const buildTime = () => 28 - env.DEV ? DateTime.fromJSDate(new Date(), TZ).toSeconds() : BUILD_TIME 28 + env.DEV ? DateTime.fromJSDate(new Date(), TZ).toSeconds() : BUILD_TIME; 29 29 30 30 const isLive = (item: Item) => 31 31 canParseDate(item.date) && 32 32 fromDateOrString(item.date).toSeconds() <= buildTime() && 33 - !item.data?.draft 33 + !item.data?.draft; 34 34 35 - const isNotVoid = <A>(a: A | null | undefined): a is A => a != null 35 + const isNotVoid = <A>(a: A | null | undefined): a is A => a != null; 36 36 37 37 const excludingStandalonePages = (item: Item): boolean => 38 - !(item.data?.standalonePage ?? false) 38 + !(item.data?.standalonePage ?? false); 39 39 40 - const filterStandalonePages = (items: Item[]) => items.filter(excludingStandalonePages) 40 + const filterStandalonePages = (items: Item[]) => items.filter(excludingStandalonePages); 41 41 42 42 /** 43 43 Use a path to create a collection from all items contained within it. ··· 55 55 .filter(isLive) 56 56 .filter(excludingStandalonePages) 57 57 .sort(byDate(Order.NewFirst)), 58 - ) 58 + ); 59 59 } 60 60 61 61 const inCollectionNamed = (name: string) => (item: Item): boolean => 62 - item.data?.collections[name]?.includes(item) ?? false 62 + item.data?.collections[name]?.includes(item) ?? false; 63 63 64 64 function latest(collection: Collection): Item[] { 65 65 const all = collection 66 66 .getAll() 67 67 .filter(isLive) 68 68 .filter(excludingStandalonePages) 69 - .sort(byDate(Order.NewFirst)) 69 + .sort(byDate(Order.NewFirst)); 70 70 71 71 return [ 72 72 all.find(inCollectionNamed('essays')), ··· 76 76 all.find(inCollectionNamed('elsewhere')), 77 77 ] 78 78 .filter(isNotVoid) 79 - .sort(byDate(Order.NewFirst)) 79 + .sort(byDate(Order.NewFirst)); 80 80 } 81 81 82 - const hasUpdated = (item: Item) => canParseDate(item.data?.updated) 82 + const hasUpdated = (item: Item) => canParseDate(item.data?.updated); 83 83 84 84 function mostRecentlyUpdated(collection: Collection): Item[] { 85 85 const all = collection ··· 87 87 .filter(isLive) 88 88 .filter(excludingStandalonePages) 89 89 .filter(hasUpdated) 90 - .sort(byUpdated(Order.NewFirst)) 90 + .sort(byUpdated(Order.NewFirst)); 91 91 92 92 return [ 93 93 all.find(inCollectionNamed('essays')), ··· 95 95 all.find(inCollectionNamed('library')), 96 96 ] 97 97 .filter(isNotVoid) 98 - .sort(byUpdated(Order.NewFirst)) 98 + .sort(byUpdated(Order.NewFirst)); 99 99 } 100 100 101 - const isFeatured = (item: Item): boolean => item.data?.featured ?? false 101 + const isFeatured = (item: Item): boolean => item.data?.featured ?? false; 102 102 103 103 const featured = (collection: Collection): Item[] => 104 104 collection ··· 107 107 .filter(excludingStandalonePages) 108 108 .sort(byDate(Order.NewFirst)) 109 109 .filter(isFeatured) 110 - .sort(byDate(Order.NewFirst)) 110 + .sort(byDate(Order.NewFirst)); 111 111 112 112 function config(config: Config): UserConfig { 113 113 config.addPlugin( ··· 115 115 only: '.content-block', 116 116 disable: ['smallCaps', 'hyphenate', 'ligatures', 'smallCaps'], 117 117 }), 118 - ) 118 + ); 119 119 120 - config.addPlugin(spacewell({ emDashes: true, enDashes: true })) 120 + config.addPlugin(spacewell({ emDashes: true, enDashes: true })); 121 121 122 - config.addFilter('md', markdown.render.bind(markdown)) 123 - config.addFilter('inlineMd', markdown.renderInline.bind(markdown)) 122 + config.addFilter('md', markdown.render.bind(markdown)); 123 + config.addFilter('inlineMd', markdown.renderInline.bind(markdown)); 124 124 125 - config.addFilter('toCollection', toCollection) 126 - config.addFilter('stringify', (obj) => JSON.stringify(obj)) 127 - config.addFilter('archiveByYears', archiveByYear) 128 - config.addFilter('absoluteUrl', absoluteUrl) 129 - config.addFilter('isoDate', isoDate) 130 - config.addFilter('toDateTime', toDateTime) 131 - config.addFilter('siteTitle', siteTitle) 125 + config.addFilter('toCollection', toCollection); 126 + config.addFilter('stringify', (obj) => JSON.stringify(obj)); 127 + config.addFilter('archiveByYears', archiveByYear); 128 + config.addFilter('absoluteUrl', absoluteUrl); 129 + config.addFilter('isoDate', isoDate); 130 + config.addFilter('toDateTime', toDateTime); 131 + config.addFilter('siteTitle', siteTitle); 132 132 config.addFilter('withValidDate', (items: Item[]) => 133 133 items.filter((item) => canParseDate(item.date)), 134 - ) 135 - config.addFilter('current', currentPage) 136 - config.addFilter('editLink', PageLinks.edit) 137 - config.addFilter('historyLink', PageLinks.history) 138 - config.addFilter('sourceLink', PageLinks.source) 139 - config.addFilter('excludingCollection', excludingCollection) 140 - config.addFilter('excludingStandalonePages', filterStandalonePages) 134 + ); 135 + config.addFilter('current', currentPage); 136 + config.addFilter('editLink', PageLinks.edit); 137 + config.addFilter('historyLink', PageLinks.history); 138 + config.addFilter('sourceLink', PageLinks.source); 139 + config.addFilter('excludingCollection', excludingCollection); 140 + config.addFilter('excludingStandalonePages', filterStandalonePages); 141 141 config.addFilter('concat', (a: Item[] | undefined, b: Item[] | undefined) => { 142 - return (a ?? []).concat(b ?? []) 143 - }) 144 - config.addFilter('localeDate', localeDate) 145 - config.addFilter('isLive', (items: Item[]) => items.filter(isLive)) 142 + return (a ?? []).concat(b ?? []); 143 + }); 144 + config.addFilter('localeDate', localeDate); 145 + config.addFilter('isLive', (items: Item[]) => items.filter(isLive)); 146 146 147 - config.addShortcode('localeDate', localeDate) 148 - config.addShortcode('copyright', copyright) 147 + config.addShortcode('localeDate', localeDate); 148 + config.addShortcode('copyright', copyright); 149 149 150 - config.addPassthroughCopy('site/_redirects') 151 - config.addPassthroughCopy('site/robots.txt') 150 + config.addPassthroughCopy('site/_redirects'); 151 + config.addPassthroughCopy('site/robots.txt'); 152 152 config.addPassthroughCopy({ 153 153 'site/_assets': 'assets', 154 154 'site/_styles': 'styles', 155 - }) 155 + }); 156 156 157 - config.addCollection('live', (collection) => collection.getAllSorted().filter(isLive)) 157 + config.addCollection('live', (collection) => collection.getAllSorted().filter(isLive)); 158 158 config.addCollection('pages', (collection) => 159 159 collection.getAll().filter((item) => item.data?.standalonePage), 160 - ) 161 - addCollectionFromDir(config, 'journal') 162 - addCollectionFromDir(config, 'essays') 163 - addCollectionFromDir(config, 'library') 164 - addCollectionFromDir(config, 'notes') 165 - addCollectionFromDir(config, 'elsewhere') 160 + ); 161 + addCollectionFromDir(config, 'journal'); 162 + addCollectionFromDir(config, 'essays'); 163 + addCollectionFromDir(config, 'library'); 164 + addCollectionFromDir(config, 'notes'); 165 + addCollectionFromDir(config, 'elsewhere'); 166 166 167 - config.addCollection('latest', latest) 168 - config.addCollection('updated', mostRecentlyUpdated) 169 - config.addCollection('featured', featured) 167 + config.addCollection('latest', latest); 168 + config.addCollection('updated', mostRecentlyUpdated); 169 + config.addCollection('featured', featured); 170 170 171 - config.setLibrary('md', markdown) 171 + config.setLibrary('md', markdown); 172 172 173 - config.setDataDeepMerge(true) 173 + config.setDataDeepMerge(true); 174 174 175 175 return { 176 176 dir: { ··· 183 183 dataTemplateEngine: 'njk', 184 184 htmlTemplateEngine: 'njk', 185 185 markdownTemplateEngine: 'njk', 186 - } 186 + }; 187 187 } 188 188 189 189 // Needs to be this way so that the import resolves as expected in `.eleventy.js`. 190 - module.exports = config 190 + module.exports = config;
+6 -6
eleventy/copyright.ts
··· 1 - const ROLLOVER_DATE = new Date('January 1, 2020') 2 - const BASE = 'copyright Chris Krycho, 2019' 1 + const ROLLOVER_DATE = new Date('January 1, 2020'); 2 + const BASE = 'copyright Chris Krycho, 2019'; 3 3 const LICENSE = { 4 4 content: { 5 5 name: 'Creative Commons Attribution 4.0', ··· 11 11 url: 12 12 'https://github.com/chriskrycho/v5.chriskrycho.com/blob/master/LICENSE.md#software', 13 13 }, 14 - } 14 + }; 15 15 16 16 const copyrightDate = (date: Date): string => 17 - date >= ROLLOVER_DATE ? `${BASE}–${date.getFullYear()}` : BASE 17 + date >= ROLLOVER_DATE ? `${BASE}–${date.getFullYear()}` : BASE; 18 18 19 19 const copyright = (date: Date, license: keyof typeof LICENSE = 'content'): string => 20 20 `${copyrightDate(date)} under a <a href='${LICENSE[license].url}'>${ 21 21 LICENSE[license].name 22 - }</a> license` 22 + }</a> license`; 23 23 24 - export default copyright 24 + export default copyright;
+3 -3
eleventy/current-page.ts
··· 1 - import { Page, Item } from '../types/eleventy' 1 + import { Page, Item } from '../types/eleventy'; 2 2 3 3 export const currentPage = (allItems: Item[], page: Page): Item | undefined => 4 - allItems.find((item) => item.url === page.url) 4 + allItems.find((item) => item.url === page.url); 5 5 6 - export default currentPage 6 + export default currentPage;
+13 -13
eleventy/date-time.ts
··· 1 - import { DateTime, DateTimeOptions } from 'luxon' 1 + import { DateTime, DateTimeOptions } from 'luxon'; 2 2 3 - type Parse = (text: string, options?: DateTimeOptions | undefined) => DateTime 3 + type Parse = (text: string, options?: DateTimeOptions | undefined) => DateTime; 4 4 5 5 const maybeDateTime = (parse: Parse, input: string): DateTime | null => { 6 - const parsed = parse(input) 7 - return parsed.isValid ? parsed : null 8 - } 6 + const parsed = parse(input); 7 + return parsed.isValid ? parsed : null; 8 + }; 9 9 10 - export const TZ = { zone: 'America/Denver' } 10 + export const TZ = { zone: 'America/Denver' }; 11 11 12 12 // Same parsing rules as 11ty itself uses: ISO or SQL, nothing else. 13 13 export const toDateTime = (input: string): DateTime => { 14 14 const dateTime = 15 15 maybeDateTime((s) => DateTime.fromISO(s, TZ), input) ?? 16 - maybeDateTime((s) => DateTime.fromSQL(s, TZ), input) 17 - if (!dateTime) throw new Error(`Could not parse date: ${input}`) 18 - return dateTime 19 - } 16 + maybeDateTime((s) => DateTime.fromSQL(s, TZ), input); 17 + if (!dateTime) throw new Error(`Could not parse date: ${input}`); 18 + return dateTime; 19 + }; 20 20 21 21 export const canParseDate = (date: unknown): date is string | Date => 22 - typeof date === 'string' || date instanceof Date 22 + typeof date === 'string' || date instanceof Date; 23 23 24 24 export const fromDateOrString = (date: Date | string): DateTime => 25 - typeof date === 'string' ? toDateTime(date) : DateTime.fromJSDate(date, TZ) 25 + typeof date === 'string' ? toDateTime(date) : DateTime.fromJSDate(date, TZ); 26 26 27 - export default toDateTime 27 + export default toDateTime;
+2 -2
eleventy/excluding-collection.ts
··· 1 - import { Item } from '../types/eleventy' 1 + import { Item } from '../types/eleventy'; 2 2 3 3 export default function excludingCollection(items: Item[], collection: Item[]): Item[] { 4 - return items.filter((item) => !collection.includes(item)) 4 + return items.filter((item) => !collection.includes(item)); 5 5 }
+91 -91
eleventy/feed.ts
··· 1 - import stripTags from 'striptags' 1 + import stripTags from 'striptags'; 2 2 3 - import { Dict, EleventyClass, Item } from '../types/eleventy' 4 - import JsonFeed, { FeedItem } from '../types/json-feed' 5 - import absoluteUrl from './absolute-url' 6 - import { canParseDate } from './date-time' 7 - import isoDate from './iso-date' 8 - import siteTitle from './site-title' 9 - import toCollection from './to-collection' 10 - import markdown from './markdown' 11 - import localeDate from './locale-date' 12 - import { DateTime } from 'luxon' 3 + import { Dict, EleventyClass, Item } from '../types/eleventy'; 4 + import JsonFeed, { FeedItem } from '../types/json-feed'; 5 + import absoluteUrl from './absolute-url'; 6 + import { canParseDate } from './date-time'; 7 + import isoDate from './iso-date'; 8 + import siteTitle from './site-title'; 9 + import toCollection from './to-collection'; 10 + import markdown from './markdown'; 11 + import localeDate from './locale-date'; 12 + import { DateTime } from 'luxon'; 13 13 14 - type BuildInfo = typeof import('../site/_data/build') 15 - type SiteConfig = typeof import('../site/_data/config') 14 + type BuildInfo = typeof import('../site/_data/build'); 15 + type SiteConfig = typeof import('../site/_data/config'); 16 16 17 17 /** Defensive function in case handed bad data */ 18 18 const optionalString = (value: unknown): string | undefined => 19 - typeof value === 'string' ? value : undefined 19 + typeof value === 'string' ? value : undefined; 20 20 21 21 interface Book { 22 - title: string 23 - author: string 24 - year?: number | string 22 + title: string; 23 + author: string; 24 + year?: number | string; 25 25 review?: { 26 26 rating: 27 27 | 'Required' 28 28 | 'Recommended' 29 29 | 'Recommended With Qualifications' 30 - | 'Not Recommended' 31 - summary: string 32 - } 33 - cover?: string 34 - link?: string 30 + | 'Not Recommended'; 31 + summary: string; 32 + }; 33 + cover?: string; 34 + link?: string; 35 35 } 36 36 37 37 /** Extending the base Eleventy item with my own data */ 38 38 declare module '../types/eleventy' { 39 39 interface Data { 40 - title?: string 41 - subtitle?: string 42 - summary?: string 43 - tags?: string[] 44 - date?: string | Date 45 - updated?: string | Date 40 + title?: string; 41 + subtitle?: string; 42 + summary?: string; 43 + tags?: string[]; 44 + date?: string | Date; 45 + updated?: string | Date; 46 46 qualifiers?: { 47 - audience?: string 48 - epistemic?: string 49 - } 50 - image?: string 51 - link?: string 52 - splash?: string 53 - book?: Book 54 - standalonePage?: boolean 55 - featured?: boolean 56 - draft?: boolean 47 + audience?: string; 48 + epistemic?: string; 49 + }; 50 + image?: string; 51 + link?: string; 52 + splash?: string; 53 + book?: Book; 54 + standalonePage?: boolean; 55 + featured?: boolean; 56 + draft?: boolean; 57 57 /** 58 58 * Allow overriding the normal feed ID to enable keeping feed entries stable even if 59 59 * the slug changes. 60 60 */ 61 - feedId?: string 61 + feedId?: string; 62 62 } 63 63 } 64 64 ··· 70 70 | 'bigint' 71 71 | 'string' 72 72 | 'symbol' 73 - | 'function' 73 + | 'function'; 74 74 75 75 function hasType<T extends TypeOf>(type: T, item: unknown): item is T { 76 - return typeof item === type 76 + return typeof item === type; 77 77 } 78 78 79 79 function isBook(maybeBook: unknown): maybeBook is Book { 80 80 if (typeof maybeBook !== 'object' || !maybeBook) { 81 - return false 81 + return false; 82 82 } 83 83 84 - const maybe = maybeBook as Dict<unknown> 84 + const maybe = maybeBook as Dict<unknown>; 85 85 86 86 return ( 87 87 typeof maybe.title === 'string' && ··· 90 90 hasType('object', maybe.review) && 91 91 hasType('string', maybe.cover) && 92 92 hasType('string', maybe.link) 93 - ) 93 + ); 94 94 } 95 95 96 96 function describe(book: Book): string { 97 97 const linked = (content: string): string => 98 - book.link ? `<a href='${book.link}' rel='nofollow'>${content}</a>` : content 98 + book.link ? `<a href='${book.link}' rel='nofollow'>${content}</a>` : content; 99 99 100 - const year = book.year ? ` (${book.year})` : '' 100 + const year = book.year ? ` (${book.year})` : ''; 101 101 102 - const title = linked(`<cite>${book.title}</cite>`) 103 - const bookInfo = `<p>${title}, ${book.author}${year}</p>` 102 + const title = linked(`<cite>${book.title}</cite>`); 103 + const bookInfo = `<p>${title}, ${book.author}${year}</p>`; 104 104 const review = book.review 105 105 ? `<p><b>${book.review.rating}:</b> ${book.review.summary}</p>` 106 - : '' 106 + : ''; 107 107 108 - return `${bookInfo}\n${review}` 108 + return `${bookInfo}\n${review}`; 109 109 } 110 110 111 111 function entryTitleFor(item: Item): string { 112 - return item.data?.title ?? localeDate(item.date, 'yyyy.MM.dd.HHmm') 112 + return item.data?.title ?? localeDate(item.date, 'yyyy.MM.dd.HHmm'); 113 113 } 114 114 115 115 function contentHtmlFor( ··· 120 120 const subtitle = 121 121 typeof item.data?.subtitle === 'string' 122 122 ? `<p><i>${markdown.renderInline(item.data.subtitle)}</i></p>` 123 - : '' 123 + : ''; 124 124 125 125 const audience = 126 126 typeof item.data?.qualifiers?.audience === 'string' 127 127 ? `<p><b>Assumed audience:</b> ${markdown.renderInline( 128 128 item.data.qualifiers.audience, 129 129 )}</p>` 130 - : '' 130 + : ''; 131 131 132 132 const epistemicStatus = 133 133 typeof item.data?.qualifiers?.epistemic === 'string' 134 134 ? `<p><b>Epistemic status:</b> ${markdown.renderInline( 135 135 item.data.qualifiers.epistemic, 136 136 )}</p>` 137 - : '' 137 + : ''; 138 138 139 - const book = item.data?.book 140 - const bookInfo = isBook(book) ? describe(book) : '' 139 + const book = item.data?.book; 140 + const bookInfo = isBook(book) ? describe(book) : ''; 141 141 142 142 const reply = includeReplyViaEmail 143 143 ? ((): string => { 144 - const replySubject = encodeURIComponent(entryTitleFor(item)) 145 - const replyUrl = `mailto:${config.author.email}?subject=${replySubject}` 146 - return `<hr/><p><a href="${replyUrl}">Reply via email!</a></p>` 144 + const replySubject = encodeURIComponent(entryTitleFor(item)); 145 + const replyUrl = `mailto:${config.author.email}?subject=${replySubject}`; 146 + return `<hr/><p><a href="${replyUrl}">Reply via email!</a></p>`; 147 147 })() 148 - : '' 148 + : ''; 149 149 150 - return subtitle + audience + epistemicStatus + bookInfo + item.templateContent + reply 150 + return subtitle + audience + epistemicStatus + bookInfo + item.templateContent + reply; 151 151 } 152 152 153 153 function titleFor(item: Item): string | undefined { 154 - const sectionMarker = toCollection(item.inputPath) 155 - const { title } = item.data ?? {} 156 - return sectionMarker && title ? `[${sectionMarker}] ${title}` : undefined 154 + const sectionMarker = toCollection(item.inputPath); 155 + const { title } = item.data ?? {}; 156 + return sectionMarker && title ? `[${sectionMarker}] ${title}` : undefined; 157 157 } 158 158 159 159 function summaryFor(item: Item): string { 160 - return item.data?.summary ?? item.data?.subtitle ?? stripTags(item.templateContent) 160 + return item.data?.summary ?? item.data?.subtitle ?? stripTags(item.templateContent); 161 161 } 162 162 163 163 /** ··· 192 192 optionalString(item.data?.book?.cover) ?? 193 193 optionalString(item.data?.image), 194 194 } 195 - : null 195 + : null; 196 196 197 197 type JSONFeedConfig = { 198 - items: Item[] 199 - config: SiteConfig 200 - permalink: string 201 - title: string 202 - includeReplyViaEmail: boolean 203 - } 198 + items: Item[]; 199 + config: SiteConfig; 200 + permalink: string; 201 + title: string; 202 + includeReplyViaEmail: boolean; 203 + }; 204 204 205 205 /** 206 206 Generate a JSON Feed compliant object for a given set of items. ··· 228 228 DateTime.fromISO(b as string).toMillis() - 229 229 DateTime.fromISO(a as string).toMillis(), 230 230 ), 231 - }) 231 + }); 232 232 233 233 interface EleventyData { 234 234 collections: { 235 - all: Item[] 236 - [key: string]: Item[] | undefined 237 - } 238 - config: SiteConfig 239 - page: Item 240 - pages: BuildInfo[] 241 - permalink?: string 235 + all: Item[]; 236 + [key: string]: Item[] | undefined; 237 + }; 238 + config: SiteConfig; 239 + page: Item; 240 + pages: BuildInfo[]; 241 + permalink?: string; 242 242 } 243 243 244 - type ClassData = ReturnType<NonNullable<EleventyClass['data']>> 244 + type ClassData = ReturnType<NonNullable<EleventyClass['data']>>; 245 245 246 246 export class JSONFeed implements EleventyClass { 247 - declare collection?: string 248 - declare title?: string 249 - declare permalink?: string 247 + declare collection?: string; 248 + declare title?: string; 249 + declare permalink?: string; 250 250 251 - includeReplyViaEmail = true 251 + includeReplyViaEmail = true; 252 252 253 253 data(): ClassData { 254 254 return { ··· 258 258 return ( 259 259 this.permalink ?? 260 260 (this.collection ? `/${this.collection}/feed.json` : '/feed.json') 261 - ) 261 + ); 262 262 }, 263 - } 263 + }; 264 264 } 265 265 266 266 render({ collections, config, page }: EleventyData): string { 267 - const collection = this.collection ?? 'live' 268 - const title = this.title ?? config.title.normal 267 + const collection = this.collection ?? 'live'; 268 + const title = this.title ?? config.title.normal; 269 269 return JSON.stringify( 270 270 jsonFeed({ 271 271 items: collections[collection] ?? [], ··· 274 274 title, 275 275 includeReplyViaEmail: this.includeReplyViaEmail, 276 276 }), 277 - ) 277 + ); 278 278 } 279 279 } 280 280 281 - export default JSONFeed 281 + export default JSONFeed;
+3 -3
eleventy/iso-date.ts
··· 1 - import { fromDateOrString } from './date-time' 1 + import { fromDateOrString } from './date-time'; 2 2 3 3 const isoDate = (date: Date | string): string => 4 - fromDateOrString(date).toISO({ includeOffset: true }) 4 + fromDateOrString(date).toISO({ includeOffset: true }); 5 5 6 - export default isoDate 6 + export default isoDate;
+4 -4
eleventy/locale-date.ts
··· 1 - import { fromDateOrString } from './date-time' 2 - import { TZ_OPTIONS } from './archive-by-year' 1 + import { fromDateOrString } from './date-time'; 2 + import { TZ_OPTIONS } from './archive-by-year'; 3 3 4 4 export const localeDate = (date: Date | string, format = 'DDD'): string => 5 - fromDateOrString(date).toFormat(format, TZ_OPTIONS) 5 + fromDateOrString(date).toFormat(format, TZ_OPTIONS); 6 6 7 - export default localeDate 7 + export default localeDate;
+31 -31
eleventy/markdown.ts
··· 1 - import { getLanguage, highlight as _highlight } from 'highlight.js' 2 - import markdownIt from 'markdown-it' 3 - import abbr from 'markdown-it-abbr' 4 - import anchor, { AnchorOptions } from 'markdown-it-anchor' 5 - import defList from 'markdown-it-deflist' 6 - import footnotes from 'markdown-it-footnote' 7 - import implicitFigures from 'markdown-it-implicit-figures' 8 - import sup from 'markdown-it-sup' 9 - import Core from 'markdown-it/lib/parser_core' 10 - import Token from 'markdown-it/lib/token' 11 - import { env } from 'process' 12 - import { Result } from 'true-myth' 13 - import slugify from 'uslug' 1 + import { getLanguage, highlight as _highlight } from 'highlight.js'; 2 + import markdownIt from 'markdown-it'; 3 + import abbr from 'markdown-it-abbr'; 4 + import anchor, { AnchorOptions } from 'markdown-it-anchor'; 5 + import defList from 'markdown-it-deflist'; 6 + import footnotes from 'markdown-it-footnote'; 7 + import implicitFigures from 'markdown-it-implicit-figures'; 8 + import sup from 'markdown-it-sup'; 9 + import Core from 'markdown-it/lib/parser_core'; 10 + import Token from 'markdown-it/lib/token'; 11 + import { env } from 'process'; 12 + import { Result } from 'true-myth'; 13 + import slugify from 'uslug'; 14 14 15 15 type HighlightError = { 16 - short: string 17 - long: string 18 - } 16 + short: string; 17 + long: string; 18 + }; 19 19 20 20 function highlight( 21 21 languageName: string, ··· 27 27 long: `error highlighting '${languageName}' with highlight.js\ncontent:\n${content}\n`, 28 28 }, 29 29 () => _highlight(languageName, content).value, 30 - ) 30 + ); 31 31 } 32 32 33 33 function logErr(err: HighlightError): void { 34 - console.error(env['DEBUG'] ? err.long : err.short) 34 + console.error(env['DEBUG'] ? err.long : err.short); 35 35 } 36 36 37 37 /** ··· 51 51 content: opts.permalinkSymbol, 52 52 }), 53 53 new Token('span_close', 'span', -1), 54 - ] 54 + ]; 55 55 56 56 const openTokens = [ 57 57 Object.assign(new Token('link_open', 'a', 1), { ··· 60 60 ['href', opts.permalinkHref?.(slug)], 61 61 ], 62 62 }), 63 - ] 63 + ]; 64 64 65 - const closeTokens = [...marker, new Token('link_close', 'a', -1)] 65 + const closeTokens = [...marker, new Token('link_close', 'a', -1)]; 66 66 67 - state.tokens[idx + 1].children?.unshift(...openTokens) 68 - state.tokens[idx + 1].children?.push(...closeTokens) 67 + state.tokens[idx + 1].children?.unshift(...openTokens); 68 + state.tokens[idx + 1].children?.push(...closeTokens); 69 69 } 70 70 71 71 const md = markdownIt({ ··· 73 73 highlight: (str, lang) => 74 74 lang && getLanguage(lang) 75 75 ? highlight(lang, str).unwrapOrElse((e) => { 76 - logErr(e) 77 - return str 76 + logErr(e); 77 + return str; 78 78 }) 79 79 : str, 80 80 }) ··· 92 92 renderPermalink, 93 93 slugify, 94 94 }) 95 - .use(abbr) 95 + .use(abbr); 96 96 97 97 md.renderer.rules.footnote_caption = (tokens, idx): string => { 98 - let n = Number(tokens[idx].meta.id + 1).toString() 98 + let n = Number(tokens[idx].meta.id + 1).toString(); 99 99 100 100 if (tokens[idx].meta.subId > 0) { 101 - n += ':' + tokens[idx].meta.subId 101 + n += ':' + tokens[idx].meta.subId; 102 102 } 103 103 104 - return n 105 - } 104 + return n; 105 + }; 106 106 107 - export default md 107 + export default md;
+5 -5
eleventy/page-links.ts
··· 1 - import SiteConfig from '../site/_data/config' 1 + import SiteConfig from '../site/_data/config'; 2 2 3 - const corrected = (path: string): string => path.replace(/^\.\//, '') 3 + const corrected = (path: string): string => path.replace(/^\.\//, ''); 4 4 5 5 /** Link to the history of the file on GitHub */ 6 6 export const history = (path: string): string => 7 - `${SiteConfig.repo}/commits/main/${corrected(path)}` 7 + `${SiteConfig.repo}/commits/main/${corrected(path)}`; 8 8 9 9 /** Link to edit the file on GitHub */ 10 10 export const edit = (inputPath: string): string => 11 - `${SiteConfig.repo}/edit/main/${corrected(inputPath)}` 11 + `${SiteConfig.repo}/edit/main/${corrected(inputPath)}`; 12 12 13 13 /** Link to view the file on GitHub */ 14 14 export const source = (inputPath: string): string => 15 - `${SiteConfig.repo}/blob/main/${corrected(inputPath)}` 15 + `${SiteConfig.repo}/blob/main/${corrected(inputPath)}`;
+18 -18
eleventy/plugin-spacewell.ts
··· 1 - import spacewell, { Options } from '../lib/spacewell' 2 - import { Config } from 'eleventy' 3 - import cheerio from 'cheerio' 1 + import spacewell, { Options } from '../lib/spacewell'; 2 + import { Config } from 'eleventy'; 3 + import cheerio from 'cheerio'; 4 4 5 - type Plugin = (eleventyConfig: Config, pluginNamespace?: string) => string | void 6 - type Content = Parameters<Config['addTransform']>[0] 5 + type Plugin = (eleventyConfig: Config, pluginNamespace?: string) => string | void; 6 + type Content = Parameters<Config['addTransform']>[0]; 7 7 8 - const PAGE_CONTENT_SELECTOR = '.content' 8 + const PAGE_CONTENT_SELECTOR = '.content'; 9 9 10 10 export default function plugin(options: Options): Plugin { 11 - const wellSpaced = spacewell(options) 11 + const wellSpaced = spacewell(options); 12 12 13 13 const transform: Plugin = (eleventyConfig, pluginNamespace) => { 14 14 const t = (content: Content, outputPath: string): string => { 15 15 if (!outputPath.endsWith('.html')) { 16 - return content 16 + return content; 17 17 } 18 18 19 - const dom = cheerio.load(content) 20 - const pageContent = dom.html(PAGE_CONTENT_SELECTOR) 19 + const dom = cheerio.load(content); 20 + const pageContent = dom.html(PAGE_CONTENT_SELECTOR); 21 21 22 22 if (pageContent) { 23 - dom(PAGE_CONTENT_SELECTOR).replaceWith(wellSpaced(pageContent)) 24 - return dom.html() 23 + dom(PAGE_CONTENT_SELECTOR).replaceWith(wellSpaced(pageContent)); 24 + return dom.html(); 25 25 } 26 26 27 - return content 28 - } 27 + return content; 28 + }; 29 29 30 30 return pluginNamespace 31 31 ? eleventyConfig.namespace(pluginNamespace, () => { 32 - eleventyConfig.addTransform('spacewell', t) 32 + eleventyConfig.addTransform('spacewell', t); 33 33 }) 34 - : eleventyConfig.addTransform('spacewell', t) 35 - } 34 + : eleventyConfig.addTransform('spacewell', t); 35 + }; 36 36 37 - return transform 37 + return transform; 38 38 }
+9 -9
eleventy/plugin-typeset.ts
··· 1 - import typeset, { Options } from 'typeset' 2 - import { Config } from 'eleventy' 1 + import typeset, { Options } from 'typeset'; 2 + import { Config } from 'eleventy'; 3 3 4 - type Plugin = (eleventyConfig: Config, pluginNamespace?: string) => string | void 5 - type Content = Parameters<Config['addTransform']>[0] 4 + type Plugin = (eleventyConfig: Config, pluginNamespace?: string) => string | void; 5 + type Content = Parameters<Config['addTransform']>[0]; 6 6 7 7 export default function plugin(options: Options): Plugin { 8 8 const transform: Plugin = (eleventyConfig, pluginNamespace) => { 9 9 const t = (content: Content, outputPath: string): string => 10 - outputPath.endsWith('.html') ? typeset(content, options) : content 10 + outputPath.endsWith('.html') ? typeset(content, options) : content; 11 11 12 12 return pluginNamespace 13 13 ? eleventyConfig.namespace(pluginNamespace, () => { 14 - eleventyConfig.addTransform('typeset', t) 14 + eleventyConfig.addTransform('typeset', t); 15 15 }) 16 - : eleventyConfig.addTransform('typeset', t) 17 - } 16 + : eleventyConfig.addTransform('typeset', t); 17 + }; 18 18 19 - return transform 19 + return transform; 20 20 }
+9 -9
eleventy/site-title.ts
··· 1 - import SiteConfig from '../site/_data/config' 2 - type SiteConfig = typeof SiteConfig 1 + import SiteConfig from '../site/_data/config'; 2 + type SiteConfig = typeof SiteConfig; 3 3 4 - const SEP = ' — ' 4 + const SEP = ' — '; 5 5 6 6 const baseTitle = (siteName: string, authorName: string): string => 7 - `${siteName}, by ${authorName}` 7 + `${siteName}, by ${authorName}`; 8 8 9 - const extended = (base: string, itemTitle: string): string => `${itemTitle}${SEP}${base}` 9 + const extended = (base: string, itemTitle: string): string => `${itemTitle}${SEP}${base}`; 10 10 11 11 export const siteTitle = (pageTitle: string | undefined, config: SiteConfig): string => { 12 - const base = baseTitle(config.title.normal, config.author.name) 12 + const base = baseTitle(config.title.normal, config.author.name); 13 13 return pageTitle && pageTitle !== config.title.normal 14 14 ? extended(base, pageTitle) 15 - : base 16 - } 15 + : base; 16 + }; 17 17 18 - export default siteTitle 18 + export default siteTitle;
+5 -5
eleventy/to-collection.ts
··· 1 - import path from 'path' 1 + import path from 'path'; 2 2 3 - const EXCLUDES = ['.', 'site'] 3 + const EXCLUDES = ['.', 'site']; 4 4 5 - const excluded = (entry: string): boolean => !EXCLUDES.includes(entry) 5 + const excluded = (entry: string): boolean => !EXCLUDES.includes(entry); 6 6 7 7 /** Get the collection corresponding to a given path slug */ 8 8 export function toCollection(slug: string): string | undefined { 9 - return path.dirname(slug.trim()).split(path.sep).filter(excluded)[0] 9 + return path.dirname(slug.trim()).split(path.sep).filter(excluded)[0]; 10 10 } 11 11 12 - export default toCollection 12 + export default toCollection;
+5 -5
eleventy/utils.ts
··· 1 - import { env } from 'process' 1 + import { env } from 'process'; 2 2 3 3 interface ToString { 4 - toString(): string 4 + toString(): string; 5 5 } 6 6 7 - export const toString = (a: ToString): string => a.toString() 7 + export const toString = (a: ToString): string => a.toString(); 8 8 9 9 export const logErr = (err: unknown): void => { 10 10 if (env['DEBUG']) { 11 - console.error(err) 11 + console.error(err); 12 12 } 13 - } 13 + };
+18 -18
gulpfile.js
··· 1 - const { src, dest, parallel, series, watch } = require('gulp') 2 - const sass = require('gulp-sass') 3 - const del = require('del') 1 + const { src, dest, parallel, series, watch } = require('gulp'); 2 + const sass = require('gulp-sass'); 3 + const del = require('del'); 4 4 5 - sass.compiler = require('sass') 5 + sass.compiler = require('sass'); 6 6 7 7 const build = (file) => () => 8 8 src(file) ··· 14 14 }) 15 15 .on('error', sass.logError), 16 16 ) 17 - .pipe(dest('./site/_styles')) 17 + .pipe(dest('./site/_styles')); 18 18 19 19 function style() { 20 - return build('./site/_includes/styles/style.scss')() 20 + return build('./site/_includes/styles/style.scss')(); 21 21 } 22 22 23 23 function print() { 24 - return build('./site/_includes/styles/print.scss')() 24 + return build('./site/_includes/styles/print.scss')(); 25 25 } 26 26 27 27 function fonts() { 28 - return build('./site/_includes/styles/fonts.scss')() 28 + return build('./site/_includes/styles/fonts.scss')(); 29 29 } 30 30 31 31 function clean() { ··· 33 33 './site/_includes/styles/style.css', 34 34 './site/_includes/styles/fonts.css', 35 35 './site/_includes/styles/print.css', 36 - ]) 36 + ]); 37 37 } 38 38 39 - const all = parallel(style, fonts, print) 39 + const all = parallel(style, fonts, print); 40 40 41 41 function watchStyles() { 42 - watch('./site/_includes/styles/**/*.scss', all) 42 + watch('./site/_includes/styles/**/*.scss', all); 43 43 } 44 44 45 - exports.clean = clean 46 - exports.style = style 47 - exports.fonts = fonts 48 - exports.print = print 49 - exports.watch = watchStyles 50 - exports.all = all 51 - exports.default = series(clean, all) 45 + exports.clean = clean; 46 + exports.style = style; 47 + exports.fonts = fonts; 48 + exports.print = print; 49 + exports.watch = watchStyles; 50 + exports.all = all; 51 + exports.default = series(clean, all);
+21 -21
lib/spacewell.ts
··· 1 - import { logErr } from '../eleventy/utils' 1 + import { logErr } from '../eleventy/utils'; 2 2 3 - const THIN_SP = '&thinsp;' 3 + const THIN_SP = '&thinsp;'; 4 4 // const HAIR_SP = '&hairsp;' 5 - const EM_DASH = '&mdash;' 6 - const EN_DASH = '&ndash;' 5 + const EM_DASH = '&mdash;'; 6 + const EN_DASH = '&ndash;'; 7 7 8 8 /** 9 9 Wrap em dashes and their immediate neighbors in non-breaking span and hair spaces. ··· 13 13 return content.replace( 14 14 /(—|&mdash;|&#8212;|&#x2014;)/g, 15 15 `${THIN_SP}${EM_DASH}${THIN_SP}`, 16 - ) 16 + ); 17 17 } 18 18 19 19 /** ··· 22 22 variant is used. 23 23 */ 24 24 export function enDashes(content: string): string { 25 - const OPEN = '<dash-wrap>' 26 - const CLOSE = '</dash-wrap>' 25 + const OPEN = '<dash-wrap>'; 26 + const CLOSE = '</dash-wrap>'; 27 27 28 28 // Do numbers first. Include a variety of ways digits might be constructed, 29 29 // including e.g. Bible verses, other punctuation, etc. 30 - const numPatt = /([\d:.⅒⅑⅛⅜⅝⅞⅐⅙⅚⅕⅖⅗⅘¼¾⅓⅔½]+) ?(–|&ndash;|&8211;|&#x2013;) ?([\d:.⅒⅑⅛⅜⅝⅞⅐⅙⅚⅕⅖⅗⅘¼¾⅓⅔½]+)/g 31 - const wordPatt = /(\w+) ?(–|&ndash;|&8211;|&x2013;) ?(\w+)/g 32 - const replacement = `${OPEN}$1${THIN_SP}${EN_DASH}${THIN_SP}$3${CLOSE}` 30 + const numPatt = /([\d:.⅒⅑⅛⅜⅝⅞⅐⅙⅚⅕⅖⅗⅘¼¾⅓⅔½]+) ?(–|&ndash;|&8211;|&#x2013;) ?([\d:.⅒⅑⅛⅜⅝⅞⅐⅙⅚⅕⅖⅗⅘¼¾⅓⅔½]+)/g; 31 + const wordPatt = /(\w+) ?(–|&ndash;|&8211;|&x2013;) ?(\w+)/g; 32 + const replacement = `${OPEN}$1${THIN_SP}${EN_DASH}${THIN_SP}$3${CLOSE}`; 33 33 34 - return content.replace(numPatt, replacement).replace(wordPatt, replacement) 34 + return content.replace(numPatt, replacement).replace(wordPatt, replacement); 35 35 } 36 36 37 37 /** ··· 43 43 // sentences. Basically, I *think* it should just be anytime 44 44 // that the period follows a capital letter, but there may be 45 45 // the occasional exception. 46 - logErr('`spacewell#initials()` not yet implemented.') 47 - return content 46 + logErr('`spacewell#initials()` not yet implemented.'); 47 + return content; 48 48 } 49 49 50 50 // NOTE: keys are mapped to names of functions in the module. ··· 52 52 emDashes, 53 53 enDashes, 54 54 initials, 55 - } as const 55 + } as const; 56 56 57 57 export interface Options { 58 - emDashes?: boolean 59 - enDashes?: boolean 60 - initials?: boolean 58 + emDashes?: boolean; 59 + enDashes?: boolean; 60 + initials?: boolean; 61 61 } 62 62 63 63 /** ··· 66 66 @param options Options for which spacing rules to use. 67 67 @param content A document element to apply rules to. 68 68 */ 69 - export default function spacewell(options: Options): (content: string) => string 70 - export default function spacewell(options: Options, content: string): string 69 + export default function spacewell(options: Options): (content: string) => string; 70 + export default function spacewell(options: Options, content: string): string; 71 71 export default function spacewell( 72 72 options: Options, 73 73 content?: string, ··· 75 75 function op(c: string): string { 76 76 return (Object.keys(options) as Array<keyof Options>) 77 77 .filter((key) => Boolean(options[key])) 78 - .reduce((transformed, cfgKey) => FUNCTIONS[cfgKey](transformed), c) 78 + .reduce((transformed, cfgKey) => FUNCTIONS[cfgKey](transformed), c); 79 79 } 80 80 81 - return content ? op(content) : op 81 + return content ? op(content) : op; 82 82 }
-1
package-lock.json
··· 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 - "name": "v5.chriskrycho.com", 9 8 "version": "1.0.0", 10 9 "license": "MIT", 11 10 "devDependencies": {
-1
package.json
··· 82 82 }, 83 83 "prettier": { 84 84 "singleQuote": true, 85 - "semi": false, 86 85 "printWidth": 90, 87 86 "trailingComma": "all" 88 87 },
+1 -1
site/_data/build.js
··· 1 1 module.exports = { 2 2 date: new Date(), 3 - } 3 + };
+1 -1
site/_data/config.js
··· 24 24 'https://stackoverflow.com/users/564181/chris-krycho', 25 25 ], 26 26 }, 27 - } 27 + };
+1 -1
site/_data/pages.js
··· 12 12 { type: 'page', title: 'Speaking', path: '/speaking/' }, 13 13 { type: 'separator' }, 14 14 { type: 'page', title: 'Colophon', path: '/colophon/' }, 15 - ] 15 + ];
+4 -4
site/_feeds/elsewhere.11ty.js
··· 1 - import JSONFeed from '../../eleventy/feed' 1 + import JSONFeed from '../../eleventy/feed'; 2 2 3 3 module.exports = class ElsewhereFeed extends JSONFeed { 4 - collection = 'elsewhere' 5 - title = 'Elsewhere' 6 - } 4 + collection = 'elsewhere'; 5 + title = 'Elsewhere'; 6 + };
+4 -4
site/_feeds/essays.11ty.js
··· 1 - import JSONFeed from '../../eleventy/feed' 1 + import JSONFeed from '../../eleventy/feed'; 2 2 3 3 module.exports = class EssaysFeed extends JSONFeed { 4 - collection = 'essays' 5 - title = 'Essays' 6 - } 4 + collection = 'essays'; 5 + title = 'Essays'; 6 + };
+4 -4
site/_feeds/feed-without-reply.11ty.js
··· 1 - import JSONFeed from '../../eleventy/feed' 1 + import JSONFeed from '../../eleventy/feed'; 2 2 3 3 module.exports = class FeedWithoutReply extends JSONFeed { 4 - includeReplyViaEmail = false 5 - permalink = '/feed-without-reply.json' 6 - } 4 + includeReplyViaEmail = false; 5 + permalink = '/feed-without-reply.json'; 6 + };
+4 -4
site/_feeds/journal.11ty.js
··· 1 - import JSONFeed from '../../eleventy/feed' 1 + import JSONFeed from '../../eleventy/feed'; 2 2 3 3 module.exports = class JournalFeed extends JSONFeed { 4 - collection = 'journal' 5 - title = 'Journal' 6 - } 4 + collection = 'journal'; 5 + title = 'Journal'; 6 + };
+4 -4
site/_feeds/library.11ty.js
··· 1 - import JSONFeed from '../../eleventy/feed' 1 + import JSONFeed from '../../eleventy/feed'; 2 2 3 3 module.exports = class LibraryFeed extends JSONFeed { 4 - collection = 'library' 5 - title = 'Library' 6 - } 4 + collection = 'library'; 5 + title = 'Library'; 6 + };
+4 -4
site/_feeds/notes.11ty.js
··· 1 - import JSONFeed from '../../eleventy/feed' 1 + import JSONFeed from '../../eleventy/feed'; 2 2 3 3 module.exports = class NotesFeed extends JSONFeed { 4 - collection = 'notes' 5 - title = 'Notes' 6 - } 4 + collection = 'notes'; 5 + title = 'Notes'; 6 + };
+112 -109
types/eleventy.d.ts
··· 1 - type ServeStaticOptions = import('serve-static').ServeStaticOptions 1 + type ServeStaticOptions = import('serve-static').ServeStaticOptions; 2 2 3 3 // ---- Utility types 4 4 interface Dict<T = unknown> { 5 - [key: string]: T | undefined 5 + [key: string]: T | undefined; 6 6 } 7 7 8 8 // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 - type AnyFunction<T = any> = (...args: any[]) => T 9 + type AnyFunction<T = any> = (...args: any[]) => T; 10 10 11 11 // ---- Eleventy types 12 12 interface BrowserSyncConfig { ··· 14 14 ui?: 15 15 | false 16 16 | { 17 - port: number 17 + port: number; 18 18 weinre?: { 19 - port: number 20 - } 21 - } 19 + port: number; 20 + }; 21 + }; 22 22 23 23 files?: 24 24 | string 25 25 | Array< 26 26 | string 27 27 | { 28 - match?: string[] 29 - fn?: (event: unknown, file: string) => unknown 28 + match?: string[]; 29 + fn?: (event: unknown, file: string) => unknown; 30 30 } 31 31 > 32 - | false 32 + | false; 33 33 34 - watchEvents?: string[] 35 - watch?: boolean 36 - ignore?: string[] 37 - single?: boolean 34 + watchEvents?: string[]; 35 + watch?: boolean; 36 + ignore?: string[]; 37 + single?: boolean; 38 38 watchOptions?: { 39 - ignoreInitial?: boolean 40 - ignored?: boolean 41 - } 39 + ignoreInitial?: boolean; 40 + ignored?: boolean; 41 + }; 42 42 server?: 43 43 | boolean 44 44 | string 45 45 | string[] 46 46 | { 47 - baseDir?: string 48 - directory?: boolean 49 - serveStaticOptions?: ServeStaticOptions 50 - routes?: Dict<string> 51 - } 47 + baseDir?: string; 48 + directory?: boolean; 49 + serveStaticOptions?: ServeStaticOptions; 50 + routes?: Dict<string>; 51 + }; 52 52 53 53 /* eslint-disable @typescript-eslint/no-explicit-any */ 54 54 proxy?: 55 55 | string 56 56 | boolean 57 57 | { 58 - target?: string 59 - ws?: boolean 60 - middleware?: any 61 - reqHeaders?: string[] 62 - proxyReq?: any 63 - proxyRes?: any 64 - } 58 + target?: string; 59 + ws?: boolean; 60 + middleware?: any; 61 + reqHeaders?: string[]; 62 + proxyReq?: any; 63 + proxyRes?: any; 64 + }; 65 65 /* eslint-enable @typescript-eslint/no-explicit-any */ 66 66 } 67 67 68 - type Empty = { isEmpty: true; empty: string } | { isEmpty: false } 68 + type Empty = { isEmpty: true; empty: string } | { isEmpty: false }; 69 69 70 70 type GrayMatter = { 71 - content: string 72 - excerpt?: string 73 - orig: Buffer 74 - language: string 75 - matter: string 76 - stringify(): string 77 - } & Empty 71 + content: string; 72 + excerpt?: string; 73 + orig: Buffer; 74 + language: string; 75 + matter: string; 76 + stringify(): string; 77 + } & Empty; 78 78 79 - export type Engine = (input: string) => GrayMatter 79 + export type Engine = (input: string) => GrayMatter; 80 80 81 81 export type EngineName = 82 82 | 'html' ··· 90 90 | 'ejs' 91 91 | 'haml' 92 92 | 'pug' 93 - | 'jstl' 93 + | 'jstl'; 94 94 95 95 export interface Page { 96 96 /** the full path to the source input file (including the path to the input directory) */ 97 - inputPath: string 97 + inputPath: string; 98 98 /** 99 99 Mapped from the input file name, useful for permalinks. Read more about 100 100 [`fileSlug`]. 101 101 102 102 [`fileSlug`]: https://www.11ty.io/docs/data/#fileslug 103 103 */ 104 - fileSlug: string 104 + fileSlug: string; 105 105 /** the full path to the output file to be written for this content */ 106 - outputPath: string 106 + outputPath: string; 107 107 /** url used to link to this piece of content. */ 108 - url: string 108 + url: string; 109 109 /** 110 110 the resolved date used for sorting. Read more about [Content Dates]. 111 111 112 112 [Content Dates]: https://www.11ty.io/docs/dates/ 113 113 */ 114 - date: string | Date 114 + date: string | Date; 115 115 } 116 116 117 117 interface Data { 118 118 collections: { 119 - [key: string]: Item[] | undefined 120 - } 119 + [key: string]: Item[] | undefined; 120 + }; 121 121 } 122 122 123 123 /** An `Item` is just like a `Page`, but with the actual data from render available. */ 124 124 interface Item extends Page { 125 125 /** all data for this piece of content (includes any data inherited from layouts) */ 126 - data?: Data 126 + data?: Data; 127 127 128 128 /** the rendered content of this template. This does *not• include layout wrappers */ 129 - templateContent: string 129 + templateContent: string; 130 130 } 131 131 132 132 export interface Collection { 133 - getAll(): Item[] 133 + getAll(): Item[]; 134 134 135 135 /** 136 136 Note that while Array `.reverse()` mutates the array in-place, all Eleventy ··· 140 140 141 141 [warning]: https://www.11ty.io/docs/collections/#array-reverse 142 142 */ 143 - getAllSorted(): Item[] 143 + getAllSorted(): Item[]; 144 144 145 - getFilteredByTag(tagName: string): Item[] 145 + getFilteredByTag(tagName: string): Item[]; 146 146 147 - getFilteredByGlob(glob: string | string[]): Item[] 147 + getFilteredByGlob(glob: string | string[]): Item[]; 148 148 } 149 149 150 150 interface Renderer { 151 - render(input: string): string 151 + render(input: string): string; 152 152 } 153 153 154 - type Raw<T = unknown> = string | Buffer | Promise<T> 154 + type Raw<T = unknown> = string | Buffer | Promise<T>; 155 155 156 156 export interface EleventyClass { 157 157 data?: () => { 158 - excludeFromEleventyCollections?: boolean 159 - standalonePage?: boolean 160 - permalink?: (...args: unknown[]) => Raw 161 - [key: string]: unknown 162 - } 158 + excludeFromEleventyCollections?: boolean; 159 + standalonePage?: boolean; 160 + permalink?: (...args: unknown[]) => Raw; 161 + [key: string]: unknown; 162 + }; 163 163 164 - render(...args: unknown[]): Raw 164 + render(...args: unknown[]): Raw; 165 165 } 166 166 167 167 export interface Config { 168 168 dir?: { 169 169 /** Controls the top level directory/file/glob that we’ll use to look for templates. */ 170 - input?: string 170 + input?: string; 171 171 172 172 /** Controls the directory inside which the finished templates will be written to. */ 173 - output?: string 173 + output?: string; 174 174 175 175 /** 176 176 The includes directory is meant for [Eleventy layouts], include files, extends ··· 181 181 182 182 **Note:** This value is relative to your input directory. 183 183 */ 184 - includes?: string 184 + includes?: string; 185 185 186 186 /** 187 187 This configuration option is optional but useful if you want your [Eleventy ··· 202 202 203 203 **Note:** This value is relative to your input directory. 204 204 */ 205 - layouts?: string 205 + layouts?: string; 206 206 207 - data?: string 208 - } 207 + data?: string; 208 + }; 209 209 210 210 /** 211 211 The `data.dir` global data files run through this template engine before transforming ··· 213 213 214 214 [Global Data Files]: https://www.11ty.io/docs/data-global/ 215 215 */ 216 - dataTemplateEngine?: EngineName | false 217 - markdownTemplateEngine?: EngineName | false 218 - htmlTemplateEngine?: EngineName | false 219 - templateFormats?: EngineName[] 216 + dataTemplateEngine?: EngineName | false; 217 + markdownTemplateEngine?: EngineName | false; 218 + htmlTemplateEngine?: EngineName | false; 219 + templateFormats?: EngineName[]; 220 220 221 221 /** 222 222 If your site lives in a different subdirectory (particularly useful with GitHub ··· 225 225 structure. Leading or trailing slashes are all normalized away, so don’t worry about 226 226 it. 227 227 */ 228 - pathPrefix?: string 229 - passthroughFileCopy?: boolean 230 - htmlOutputSuffx?: string 231 - jsDataFileSuffix?: string 228 + pathPrefix?: string; 229 + passthroughFileCopy?: boolean; 230 + htmlOutputSuffx?: string; 231 + jsDataFileSuffix?: string; 232 232 233 233 addCollection( 234 234 name: string, 235 235 builder: ( 236 236 collection: Collection, 237 237 ) => Page[] | Record<string, unknown> | Promise<Record<string, unknown>>, 238 - ): void 238 + ): void; 239 239 240 - addFilter(name: string, filter: AnyFunction): string | void 240 + addFilter(name: string, filter: AnyFunction): string | void; 241 241 242 242 addTransform( 243 243 name: string, 244 244 transform: (content: string, outputPath: string) => string | Promise<string>, 245 - ): string 245 + ): string; 246 246 247 247 addLinter( 248 248 name: string, ··· 251 251 inputPath: string, 252 252 outputPath: string, 253 253 ) => void | Promise<void>, 254 - ): void 254 + ): void; 255 255 256 - addShortcode(name: string, shortcode: AnyFunction<string>): string 257 - addLiquidShortcode(name: string, shortcode: AnyFunction<string>): void 258 - addNunjucksShortcode(name: string, shortcode: AnyFunction<string>): void 259 - addHandlebarsShortcode(name: string, shortcode: AnyFunction<string>): void 260 - addJavascriptShortcode(name: string, shortcode: AnyFunction<string>): void 256 + addShortcode(name: string, shortcode: AnyFunction<string>): string; 257 + addLiquidShortcode(name: string, shortcode: AnyFunction<string>): void; 258 + addNunjucksShortcode(name: string, shortcode: AnyFunction<string>): void; 259 + addHandlebarsShortcode(name: string, shortcode: AnyFunction<string>): void; 260 + addJavascriptShortcode(name: string, shortcode: AnyFunction<string>): void; 261 261 addPairedShortcode( 262 262 name: string, 263 263 shortcode: <A>(content: string, ...args: A[]) => string, 264 - ): void 264 + ): void; 265 265 266 - addJavaScriptFunction(name: string, fn: AnyFunction<string>): void 266 + addJavaScriptFunction(name: string, fn: AnyFunction<string>): void; 267 267 268 268 addLiquidFilter( 269 269 name: string, 270 270 filter: <A>(...args: A[]) => unknown, 271 - ): Record<string, unknown> 271 + ): Record<string, unknown>; 272 272 273 - addNunjucksFilter(name: string, filter: <A>(...args: A[]) => unknown): void 273 + addNunjucksFilter(name: string, filter: <A>(...args: A[]) => unknown): void; 274 274 275 275 addNunjucksAsyncFilter( 276 276 name: string, 277 277 filter: <T>(value: T, callback: <E, R>(err: E | null, res: R) => unknown) => void, 278 - ): void 278 + ): void; 279 279 addNunjucksAsyncFilter( 280 280 name: string, 281 281 filter: <T, U>( ··· 283 283 value2: U, 284 284 callback: <E, R>(err: E | null, res: R) => unknown, 285 285 ) => void, 286 - ): void 286 + ): void; 287 287 addNunjucksAsyncFilter( 288 288 name: string, 289 289 filter: <T, U, V>( ··· 292 292 value3: V, 293 293 callback: <E, R>(err: E | null, res: R) => unknown, 294 294 ) => void, 295 - ): void 295 + ): void; 296 296 297 - addHandlebarsHelper(name: string, helper: AnyFunction<string>): Record<string, unknown> 297 + addHandlebarsHelper( 298 + name: string, 299 + helper: AnyFunction<string>, 300 + ): Record<string, unknown>; 298 301 299 302 /** 300 303 Plugins are custom code that Eleventy can import into a project from an external ··· 306 309 plugin’s documentation (e.g. the [eleventy-plugin-syntaxhighlight README](https://github.com/11ty/eleventy-plugin-syntaxhighlight/blob/master/README.md)) 307 310 to learn what options are available to you. 308 311 */ 309 - addPlugin<F extends AnyFunction>(fn: F, config?: Parameters<F>[0]): void 312 + addPlugin<F extends AnyFunction>(fn: F, config?: Parameters<F>[0]): void; 310 313 311 314 /** 312 315 Searching the entire directory structure for files to copy based on file extensions ··· 318 321 319 322 @param path The file path to copy (may be an individual file or directory.) 320 323 */ 321 - addPassthroughCopy(path: string): void 322 - addPassthroughCopy(mapping: { [inputPath: string]: string }): void 324 + addPassthroughCopy(path: string): void; 325 + addPassthroughCopy(mapping: { [inputPath: string]: string }): void; 323 326 324 327 /** 325 328 You can namespace parts of your configuration using `eleventyConfig.namespace`. This ··· 329 332 @param withName The string prefix to apply to the items. 330 333 @param context A callback in which to add your namespaced items. 331 334 */ 332 - namespace(withName: string, context: () => void): void 335 + namespace(withName: string, context: () => void): void; 333 336 334 - setTemplateFormats(to: EngineName[]): void 337 + setTemplateFormats(to: EngineName[]): void; 335 338 336 339 /** 337 340 * Opts in to a full deep merge when combining the Data Cascade. This will use ··· 346 349 * 347 350 * @param to `true` to enable deep merge, `false` (the current default) to opt out. 348 351 */ 349 - setDataDeepMerge(to: boolean): void 350 - setWatchJavaScriptDependencies(to: boolean): void 351 - setBrowserSyncConfig(to: BrowserSyncConfig): void 352 + setDataDeepMerge(to: boolean): void; 353 + setWatchJavaScriptDependencies(to: boolean): void; 354 + setBrowserSyncConfig(to: BrowserSyncConfig): void; 352 355 setFrontMatterParsingOptions(to: { 353 - excerpt?: boolean 354 - excerpt_separator?: string 355 - excerpt_alias?: string 356 - engines?: Dict<Engine> 357 - }): void 358 - setLibrary(to: EngineName, using: Renderer): void 356 + excerpt?: boolean; 357 + excerpt_separator?: string; 358 + excerpt_alias?: string; 359 + engines?: Dict<Engine>; 360 + }): void; 361 + setLibrary(to: EngineName, using: Renderer): void; 359 362 } 360 363 361 364 type NonMethodNames<T> = { 362 - [K in keyof T]: T[K] extends AnyFunction ? never : K 363 - }[keyof T] 365 + [K in keyof T]: T[K] extends AnyFunction ? never : K; 366 + }[keyof T]; 364 367 365 - type F = NonMethodNames<Config> 368 + type F = NonMethodNames<Config>; 366 369 367 - export type UserConfig = Pick<Config, NonNullable<NonMethodNames<Config>>> 370 + export type UserConfig = Pick<Config, NonNullable<NonMethodNames<Config>>>;
+39 -39
types/json-feed.d.ts
··· 3 3 The URL of the version of the format the feed uses. This should appear at the very 4 4 top, though we recognize that not all JSON generators allow for ordering. 5 5 */ 6 - version: 'https://jsonfeed.org/version/1' 6 + version: 'https://jsonfeed.org/version/1'; 7 7 8 8 /** 9 9 The name of the feed, which will often correspond to the name of the website (blog, 10 10 for instance), though not necessarily 11 11 */ 12 - title: string 12 + title: string; 13 13 14 14 /** 15 15 (optional but strongly recommended) The URL of the resource that the feed describes. ··· 18 18 required. But it may not make sense in the case of a file created on a desktop 19 19 computer, when that file is not shared or is shared only privately. 20 20 */ 21 - home_page_url?: string 21 + home_page_url?: string; 22 22 23 23 /** 24 24 (optional but strongly recommended) The URL of the feed, and serves as the unique 25 25 identifier for the feed. As with `home_page_url`, this should be considered required 26 26 for feeds on the public web. 27 27 */ 28 - feed_url?: string 28 + feed_url?: string; 29 29 30 30 /** 31 31 Provides more detail, beyond the `title`, on what the feed is about. A feed reader 32 32 may display this text. 33 33 */ 34 - description?: string 34 + description?: string; 35 35 36 36 /** 37 37 Description of the purpose of the feed. This is for the use of people looking at 38 38 the raw JSON, and should be ignored by feed readers. 39 39 */ 40 - user_comment?: string 40 + user_comment?: string; 41 41 42 42 /** 43 43 The URL of a feed that provides the next n items, where n is determined by the ··· 46 46 be the same as `feed_url`, and it must not be the same as a previous `next_url` 47 47 (to avoid infinite loops). 48 48 */ 49 - next_url?: string 49 + next_url?: string; 50 50 51 51 /** 52 52 The URL of an image for the feed suitable to be used in a timeline, much the way an ··· 54 54 so that it can be scaled-down and so that it can look good on retina displays. It 55 55 should use transparency where appropriate, since it may be rendered on a non-white background. 56 56 */ 57 - icon?: string 57 + icon?: string; 58 58 59 59 /** 60 60 The URL of an image for the feed suitable to be used in a source list. It should be ··· 62 62 on retina displays). As with `icon`, this image should use transparency where 63 63 appropriate, since it may be rendered on a non-white background. 64 64 */ 65 - favicon?: string 65 + favicon?: string; 66 66 67 67 /** 68 68 Specifies the feed author. The author object has several members. These are all 69 69 optional ― but if you provide an author object, then at least one is required. 70 70 */ 71 - author?: Author 71 + author?: Author; 72 72 73 73 /** 74 74 Says whether or not the feed is finished ― that is, whether or not it will ever ··· 76 76 could expire. If the value is true, then it’s expired. Any other value, or the 77 77 absence of expired, means the feed may continue to update. 78 78 */ 79 - expired?: boolean 79 + expired?: boolean; 80 80 81 81 /** 82 82 Describes endpoints that can be used to subscribe to real-time notifications from ··· 85 85 86 86 [“Subscribing to Real-time Notifications”]: https://jsonfeed.org/version/1#subscribing-to-real-time-notifications 87 87 */ 88 - hubs?: Hub[] 88 + hubs?: Hub[]; 89 89 90 90 /** The items in the feed. */ 91 - items: FeedItem[] 91 + items: FeedItem[]; 92 92 } 93 93 94 94 export interface FeedItem { ··· 99 99 coerce it to a string. Ideally, the `id` is the full URL of the resource described by 100 100 the item, since URLs make great unique identifiers. 101 101 */ 102 - id: string 102 + id: string; 103 103 104 104 /** 105 105 The URL of the resource described by the item. It’s the permalink. This may be the 106 106 same as the id ― but should be present regardless. 107 107 */ 108 - url?: string 108 + url?: string; 109 109 110 110 /** 111 111 The URL of a page elsewhere. This is especially useful for linkblogs. If `url` links 112 112 to where you’re talking about a thing, then `external_url` links to the thing you’re 113 113 talking about. 114 114 */ 115 - external_url?: string 115 + external_url?: string; 116 116 117 117 /** 118 118 Plain text. Microblog items in particular may omit titles. 119 119 */ 120 - title?: string 120 + title?: string; 121 121 122 122 /** 123 123 The plain text of the item. ··· 127 127 whichever makes sense for your resource. (It doesn’t even have to be the same for 128 128 each item in a feed.) 129 129 */ 130 - content_text?: string 130 + content_text?: string; 131 131 132 132 /** 133 133 The HTML of the item. Important: the only place HTML is allowed in this format is in ··· 138 138 whichever makes sense for your resource. (It doesn’t even have to be the same for 139 139 each item in a feed.) 140 140 */ 141 - content_html?: string 141 + content_html?: string; 142 142 143 143 /** 144 144 A plain text sentence or two describing the item. This might be presented in a 145 145 timeline, for instance, where a detail view would display all of `content_html` or 146 146 `content_text`. 147 147 */ 148 - summary?: string 148 + summary?: string; 149 149 150 150 /** 151 151 The URL of the main image for the item. This image may also appear in the ··· 153 153 featured image. Feed readers may use the image as a preview (probably resized as a 154 154 thumbnail and placed in a timeline). 155 155 */ 156 - image?: string 156 + image?: string; 157 157 158 158 /** 159 159 The URL of an image to use as a banner. Some blogging systems (such as [Medium]) ··· 164 164 165 165 [Medium]: https://medium.com/ 166 166 */ 167 - banner_image?: string 167 + banner_image?: string; 168 168 169 169 /** 170 170 Specifies the date in [RFC 3339] format. (Example: `2010-02-07T14:04:00-05:00`.) 171 171 172 172 [RFC 3339]: https://www.ietf.org/rfc/rfc3339.txt 173 173 */ 174 - date_published?: string 174 + date_published?: string; 175 175 176 176 /** 177 177 Specifies the modification date in [RFC 3339] format. 178 178 179 179 [RFC 3339]: https://www.ietf.org/rfc/rfc3339.txt 180 180 */ 181 - date_modified?: string 181 + date_modified?: string; 182 182 183 183 /** 184 184 The same structure as the top-level `author`. If not specified in an item, then the 185 185 top-level `author`, if present, is the author of the item. 186 186 */ 187 - author?: Author 187 + author?: Author; 188 188 189 189 /** 190 190 Any plain text values you want. Tags tend to be just one word, but they may be 191 191 anything. Note: they are not the equivalent of Twitter hashtags. Some blogging 192 192 systems and other feed formats call these categories. 193 193 */ 194 - tags?: string[] 194 + tags?: string[]; 195 195 196 196 /** 197 197 An individual item may have one or more attachments. List related resources. 198 198 Podcasts, for instance, would include an attachment that’s an audio or video file. 199 199 */ 200 - attachments?: Attachment[] 200 + attachments?: Attachment[]; 201 201 } 202 202 203 203 export interface Attachment { 204 204 /** Specifies the location of the attachment. */ 205 - url: string 205 + url: string; 206 206 207 207 /** Specifies the type of the attachment, such as “audio/mpeg.” */ 208 - mime_type: string 208 + mime_type: string; 209 209 210 210 /** 211 211 A name for the attachment. Important: if there are multiple attachments, and two or ··· 213 213 as alternate representations of the same thing. In this way a podcaster, for 214 214 instance, might provide an audio recording in different formats. 215 215 */ 216 - title?: string 216 + title?: string; 217 217 218 218 /** 219 219 Specifies how large the file is. 220 220 */ 221 - size_in_bytes?: number 221 + size_in_bytes?: number; 222 222 223 223 /** 224 224 Specifies how long it takes to listen to or watch, when played at normal speed 225 225 */ 226 - duration_in_seconds?: number 226 + duration_in_seconds?: number; 227 227 } 228 228 229 229 /** ··· 237 237 */ 238 238 export interface Author { 239 239 /** The author’s name */ 240 - name?: string 240 + name?: string; 241 241 242 242 /** 243 243 The URL of a site owned by the author. It could be a blog, micro-blog, Twitter ··· 245 245 but that’s not required. The URL could be a mailto: link, though we suspect that 246 246 will be rare. 247 247 */ 248 - url?: string 248 + url?: string; 249 249 250 250 /** 251 251 The URL for an image for the author. As with icon, it should be square and 252 252 relatively large ― such as 512 x 512 ― and should use transparency where 253 253 appropriate, since it may be rendered on a non-white background. 254 254 */ 255 - avatar?: string 255 + avatar?: string; 256 256 } 257 257 258 258 /** ··· 272 272 the JSON Feed website 273 273 */ 274 274 export interface Hub { 275 - type: string 276 - topic: string 277 - [key: string]: unknown 275 + type: string; 276 + topic: string; 277 + [key: string]: unknown; 278 278 } 279 279 280 - export default JsonFeed 280 + export default JsonFeed;
+2 -2
types/markdown-it-abbr.d.ts
··· 4 4 // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 5 // TypeScript Version: 2.3 6 6 7 - import MarkdownIt = require('markdown-it') 7 + import MarkdownIt = require('markdown-it'); 8 8 9 9 declare module 'markdown-it-abbr' { 10 - export default function abbr(md: MarkdownIt): void 10 + export default function abbr(md: MarkdownIt): void; 11 11 }
+2 -2
types/markdown-it-deflist.d.ts
··· 4 4 // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 5 // TypeScript Version: 2.3 6 6 7 - import MarkdownIt = require('markdown-it') 7 + import MarkdownIt = require('markdown-it'); 8 8 9 9 declare module 'markdown-it-deflist' { 10 - export default function defList(md: MarkdownIt): void 10 + export default function defList(md: MarkdownIt): void; 11 11 }
+2 -2
types/markdown-it-footnote.d.ts
··· 4 4 // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 5 // TypeScript Version: 2.3 6 6 7 - import MarkdownIt = require('markdown-it') 7 + import MarkdownIt = require('markdown-it'); 8 8 9 9 declare module 'markdown-it-footnote' { 10 - export default function footnote(md: MarkdownIt): void 10 + export default function footnote(md: MarkdownIt): void; 11 11 }
+6 -6
types/markdown-it-implicit-figures.d.ts
··· 4 4 // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 5 // TypeScript Version: 2.3 6 6 7 - import MarkdownIt = require('markdown-it') 7 + import MarkdownIt = require('markdown-it'); 8 8 9 9 declare module 'markdown-it-implicit-figures' { 10 10 export interface Options { ··· 13 13 `<figure data-type="image">`. This can be useful for applying special styling for 14 14 different kind of figures. 15 15 */ 16 - dataType?: boolean 16 + dataType?: boolean; 17 17 18 18 /** 19 19 Set `figcaption` to `true` to put the alternative text in a ··· 26 26 </figure> 27 27 ``` 28 28 */ 29 - figcaption?: boolean 29 + figcaption?: boolean; 30 30 31 31 /** 32 32 Set `tabindex` to `true` to add a `tabindex` property to each figure, beginning ··· 34 34 with [this css-trick](https://css-tricks.com/expanding-images-html5/), which 35 35 expands figures upon mouse-over. 36 36 */ 37 - tabindex?: boolean 37 + tabindex?: boolean; 38 38 39 39 /** 40 40 Put a link around the image if there is none yet. For example: ··· 43 43 <a href="img.png"><img src="img.png"></a> 44 44 ``` 45 45 */ 46 - link?: boolean 46 + link?: boolean; 47 47 } 48 48 49 49 /** ··· 70 70 <figure><a href="page.html"><img src="fig.png" alt=""></a></figure> 71 71 ``` 72 72 */ 73 - export default function implicitFigures(md: MarkdownIt, options?: Options): void 73 + export default function implicitFigures(md: MarkdownIt, options?: Options): void; 74 74 }
+3 -3
types/markdown-it-sup.d.ts
··· 4 4 // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 5 // TypeScript Version: 2.3 6 6 7 - import MarkdownIt = require('markdown-it') 7 + import MarkdownIt = require('markdown-it'); 8 8 9 - declare function sup(md: MarkdownIt): void 10 - export = sup 9 + declare function sup(md: MarkdownIt): void; 10 + export = sup;
+6 -6
types/typeset.d.ts
··· 5 5 | 'smallCaps' 6 6 | 'punctuation' 7 7 | 'hangingPunctuation' 8 - | 'spaces' 8 + | 'spaces'; 9 9 10 10 export type Options = { 11 11 /** string of a CSS selector to skip */ 12 - ignore?: string 12 + ignore?: string; 13 13 /** string of a CSS selector to only apply typeset */ 14 - only?: string 14 + only?: string; 15 15 /** array of features to disable */ 16 - disable?: OptionName[] 17 - } 16 + disable?: OptionName[]; 17 + }; 18 18 19 - export default function typeset(html: string, options?: Options): string 19 + export default function typeset(html: string, options?: Options): string;